/**************************************************************************
 * This program is Copyright (C) 1986-2002 by Jonathan Payne.  JOVE is    *
 * provided by Jonathan and Jovehacks without charge and without          *
 * warranty.  You may copy, modify, and/or distribute JOVE, provided that *
 * this notice is included in all the source files and documentation.     *
 **************************************************************************/

#include "jove.h"
#include "jctype.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 "para.h"
#include "screen.h"	/* for tabstop */
#include "proc.h"
#include "wind.h"

private void
	DoNewline(jbool indentp);

#ifdef LISP
private Bufpos
	*lisp_indent(void);
#endif

/* Make a new line after "after" in buffer "buf", unless "after" is NULL,
 * in which case we insert the new line before first line.
 */
LinePtr
listput(Buffer *buf, LinePtr after)
{
	LinePtr	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(long num)
{
	char	newline[LBSIZE];
	LinePtr	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 beforehand.
 */
void
n_indent(int goal)
{
	int	dotcol;

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

	if (tabstop != 0) {
		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(void)
{
	if (MinorMode(Abbrev) && !jisident(LastKeyStruck)
	    && !bolp() && jisident(linebuf[curchar - 1]))
		AbbrevExpand();
}
#endif

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

void
overwrite(DAPchar c, int n)
{
	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' || tabstop == 0
				|| TABDIST(calc_pos(linebuf, curchar)) == 1))
		{
			del_char(FORWARD, 1, NO);
		}
		insert_c(c, 1);
	}
}

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

	/* If we are in fill mode and at or beyond the right margin,
	 * we break the line.  However, we won't do this if the new
	 * character is whitespace and we are at the end of the line:
	 * DoJustify would discard the trailing space.
	 */
	if (MinorMode(Fill) && calc_pos(linebuf, curchar) >= RMargin
	&& !(jiswhite(LastKeyStruck) && eolp()))
	{
		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(DAPchar c, int 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(void)
{
#ifdef LISP
	if (MajorMode(LISPMODE) && (bolp() || !eolp())) {
		int	dotchar = curchar;

		ToIndent();
		if (dotchar > curchar) {
			Mark	*m = MakeMark(curline, dotchar);

			(void) lisp_indent();
			ToMark(m);
			DelMark(m);
		} else {
			(void) lisp_indent();
			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(void)
{
	ZXchar	c = ask_ks();

	if (c == '\0') {
		int	n = arg_value();

		while (n-- > 0)
			ins_str("^@");
	} else {
		Insert(c);
#ifdef PCNONASCII
		if (c == PCNONASCII) {
			c = waitchar();
			if (c == '\0') {
				int	n = arg_value();

				while (n-- > 0)
					ins_str("^@");
			} else {
				Insert(c);
			}
		}
#endif
	}
}

/* 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,		/* VAR: paren flash delay in tenths of a second */
	CIndIncrmt = 8;	/* VAR: how much each indentation level pushes over in C mode */

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

	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
	    && !PreEmptOutput()
#endif
	    && !in_macro())
	{
		b_char(1);	/* Back onto the ')' */
		if (!tried)
			bp = m_paren(c, BACKWARD, NO, YES);
		f_char(1);
		if (bp != NULL) {
			int	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(void)
{
	DoNewline(YES);
}

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

private void
DoNewline(jbool 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)
	{
		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(const char *str)
{
	ins_str_wrap(str, NO, LBSIZE-1);
}

void
ins_str_wrap(const char *str, jbool ok_nl, int wrap_off)
{
	char	c;
	Bufpos	save;
	int	llen;

	if (*str == '\0')
		return;		/* ain't nothing to insert! */

	if (wrap_off > LBSIZE-1)
		wrap_off = LBSIZE-1;
	DOTsave(&save);
	llen = strlen(linebuf);
	while ((c = *str++) != '\0') {
		if (c == '\n' || (ok_nl && llen >= wrap_off)) {
			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(long n)
{
	Bufpos	dot;

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

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

/* Take the region FLINE/FCHAR to TLINE/TCHAR and insert it at
 * ATLINE/ATCHAR in WHATBUF.
 */
const Bufpos *
DoYank(LinePtr fline, int fchar,
       LinePtr tline, int tchar,
       LinePtr atline, int atchar,
       Buffer *whatbuf)
{
	LinePtr	newline,
		startline = atline;
	int	startchar = atchar;
	static Bufpos
		bp;
	char	save[LBSIZE],
		buf[LBSIZE];

	lsave();
	if (whatbuf != NULL)
		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 = jputline(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;
	}

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

void
YankPop(void)
{
	Mark		*mp = CurMark();
	LinePtr		line, last;
	const Bufpos	*dot;

	switch (last_cmd) {
	case YANKCMD:
		{
			/* Direction to rotate the ring */
			int	dir = arg_value() < 0? 1 : NUMKILLS - 1;

			/* Now must find a recently killed region. */
			do {
				killptr = (killptr+dir) % NUMKILLS;
			} while (killbuf[killptr] == NULL);
		}
		break;
	case UNDOABLECMD:
		break;
	default:
		complain("Yank something first!");
		/*NOTREACHED*/
	}
	lfreelist(reg_delete(mp->m_line, mp->m_char, curline, curchar));
	line = killbuf[killptr];
	last = lastline(line);
	dot = DoYank(line, 0, last, length(last), curline, curchar, curbuf);
	MarkSet(CurMark(), curline, curchar);
	SetDot(dot);
	if (last_cmd == UNDOABLECMD)
		this_cmd = OTHER_CMD;
}

/* 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

#ifdef FAR_LINES
# ifdef __BORLANDC__
#  include <alloc.h>	/* Borland farmalloc() */
# else
#  ifdef OWCDOS
#   include <malloc.h>
#   define farmalloc(sz)	_fmalloc(sz)
#   define farfree(x)	_ffree(x)
#  else
#   include <dos.h>	/* Zortech farmalloc(), MSC (?) */
#  endif
# endif
typedef struct chunk _far	*ChunkPtr;
# define CHUNKMALLOC(s)	((ChunkPtr) farmalloc(s))
# define CHUNKFREE(c)	farfree((void _far *) (c))
#else
typedef struct chunk	*ChunkPtr;
# define CHUNKMALLOC(s)	((ChunkPtr) malloc(s))
# define CHUNKFREE(c)	free(c)
#endif

struct chunk {
	ChunkPtr	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). */
	struct line	c_block[1 /* or larger */];	/* Chunk of memory */
};

private ChunkPtr	fchunk = NULL;	/* first chunk */
private LinePtr	ffline = NULL;	/* First free line */
private LinePtr	faline = NULL;	/* First available line */

private void
freeline(LinePtr 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(void)
{
	if (ffline == NULL)
		return;	/* nothing to do */

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

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

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

void
lfreelist(LinePtr first)
{
	if (first != NULL)
		lfreereg(first, lastline(first));
}

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

void
lfreereg(LinePtr line1, LinePtr line2)
{
	LinePtr	next,
		last = line2->l_next;

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

private jbool
newchunk(void)
{
	LinePtr	newline;
	long	i;
	ChunkPtr
		f;
	long	nlines = CHUNKSIZE;
	jbool	done_gc = NO;

	for (;;) {
		f = CHUNKMALLOC(sizeof(struct chunk) + sizeof(struct line) * (nlines-1));
		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 */

LinePtr
nbufline(void)
{
	LinePtr	newline;

	if (faline == NULL) {
		RecycleLines();
		if (faline == NULL) {
			if (!newchunk()) {
				complain("[Out of lines] ");
				/* NOTREACHED */
			}
		}
	}
	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(ChunkPtr c)
{
	LinePtr	lp;
	long	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(void)
{
	ChunkPtr	cp;
	ChunkPtr	prev = NULL,
			next;
	long	i;
	LinePtr	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);
				CHUNKFREE(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(void)
{
	Bufpos	dot,
		end;

	if (linebuf[curchar] != '(') {
		complain((char *)NULL);
		/* NOTREACHED */
	}
	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(void)
{
	static const char *const words[] = {
		"case",
		"def",
		"dolist",
		"fluid-let",
		"lambda",
		"let",
		"lexpr",
		"macro",
		"named-l",	/* named-let and named-lambda */
		"nlambda",
		"prog",
		"selectq",
		NULL
	};
	const char	*const *wordp = words;

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

void
AddSpecial(void)
{
	const 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, copystr(word));
}

private Bufpos *
lisp_indent(void)
{
	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 (caseeqn((char *) list_data(lp),
				     &linebuf[curchar],
				     strlen((char *) list_data(lp))))
				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 */
