/***************************************************************************
 * This program is Copyright (C) 1986, 1987, 1988 by Jonathan Payne.  JOVE *
 * is provided to you without charge, and with no warranty.  You may give  *
 * away copies of JOVE, including sources, provided that this notice is    *
 * included in all the files.                                              *
 ***************************************************************************/

#include "jove.h"
#include "ctype.h"
#include "list.h"
#include "chars.h"
#include "disp.h"
#include "abbrev.h"
#include "ask.h"
#include "c.h"
#include "delete.h"
#include "insert.h"
#include "fmt.h"
#include "macros.h"
#include "marks.h"
#include "misc.h"
#include "move.h"
#include "paragraph.h"
#include "screen.h"	/* for tabstop */
#include "sysprocs.h"
#include "proc.h"
#include "wind.h"

private void
	DoNewline proto((bool indentp));

private Bufpos
	*lisp_indent proto((void));

/* Make a new line after "after" in buffer "buf", unless "after" is NULL,
   in which case we insert the new line before first line. */

Line *
listput(buf, after)
register Buffer	*buf;
register Line	*after;
{
	register Line	*newline = nbufline();

	newline->l_prev = after;
	if (after == NULL) {	/* Before the first line */
		newline->l_next = buf->b_first;
		buf->b_first = newline;
	} else {
		newline->l_next = after->l_next;
		after->l_next = newline;
	}
	if (newline->l_next != NULL)
		newline->l_next->l_prev = newline;
	else if (buf != NULL)
		buf->b_last = newline;
	if (buf && buf->b_dot == NULL)
		buf->b_dot = newline;
	return newline;
}

/* Divide the current line and move the current line to the next one */

void
LineInsert(num)
register int	num;
{
	char	newline[LBSIZE];
	register Line	*newdot,
			*olddot;
	int	oldchar;

	olddot = curline;
	oldchar = curchar;

	newdot = curline;
	while (--num >= 0) {
		newdot = listput(curbuf, newdot);
		SavLine(newdot, NullStr);
	}

	modify();
	if (curchar != 0) {
		strcpy(newline, &linebuf[curchar]);
		linebuf[curchar] = '\0';	/* Shorten this line */
		SavLine(curline, linebuf);
		strcpy(linebuf, newline);
	} else {	/* Redisplay optimization */
		newdot->l_dline = curline->l_dline;
		SavLine(curline, NullStr);
	}

	makedirty(curline);
	curline = newdot;
	curchar = 0;
	makedirty(curline);
	IFixMarks(olddot, oldchar, curline, curchar);
}

/* Inserts tabs and spaces to move the cursor to column GOAL.  It
   Uses the most optimal number of tabs and spaces no matter what
   was there before hand. */

void
n_indent(goal)
register int	goal;
{
	int	dotcol;

	DelWtSpace();
	dotcol = calc_pos(linebuf, curchar);

	for (;;) {
		int	incrmt = TABDIST(dotcol);

		if (dotcol + incrmt > goal)
			break;
		insert_c('\t', 1);
		dotcol += incrmt;
	}
	if (dotcol != goal)
		insert_c(' ', (goal - dotcol));
}

#ifdef	ABBREV
void
MaybeAbbrevExpand()
{
	if (MinorMode(Abbrev) && !jisident(LastKeyStruck)
	&& !bolp() && jisident(linebuf[curchar - 1]))
		AbbrevExpand();
}
#endif

private void
Insert(c)
int	c;
{
	if (c == CTL('J'))
		LineInsert(arg_value());
	else
		insert_c(c, arg_value());
}

void
overwrite(c, n)
int	c,
	n;
{
	register int	i;

	for (i = 0; i < n; i++) {
		/* Delete one *column* forward (except that we don't
		 * notice that control characters take two columns).
		 */
		if (!eolp() && (linebuf[curchar] != '\t'
		  || TABDIST(calc_pos(linebuf, curchar)) == 1))
		{
			del_char(FORWARD, 1, NO);
		}
		insert_c(c, 1);
	}
}

void
SelfInsert()
{
#ifdef	ABBREV
	MaybeAbbrevExpand();
#endif
	if (LastKeyStruck != CTL('J') && MinorMode(OverWrite))
		overwrite(LastKeyStruck, arg_value());
	else
		Insert(LastKeyStruck);

	if (MinorMode(Fill) && calc_pos(linebuf, curchar) >= RMargin) {
		int margin;
		Bufpos save;

		if (MinorMode(Indent)) {
			DOTsave(&save);
			ToIndent();
			margin = calc_pos(linebuf, curchar);
			SetDot(&save);
		} else {
			margin = LMargin;
		}
		DoJustify(curline, 0, curline,
			  curchar + (int)strlen(&linebuf[curchar]), YES, margin);
	}
}

/* insert character C N times at point */
void
insert_c(c, n)
int	c,
	n;
{
	if (n > 0) {
		modify();
		makedirty(curline);
		ins_c(c, linebuf, curchar, n, LBSIZE);
		IFixMarks(curline, curchar, curline, curchar + n);
		curchar += n;
	}
}

/* Tab in to the right place for C mode */

void
Tab()
{
#ifdef	LISP
	if (MajorMode(LISPMODE) && (bolp() || !eolp())) {
		int	dotchar = curchar;
		Mark	*m = NULL;

		ToIndent();
		if (dotchar > curchar)
			m = MakeMark(curline, dotchar, M_FLOATER);
		(void) lisp_indent();
		if (m) {
			ToMark(m);
			DelMark(m);
		} else
			ToIndent();
		return;
	}
#endif
	if (MajorMode(CMODE)) {
		if (within_indent())
			(void) c_indent(NO);
		else {
			int	curpos,
				tabbed_pos;

			skip_wht_space();
			curpos = calc_pos(linebuf, curchar);
			tabbed_pos = curpos + (CIndIncrmt - (curpos % CIndIncrmt));
			n_indent(tabbed_pos);
		}
	} else
		SelfInsert();
}

void
QuotChar()
{
	int	c,
		slow = NO;

	c = waitchar(&slow);
	if (c != CTL('@'))
		Insert(c);
}

/* Insert the paren.  If in C mode and c is a '}' then insert the
   '}' in the "right" place for C indentation; that is indented
   the same amount as the matching '{' is indented. */

int	PDelay = 5,	/* 1/2 a second */
	CIndIncrmt = 8;

void
DoParen()
{
	Bufpos	*bp = NULL;	/* avoid uninitialized complaint from gcc -W */
	int	tried = NO,
		nx,
		c = LastKeyStruck;

	if (!jisclosep(c)) {
		SelfInsert();
		return;
	}

	if (MajorMode(CMODE) && c == '}' && within_indent()) {
		bp = c_indent(YES);
		tried = YES;
	}
#ifdef	LISP
	if (MajorMode(LISPMODE) && c == ')' && blnkp(linebuf)) {
		bp = lisp_indent();
		tried = YES;
	}
#endif
	SelfInsert();

	if (MinorMode(ShowMatch)
#ifndef	MAC
	&& !charp()
#endif
	&& !in_macro())
	{
		b_char(1);	/* Back onto the ')' */
		if (!tried)
			bp = m_paren(c, BACKWARD, NO, YES);
		f_char(1);
		if (bp != NULL) {
			nx = in_window(curwind, bp->p_line);
			if (nx != -1) {		/* is visible */
				Bufpos	b;

				DOTsave(&b);
				SetDot(bp);
				SitFor(PDelay);
				SetDot(&b);
			} else
				s_mess("%s", lcontents(bp->p_line));
		}
		mp_error();	/* display error message */
	}
}

void
LineAI()
{
	DoNewline(YES);
}

void
Newline()
{
	DoNewline(MinorMode(Indent));
}

private void
DoNewline(indentp)
bool	indentp;
{
	Bufpos	save;
	int	indent;

	/* first we calculate the indent of the current line */
	DOTsave(&save);
	ToIndent();
	indent = calc_pos(linebuf, curchar);
	SetDot(&save);

#ifdef	ABBREV
	MaybeAbbrevExpand();
#endif
	if (
#ifdef	LISP
	    MajorMode(LISPMODE) ||
#endif
	    indentp || blnkp(linebuf))
	{
		DelWtSpace();
	}

	/* If there is more than 2 blank lines in a row then don't make
	   a newline, just move down one. */
	if (arg_value() == 1 && eolp() && TwoBlank())
		SetLine(curline->l_next);
	else
		LineInsert(arg_value());

	if (indentp) {
#ifdef	LISP
	    if (MajorMode(LISPMODE))
		(void) lisp_indent();
	    else
#endif
	    {
		Bol();
		n_indent((LMargin == 0) ? indent : LMargin);
	    }
	}
}

void
ins_str(str, ok_nl)
register char	*str;
int	ok_nl;
{
	register char	c;
	Bufpos	save;
	int	llen;

	if (*str == '\0')
		return;		/* ain't nothing to insert! */
	DOTsave(&save);
	llen = strlen(linebuf);
	while ((c = *str++) != '\0') {
		if (c == '\n' || (ok_nl && llen >= LBSIZE - 2)) {
			IFixMarks(save.p_line, save.p_char, curline, curchar);
			modify();
			makedirty(curline);
			LineInsert(1);
			DOTsave(&save);
			llen = strlen(linebuf);
		}
		if (c != '\n') {
			ins_c(c, linebuf, curchar++, 1, LBSIZE);
			llen += 1;
		}
	}
	IFixMarks(save.p_line, save.p_char, curline, curchar);
	modify();
	makedirty(curline);
}

void
open_lines(n)
int	n;
{
	Bufpos	dot;

	DOTsave(&dot);
	LineInsert(n);	/* Open the lines... */
	SetDot(&dot);
}

void
OpenLine()
{
	open_lines(arg_value());
}

/* Take the region FLINE/FCHAR to TLINE/TCHAR and insert it at
   ATLINE/ATCHAR in WHATBUF. */

Bufpos *
DoYank(fline, fchar, tline, tchar, atline, atchar, whatbuf)
Line	*fline,
	*tline,
	*atline;
int	fchar,
	tchar,
	atchar;
Buffer	*whatbuf;
{
	register Line	*newline;
	static Bufpos	bp;
	char	save[LBSIZE],
		buf[LBSIZE];
	Line	*startline = atline;
	int	startchar = atchar;

	lsave();
	if (whatbuf)
		modify();
	(void) ltobuf(atline, genbuf);
	strcpy(save, &genbuf[atchar]);

	(void) ltobuf(fline, buf);
	if (fline == tline)
		buf[tchar] = '\0';

	linecopy(genbuf, atchar, &buf[fchar]);
	atline->l_dline = putline(genbuf);
	makedirty(atline);

	fline = fline->l_next;
	while (fline != tline->l_next) {
		newline = listput(whatbuf, atline);
		newline->l_dline = fline->l_dline;
		makedirty(newline);
		fline = fline->l_next;
		atline = newline;
		atchar = 0;
	}

	getline(atline->l_dline, genbuf);
	atchar += tchar;
	linecopy(genbuf, atchar, save);
	atline->l_dline = putline(genbuf);
	makedirty(atline);
	IFixMarks(startline, startchar, atline, atchar);
	bp.p_line = atline;
	bp.p_char = atchar;
	this_cmd = YANKCMD;
	getDOT();			/* Whatever used to be in linebuf */
	return &bp;
}

void
YankPop()
{
	Line	*line,
		*last;
	Mark	*mp = CurMark();
	Bufpos	*dot;
	int	dir = -1;	/* Direction to rotate the ring */

	if (last_cmd != YANKCMD)
		complain("Yank something first!");

	lfreelist(reg_delete(mp->m_line, mp->m_char, curline, curchar));

	/* Now must find a recently killed region. */

	if (arg_value() < 0)
		dir = 1;

	killptr += dir;
	for (;;) {
		if (killptr < 0)
			killptr = NUMKILLS - 1;
		else if (killptr >= NUMKILLS)
			killptr = 0;
		if (killbuf[killptr])
			break;
		killptr += dir;
	}

	this_cmd = YANKCMD;

	line = killbuf[killptr];
	last = lastline(line);
	dot = DoYank(line, 0, last, length(last), curline, curchar, curbuf);
	MarkSet(CurMark(), curline, curchar);
	SetDot(dot);
}

/* This is an attempt to reduce the amount of memory taken up by each line.
   Without this each malloc of a line uses sizeof (line) + sizeof(HEADER)
   where line is 3 words and HEADER is 1 word.
   This is going to allocate memory in chucks of CHUNKSIZE * sizeof (line)
   and divide each chuck into Lines.  A line is free in a chunk when its
   line->l_dline == NULL_DADDR, so freeline sets l_dline to NULL_DADDR. */

#define CHUNKSIZE	300

struct chunk {
	struct chunk	*c_nextchunk;	/* Next chunk of lines */
	int	c_nlines;	/* Number of lines in this chunk (so they
				   don't all have to be CHUNKSIZE long). */
	Line	c_block[1 /* or larger */];	/* Chunk of memory */
};

private struct chunk	*fchunk = NULL;	/* first chunk */
private Line	*ffline = NULL;	/* First free line */
private Line	*faline = NULL;	/* First available line */

private void
freeline(line)
register Line	*line;
{
	line->l_dline = NULL_DADDR;
	line->l_next = ffline;
	if (ffline)
		ffline->l_prev = line;
	line->l_prev = NULL;
	ffline = line;
}

/* Make sure that there are no dangling references to lines in the free list,
 * then move them to the end of the avail list.
 */

private void
RecycleLines()
{
	if (ffline == NULL)
		return;	/* nothing to do */

	ChkErrorLines();
	/* ChkWindowLines(); -- nothing needs attention */
	/* ChkBufLines(); -- nothing needs attention */

	if (faline == NULL) {
		faline = ffline;
	} else {
		Line	*laline = lastline(faline);

		laline->l_next = ffline;
		ffline->l_prev = laline;
	}
	ffline = NULL;
}

void
lfreelist(first)
register Line	*first;
{
	if (first != NULL)
		lfreereg(first, lastline(first));
}

/* Append region from line1 to line2 onto the free list of lines */

void
lfreereg(line1, line2)
register Line	*line1,
		*line2;
{
	register Line	*next,
			*last = line2->l_next;

	while (line1 != last) {
		next = line1->l_next;
		freeline(line1);
		line1 = next;
	}
}

private bool
newchunk()
{
	register Line	*newline;
	register int	i;
	struct chunk	*f;
	int	nlines = CHUNKSIZE;
	bool	done_gc = NO;

	for (;;) {
		f = (struct chunk *) malloc(
			(sizeof(struct chunk) - sizeof(Line))
			+ (sizeof(Line) * nlines));
		if (f != NULL)
			break;
		if (!done_gc) {
			GCchunks();
			done_gc = YES;
		} else {
			nlines /= 2;
			if (nlines <= 0)
				return NO;
		}
	}

	f->c_nlines = nlines;
	for (i = 0, newline = f->c_block; i < nlines; newline++, i++) {
		newline->l_dline = NULL_DADDR;
		newline->l_next = faline;
		if (faline)
			faline->l_prev = newline;
		newline->l_prev = NULL;
		faline = newline;
	}
	f->c_nextchunk = fchunk;
	fchunk = f;
	return YES;
}

/* New BUFfer LINE */

Line *
nbufline()
{
	register Line	*newline;

	if (faline == NULL) {
		RecycleLines();
		if (faline == NULL) {
			if (!newchunk())
				complain("[Out of lines] ");
		}
	}
	newline = faline;
	faline = newline->l_next;
	if (faline)
		faline->l_prev = NULL;
	return newline;
}

/* Remove the free lines, in chunk c, from the free list because they are
   no longer free. */

private void
remfreelines(c)
register struct chunk	*c;
{
	register Line	*lp;
	register int	i;

	for (lp = c->c_block, i = c->c_nlines; i != 0 ; lp++, i--) {
		if (lp->l_prev == NULL)
			faline = lp->l_next;
		else
			lp->l_prev->l_next = lp->l_next;
		if (lp->l_next != NULL)
			lp->l_next->l_prev = lp->l_prev;
	}
}

/* This is used to garbage collect the chunks of lines when malloc fails
   and we are NOT looking for a new buffer line.  This goes through each
   chunk, and if every line in a given chunk is not allocated, the entire
   chunk is `free'd by "free()". */

/* ??? I think that this WILL be called when we are looking for a new
 * buffer line: nbufline() => newchunk() => GCchunks() -- DHR
 */

void
GCchunks()
{
	register struct chunk	*cp;
	struct chunk	*prev = NULL,
			*next;
	register int	i;
	register Line	*newline;

	RecycleLines();
	for (cp = fchunk; cp != NULL; cp = next) {
		next = cp->c_nextchunk;
		for (i = cp->c_nlines, newline = cp->c_block; ; newline++, i--) {
			if (i == 0) {
				/* Empty: unlink and free it!!! */
				if (prev == NULL)
					fchunk = cp->c_nextchunk;
				else
					prev->c_nextchunk = cp->c_nextchunk;
				remfreelines(cp);
				free((UnivPtr) cp);
				break;
			}
			if (newline->l_dline != NULL_DADDR) {
				/* it's a keeper */
				prev = cp;
				break;
			}
		}
	}
}

#ifdef	LISP

#include "re.h"

/* Grind S-Expr */

void
GSexpr()
{
	Bufpos	dot,
		end;

	if (linebuf[curchar] != '(')
		complain((char *)NULL);
	DOTsave(&dot);
	FSexpr();
	DOTsave(&end);
	SetDot(&dot);
	for (;;) {
		if (curline == end.p_line)
			break;
		line_move(FORWARD, 1, NO);
		if (!blnkp(linebuf))
			(void) lisp_indent();
	}
	SetDot(&dot);
}

/* lisp_indent() indents a new line in Lisp Mode, according to where
   the matching close-paren would go if we typed that (sort of). */

private List	*specials = NULL;

private void
init_specials()
{
	static char *const words[] = {
		"case",
		"def",
		"dolist",
		"fluid-let",
		"lambda",
		"let",
		"lexpr",
		"macro",
		"named-l",	/* named-let and named-lambda */
		"nlambda",
		"prog",
		"selectq",
		NULL
	};
	char	*const *wordp = words;

	while (*wordp != NULL)
		list_push(&specials, (UnivPtr) *wordp++);
}

void
AddSpecial()
{
	char	*word;
	register List	*lp;

	if (specials == NULL)
		init_specials();
	word = ask((char *)NULL, ProcFmt);
	for (lp = specials; lp != NULL; lp = list_next(lp))
		if (strcmp((char *) list_data(lp), word) == 0)
			return;		/* already in list */
	(void) list_push(&specials, (UnivPtr) copystr(word));
}

private Bufpos *
lisp_indent()
{
	Bufpos	*bp,
		savedot;
	int	goal;

	bp = m_paren(')', BACKWARD, NO, YES);

	if (bp == NULL)
		return NULL;

	/* We want to end up

		(atom atom atom ...
		      ^ here.
	 */

	DOTsave(&savedot);
	SetDot(bp);
	f_char(1);
	if (linebuf[curchar] != '(') {
		register List	*lp;

		if (specials == NULL)
			init_specials();
		for (lp = specials; lp != NULL; lp = list_next(lp))
			if (casencmp((char *) list_data(lp),
				     &linebuf[curchar],
				     strlen((char *) list_data(lp))) == 0)
				break;
		if (lp == NULL) {	/* not special */
			int	c_char = curchar;

			while (jisident(linebuf[curchar]))
				curchar += 1;
			if (LookingAt("[ \t]*;\\|[ \t]*$", linebuf, curchar)) {
				curchar = c_char;
			} else {
				while (linebuf[curchar] == ' ')
					curchar += 1;
			}
		} else {
			curchar += 1;
		}
	}
	goal = calc_pos(linebuf, curchar);
	SetDot(&savedot);
	Bol();
	n_indent(goal);

	return bp;
}
#endif	/* LISP */
