/*-
 * Copyright (c) 1992, 1993, 1994
 *	The Regents of the University of California.  All rights reserved.
 * Copyright (c) 1994, 1995
 *	Keith Bostic.  All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. All advertising materials mentioning features or use of this software
 *    must display the following acknowledgement:
 *	This product includes software developed by the University of
 *	California, Berkeley and its contributors.
 * 4. Neither the name of the University nor the names of its contributors
 *    may be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */

#ifndef lint
static char sccsid[] = "@(#)sex_get.c	9.12 (Berkeley) 2/10/95";
#endif /* not lint */

#include <sys/types.h>
#include <sys/queue.h>
#include <sys/time.h>

#include <bitstring.h>
#include <ctype.h>
#include <limits.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <termios.h>

#include "compat.h"
#include <db.h>
#include <regex.h>

#include "vi.h"
#include "excmd.h"
#include "sex_screen.h"
#include "../vi/vcmd.h"

/*
 * !!!
 * The backslash characters was special when it preceded a newline as part of
 * a substitution replacement pattern.  For example, the input ":a\<cr>" would
 * failed immediately with an error, as the <cr> wasn't part of a substitution
 * replacement pattern.  This implies a frightening integration of the editor
 * and the parser and/or the RE engine.  There's no way I'm going to reproduce
 * those semantics.
 *
 * So, if backslashes are special, this code inserts the backslash and the next
 * character into the string, without regard for the character or the command
 * being entered.  Since "\<cr>" was illegal historically (except for the one
 * special case), and the command will fail eventually, no historical scripts
 * should break (presuming they didn't depend on the failure mode itself or the
 * characters remaining when failure occurred.
 */

static void	h_cont __P((int));		/* SIGCONT handler. */
static int	txt_dent __P((SCR *, TEXT *));
static void	txt_prompt __P((SCR *, TEXT *, ARG_CHAR_T, u_int));

static int	cont_arrived;			/* SIGCONT arrived. */

/*
 * sex_get --
 *	Get lines from the terminal for ex.
 */
input_t
sex_get(sp, tiqh, prompt, flags)
	SCR *sp;
	TEXTH *tiqh;
	ARG_CHAR_T prompt;
	u_int flags;
{
				/* State of the "[^0]^D" sequences. */
	enum { C_NOTSET, C_CARATSET, C_NOCHANGE, C_ZEROSET } carat_st;
	struct sigaction act, oact;
	CH ikey;		/* Input character. */
	TEXT *ntp, *tp, ait;	/* Input and autoindent text structures. */
	input_t rval;
	sigset_t set;
	size_t cnt;
	int eof_count;		/* Count of EOF characters. */

	/*
	 * Get a TEXT structure with some initial buffer space, reusing the
	 * last one if it's big enough.  (All TEXT bookkeeping fields default
	 * to 0 -- text_init() handles this.)
	 */
	if (tiqh->cqh_first != (void *)tiqh) {
		tp = tiqh->cqh_first;
		if (tp->q.cqe_next != (void *)tiqh || tp->lb_len < 32) {
			text_lfree(tiqh);
			goto newtp;
		}
		tp->len = 0;
	} else {
newtp:		if ((tp = text_init(sp, NULL, 0, 32)) == NULL)
			return (INP_ERR);
		CIRCLEQ_INSERT_HEAD(tiqh, tp, q);
	}

	/* Set the starting line number. */
	tp->lno = sp->lno + 1;

	/*
	 * If it's a terminal, set up autoindent, put out the prompt, and
	 * set it up so we know we were suspended.  Otherwise, turn off
	 * the autoindent flag, as that requires less special casing below.
	 *
	 * XXX
	 * Historic practice is that ^Z suspended command mode (but, because
	 * it ran in cooked mode, it was unaffected by the autowrite option.)
	 * On restart, any "current" input was discarded, whether in insert
	 * mode or not, and ex was in command mode.  This code matches historic
	 * practice, but not 'cause it's easier.
	 */
	if (F_ISSET(sp->gp, G_STDIN_TTY)) {
		if (LF_ISSET(TXT_AUTOINDENT)) {
			LF_SET(TXT_EOFCHAR);
			if (txt_auto(sp, sp->lno, NULL, 0, tp))
				return (1);
		}
		txt_prompt(sp, tp, prompt, flags);

		(void)sigemptyset(&set);
		sigaddset(&set, SIGCONT);
		act.sa_handler = h_cont;
		sigfillset(&act.sa_mask);
		act.sa_flags = 0;
		(void)sigaction(SIGCONT, &act, &oact);
	} else
		LF_CLR(TXT_AUTOINDENT);

	/*
	 * Get individual characters and enter them in the buffer.  Use the
	 * general purpose key retrieval routines (even though ex doesn't
	 * map commands or keys) because it's simpler than having two sets
	 * of routines that read from the terminal.
	 */
	for (carat_st = C_NOTSET, eof_count = 0;;) {
		rval = term_key(sp, &ikey, flags);

		/*
		 * EOF is a valid command in ex, but we don't want to run
		 * forever if the terminal driver is returning EOF because
		 * the user has disconnected.
		 */
#define	CNTRLD	'\004'
		switch (rval) {
		case INP_OK:
			eof_count = 0;
			break;
		case INP_EOF:
			if (!F_ISSET(sp->gp, G_STDIN_TTY) || ++eof_count > 40)
				goto ret;
			ikey.ch = CNTRLD;
			ikey.value = K_CNTRLD;
			break;
		case INP_ERR:
			goto ret;
		case INP_INTR:
			if (cont_arrived) {
				/* Clear the interrupt condition. */
				cont_arrived = 0;
				F_CLR(__global_list, G_SIGINT);

				/*
				 * If in input mode, return interrupted after
				 * losing the current TEXT.
				 */
				if (LF_ISSET(TXT_DOTTERM))
					goto notlast;

				/*
				 * Otherwise, start over, redisplaying the
				 * prompt.
				 */
				tp->len = 0;
				txt_prompt(sp, tp, prompt, flags);
				continue;
			}
			/*
			 * Terminate command or text input mode.  If in text
			 * input mode, put out a line separator and release
			 * the current TEXT.
			 */
			if (LF_ISSET(TXT_DOTTERM)) {
				(void)putchar('\n');
				goto notlast;
			}
			goto ret;
		}

		BINC_GOTO(sp, tp->lb, tp->lb_len, tp->len + 1);

		switch (ikey.value) {
		case K_CR:
			/*
			 * !!!
			 * Historically, <carriage-return>'s in the command
			 * weren't special, so the ex parser would return an
			 * unknown command error message.  However, if they
			 * were in a map, they terminated the command.  I'm
			 * pretty sure this still isn't right, but it handles
			 * everything I've seen so far.
			 */
			if (!F_ISSET(&ikey, CH_MAPPED))
				goto ins_ch;
			/* FALLTHROUGH */
		case K_NL:
			/*
			 * '\' can escape <carriage-return>/<newline>.
			 * We toss the backslash.
			 */
			if (LF_ISSET(TXT_BACKSLASH) &&
			    tp->len != 0 && tp->lb[tp->len - 1] == '\\') {
				--tp->len;
				goto ins_ch;
			}

			/*
			 * CR returns from the ex command line.  Terminate
			 * with a nul, needed by filter.
			 */
			if (LF_ISSET(TXT_CR)) {
				tp->lb[tp->len] = '\0';
				goto ok;
			}

			/*
			 * '.' terminates ex text input mode; release the
			 * current TEXT.
			 */
			if (LF_ISSET(TXT_DOTTERM) && tp->len == tp->ai + 1 &&
			    tp->lb[tp->len - 1] == '.')
				goto notlast;

			/* Display any accumulated error messages. */
			if (sex_refresh(sp))
				goto err;

			/* Set up bookkeeping for the new line. */
			if ((ntp = text_init(sp, NULL, 0, 32)) == NULL)
				goto err;
			ntp->lno = tp->lno + 1;

			/*
			 * Reset the autoindent line value.  0^D keeps the ai
			 * line from changing, ^D changes the level, even if
			 * there are no characters in the old line.  Note,
			 * if using the current tp structure, use the cursor
			 * as the length, the user may have erased autoindent
			 * characters.
			 */
			if (LF_ISSET(TXT_AUTOINDENT)) {
				if (carat_st == C_NOCHANGE) {
					if (txt_auto(sp,
					    OOBLNO, &ait, ait.ai, ntp))
						goto err;
					FREE_SPACE(sp, ait.lb, ait.lb_len);
				} else
					if (txt_auto(sp,
					    OOBLNO, tp, tp->len, ntp))
						goto err;
				carat_st = C_NOTSET;
			}
			txt_prompt(sp, ntp, prompt, flags);

			/*
			 * Swap old and new TEXT's, and insert the new TEXT
			 * into the queue.
			 */
			tp = ntp;
			CIRCLEQ_INSERT_TAIL(tiqh, tp, q);
			break;
		case K_CARAT:			/* Delete autoindent chars. */
			if (LF_ISSET(TXT_AUTOINDENT) && tp->len <= tp->ai)
				carat_st = C_CARATSET;
			goto ins_ch;
		case K_ZERO:			/* Delete autoindent chars. */
			if (LF_ISSET(TXT_AUTOINDENT) && tp->len <= tp->ai)
				carat_st = C_ZEROSET;
			goto ins_ch;
		case K_CNTRLD:			/* Delete autoindent char. */
			/*
			 * !!!
			 * Historically, the ^D command took (but then ignored)
			 * a count.  For simplicity, we don't return it unless
			 * it's the first character entered.  The check for len
			 * equal to 0 is okay, TXT_AUTOINDENT won't be set.
			 */
			if (LF_ISSET(TXT_CNTRLD)) {
				for (cnt = 0; cnt < tp->len; ++cnt)
					if (!isblank(tp->lb[cnt]))
						break;
				if (cnt == tp->len) {
					tp->len = 1;
					tp->lb[0] = CNTRLD;
					tp->lb[1] = '\0';

					/*
					 * Put out a line separator, in case
					 * the command fails.
					 */
					(void)putchar('\n');
					goto ok;
				}
			}

			/*
			 * POSIX 1003.1b-1993, paragraph 7.1.1.9 states that
			 * EOF characters are discarded if there are any other
			 * characters to process in the line, i.e. if the EOF
			 * is not the first character in the line.  Historic
			 * ex discarded EOF characters for this reason, even
			 * if they occurred in the middle of the input line.
			 * We match that historic practice.
			 *
			 * Note, the test for discarding in the middle of the
			 * line is done in the switch, as the CARAT forms are
			 * N + 1, not N.  There's considerable magic to make
			 * the terminal code return the EOF character at all;
			 * see the comment in sex_term.c:sex_key_read().
			 */
			if (!LF_ISSET(TXT_AUTOINDENT) || tp->len == 0)
				continue;
			switch (carat_st) {
			case C_CARATSET:	/* ^^D */
				if (tp->len > tp->ai + 1)
					continue;
				/* Save the ai string for later. */
				ait.lb = NULL;
				ait.lb_len = 0;
				BINC_GOTO(sp, ait.lb, ait.lb_len, tp->ai);
				memmove(ait.lb, tp->lb, tp->ai);
				ait.ai = ait.len = tp->ai;

				carat_st = C_NOCHANGE;
				goto leftmargin;
			case C_ZEROSET:		/* 0^D */
				if (tp->len > tp->ai + 1)
					continue;
				carat_st = C_NOTSET;
leftmargin:			sex_termcap(sp, EX_TERM_CE);
				tp->ai = tp->len = 0;
				break;
			case C_NOTSET:		/* ^D */
				if (tp->len > tp->ai)
					continue;
				if (txt_dent(sp, tp))
					goto err;
				break;
			default:
				abort();
			}

			/* Clear and redisplay the line. */
			sex_termcap(sp, EX_TERM_CE);
			txt_prompt(sp, tp, prompt, flags);
			break;
		default:
			/*
			 * See the TXT_BEAUTIFY comment in vi/v_ntext.c.
			 *
			 * Silently eliminate any iscntrl() character that
			 * wasn't already handled specially, except for <tab>
			 * and <ff>.
			 */
ins_ch:			if (LF_ISSET(TXT_BEAUTIFY) && iscntrl(ikey.ch) &&
			    ikey.value != K_FORMFEED && ikey.value != K_TAB)
				break;
			tp->lb[tp->len] = ikey.ch;
			++tp->len;
			break;
		}
	}
	/* NOTREACHED */

	if (0) {
notlast:	CIRCLEQ_REMOVE(tiqh, tp, q);
		text_free(tp);
		goto ok;
	}
	if (0) {
err:	
binc_err:	rval = INP_ERR;
	}
	if (0) {
ok:		rval = INP_OK;
	}

ret:	(void)sigaction(SIGCONT, &oact, NULL);
	return (rval);
}

/*
 * txt_prompt --
 *	Display the ex prompt, line number, ai characters.  Characters had
 *	better be printable by the terminal driver, but that's its problem,
 *	not ours.
 */
static void
txt_prompt(sp, tp, prompt, flags)
	SCR *sp;
	TEXT *tp;
	ARG_CHAR_T prompt;
	u_int flags;
{
	/* Display the prompt. */
	if (LF_ISSET(TXT_PROMPT))
		(void)printf("%c", prompt);

	/* Display the line number. */
	if (LF_ISSET(TXT_NUMBER) && O_ISSET(sp, O_NUMBER))
		(void)printf("%6ld  ", tp->lno);

	/* Print out autoindent string. */
	if (LF_ISSET(TXT_AUTOINDENT))
		(void)printf("%.*s", (int)tp->ai, tp->lb);
	(void)fflush(stdout);
}

/*
 * txt_dent --
 *	Handle ^D outdents.
 *
 * Ex version of vi/v_ntext.c:txt_dent().  See that code for the (usual)
 * ranting and raving.  This is a fair bit simpler as ^T isn't special.
 */
static int
txt_dent(sp, tp)
	SCR *sp;
	TEXT *tp;
{
	u_long sw, ts;
	size_t cno, off, scno, spaces, tabs;

	ts = O_VAL(sp, O_TABSTOP);
	sw = O_VAL(sp, O_SHIFTWIDTH);

	/* Get the current screen column. */
	for (off = scno = 0; off < tp->len; ++off)
		if (tp->lb[off] == '\t')
			scno += COL_OFF(scno, ts);
		else
			++scno;

	/* Get the previous shiftwidth column. */
	cno = scno;
	scno -= --scno % sw;

	/*
	 * Since we don't know what comes before the character(s) being
	 * deleted, we have to resolve the autoindent characters .  The
	 * example is a <tab>, which doesn't take up a full shiftwidth
	 * number of columns because it's preceded by <space>s.  This is
	 * easy to get if the user sets shiftwidth to a value less than
	 * tabstop, and then uses ^T to indent, and ^D to outdent.
	 *
	 * Count up spaces/tabs needed to get to the target.
	 */
	for (cno = 0, tabs = 0; cno + COL_OFF(cno, ts) <= scno; ++tabs)
		cno += COL_OFF(cno, ts);
	spaces = scno - cno;

	/* Make sure there's enough room. */
	BINC_RET(sp, tp->lb, tp->lb_len, tabs + spaces + 1);

	/* Adjust the final ai character count. */
	tp->ai = tabs + spaces;

	/* Enter the replacement characters. */
	for (tp->len = 0; tabs > 0; --tabs)
		tp->lb[tp->len++] = '\t';
	for (; spaces > 0; --spaces)
		tp->lb[tp->len++] = ' ';
	return (0);
}

/*
 * h_cont --
 *	Handle SIGCONT.
 */
static void
h_cont(signo)
	int signo;
{
	cont_arrived = 1;		/* So we know it was a TSTP. */
	F_SET(__global_list, G_SIGINT);	/* So the terminal routine returns. */
}
