#ifndef NO_IDENT
static char *Id = "$Id: crt.c,v 1.7 1995/06/04 18:58:12 tom Exp $";
#endif

/*
 * Title:	crt.c - "FLIST" display routines
 * Author:	Thomas E. Dickey
 * Created:	03 May 1984
 * Last update:
 *		27 May 1995, prototypes
 *		07 Oct 1985, ensure that if term-size changed, LPP is ok.
 *		05 Oct 1985, added code to support 80/132-column switch.
 *		15 Jun 1985, reference functions as '(*func)' to make CC2.0 happy
 *		12 Jun 1985, reset keypad mode before entering help-routine.
 *		09 May 1985, moved on-system test to 'whoami'.
 *		04 Apr 1985, forgot length-arg in strncmp-SYS$
 *		28 Mar 1985, added 'status' argument to 'error'
 *		20 Mar 1985, added 'crt_help' interface routine.
 *		14 Mar 1985, added CTRL/C-ast to force crt-reset.  Also, verify
 *			     that screen-height is even, at least four lines.
 *		02 Feb 1985, added 'crt_margin' entry, cleaned up margin-usage
 *			     in 'crt_quit', 'crt_reset'.
 *		27 Jan 1985, added 'crt_qsgr' entry.
 *		11 Jan 1985, added 'crt_reset' routine.
 *		03 Dec 1984, changed 'crt__put' to 'putraw', omitted 'crt__flush'
 *			     (no more Unix-I/O).  This should avoid a bug in
 *			     the VMS terminal driver (see: 'getpass').
 *		04 Nov 1984, merged 'crt_test' into 'crt_text'.  Added logic
 *			     supporting 'to_eql[]', 'to_chg[]', which permits
 *			     'crt_text' to skip over gaps which are similar.
 *		01 Nov 1984, in 'crt_test', skip leading spaces when writing
 *			     text to a null line.
 *		30 Oct 1984, alter threshold for 'crt_scroll'
 *		17 Oct 1984, added 'crt_refresh', 'crt_x', 'crt_y' entries
 *		15 Sep 1984, added 'crt__put', 'crt__flush' entries
 *		24 Aug 1984, use 'crt.h' include
 *		04 Aug 1984, protect 'crt_text' by killing tabs, etc.
 *		27 Jun 1984
 *		28 May 1984, to chop into 'crt' and 'dds' modules
 *
 *	These routines perform screen display/refresh operations for FLIST
 *	and BROWSE.
 */

#include	<starlet.h>
#include	<lib$routines.h>
#include	<stdlib.h>
#include	<stdio.h>
#include	<ctype.h>
#include	<descrip.h>
#include	<iodef.h>
#include	<ssdef.h>
#include	<string.h>
#include	<stsdef.h>
#include	<ttdef.h>

#include	"bool.h"
#include	"canopen.h"
#include	"names.h"
#include	"whoami.h"

#include	"strutils.h"

#include	"flist.h"	/* crt.h + error + warn */

extern	void	help (char *lib, char *prg, int maxcol);
/*
 * Valid, but no-longer-documented screen package interface
 */
extern	void scr$set_scroll (int lo, int hi);
extern	void scr$set_cursor (int y, int x);
extern	void scr$erase_page (void);
extern	void scr$erase_line (void);
extern	void scr$down_scroll (void);
extern	void scr$up_scroll (void);
extern	void lib$put_screen(struct dsc$descriptor_s *p, ...);
extern	void lib$screen_info (short *term_flags, short *term_type, short *width, short *lpp);

/*
 * VAX/VMS terminal-independent routines know about the ANSI "Set Graphics
 * rendition" codes.  Those available via the VMS run-time library are:
 * (VMS coding is additive)
 */
#define	sgrOFF		0
#define	sgrBOLD		1
#define	sgrREVERSE	2
#define	sgrBLINK	4
#define	sgrUNDERL	8

/*
 * Local (static) data:
 */

#define	FLAG_DEC	1	/* Terminal is a DEC crt		*/
#define	FLAG_ANSI	2	/* Terminal is ANSI 3.64-compatible	*/

static	char	*bigvec	= nullC; /* => crtvec[0] */

/* Make the display memory public so it can be examined: */
char	*crtvec[CRT_LINES];	/* prior-state of screen		*/

static
BYTE	sgrvec[CRT_LINES];	/* last code used via 'crt_text'	*/

static
short	term_flags,
	term_type,		/* terminal-type (see 'dcdef.h')	*/
	width,			/* Current logical-width of screen	*/
	v_width,		/* Maximum width reserved in 'crtvec[]'	*/
	lpp,			/* Current logical-length of screen	*/
	v_lpp;			/* Maximum length reserved in 'crtvec[]'*/
#define	lpp1	(lpp-1)
#define	lpp2	(lpp-2)

static
int	top_line,		/* index number of first line displayed	*/
	end_line,		/* " " " last line in screen		*/
	top_margin,		/* [1..lpp] top scrolling margin	*/
	end_margin,		/* [1..lpp] bottom scrolling margin	*/
	NLflag,			/* TRUE iff '\n'-adjust needed		*/
	lasty, lastx;		/* Last cursor position, for filtering	*/

static	int	crt__lpp0 (int length);
static	void	crt_ctl_c_ast (short *ttchan_);

/* <crt_ctl_c_ast>:
 * CTRL/C AST routine.  Reset terminal characteristics, cancel pending I/O.
 */
static
void	crt_ctl_c_ast (short *ttchan_)
{
	unsigned status;

	crt_quit (FALSE);
	crt_move (lpp-1, 1);
	status = sys$cancel(*ttchan_);
	if (! $VMS_STATUS_SUCCESS(status)) lib$stop(status);
}

/* <crt_init>:
 * Initialize the screen and this module:
 */
void	crt_init (int (*func)(short *lpp_, short *width_))
{
	register int	j;
	static	short	ttchan;
	unsigned status;
	static	$DESCRIPTOR(terminal,"SYS$COMMAND");

	/* Assign channel to terminal	*/
	status = sys$assign(&terminal, &ttchan, 0, 0);
	if (! $VMS_STATUS_SUCCESS(status)) lib$stop(status);

	/* Enable control-C AST	*/
	status = sys$qiow (0,
			ttchan,
			IO$_SETMODE | IO$M_CTRLCAST, 0, 0, 0,
			&crt_ctl_c_ast, &ttchan, 0, 0, 0, 0);
	if (! $VMS_STATUS_SUCCESS(status)) error(status, 0);

	/* Get screen size, terminal type: */
	lib$screen_info (&term_flags, &term_type, &width, &lpp);
	if ((term_flags & FLAG_DEC) == 0)
		error (0, "Terminal must be VT52, VT100 or ANSI\n");

	lpp = crt__lpp0 (lpp);

	crt__NL0 (TRUE);	/* Normally must absorb 1st '\n'	*/
	if (func)	(*func)(&lpp, &width);

	top_line = end_line = 0;
	lpp	= min(CRT_LINES-1, lpp);
	width	= min(CRT_COLS-1,  width);
	v_width	= max(width, 132);

	lastx	= lasty	= 0;

	top_margin = 1;
	end_margin = lpp;

	bigvec = calloc(lpp, 1+v_width);
	for (j = 0; j <= lpp; j++)
	{
		sgrvec[j] = 0;
		crtvec[j] = &bigvec[(1+v_width) * j];
	}
	crt_clear();
}

/*
 * "TT$_" codes for terminals encode specific types in the second hex digit.
 * To return "generic" type, mask this digit first:
 */
#define	GEN_TYPE ((-1) << 4)

int	crt_vt100 (void)   { return ((term_type & GEN_TYPE) == TT$_VT100); }
int	crt_vt52 (void)	   { return ((term_type & GEN_TYPE) == TT$_VT52); }
int	crt_ansi (void)	   { return (term_flags & FLAG_ANSI); }

int	crt_top (void)	   { return (top_line);	}
int	crt_end (void)	   { return (end_line);	}

int	crt_lpp (void)	   { return (lpp);	}
int	crt_width (void)   { return (width);	}

int	crt_topm (void)	   { return (top_margin); }
int	crt_endm (void)	   { return (end_margin); }

int	crt_x (void)	   { return (lastx);	}
int	crt_y (void)	   { return (lasty);	}

int	crt_qsgr(int inx)  { return (sgrvec[inx]); }

/* <crt_margin>:
 * If this is an ANSI terminal, we may set scrolling margins.  Both FLIST
 * and BROWSE use the special case of a status line at the end of the page.
 */
void	crt_margin (int lo, int hi)
{
	if (crt_ansi())
		scr$set_scroll (top_margin = lo, end_margin = hi);
	else
		top_margin = 1, end_margin = lpp;
}

/* <crt_set>:
 * Set top_line/end_line w/o using global variables:
 */
int	crt_set (int top, int val)
{
	if (top)	top_line = val;
	else		end_line = val;
	return (val);
}

/* <crt_quit>:
 * On exit, clear the path-mark line, to set the cursor there.  We also
 * reset the scrolling margins to their default value (without keeping this
 * in our state) so that if this is called from a FLIST-SPAWN command, then
 * we can use 'crt_reset' to restore our scrolling margins.
 *
 * The 'erase' option causes the last line of the screen to be cleared.
 * If the last input was a newline, then VMS will echo this (scrolling up
 * one line on exit).  If not (e.g., CTRL/Z), then the program will exit
 * without scrolling.
 */
void	crt_quit(int erase)
{
	putraw ("\033>");		/* Exit alternate keypad mode */
	scr$set_scroll (1, lpp);
	if (erase)			/* Make a clean exit to command line */
		crt__ED (lpp, 1);
}

/* <crt_clear>:
 * Force screen contents to be cleared.  VT100 info-call does a clear-screen
 * for VT100, but not for VT52.
 */
void	crt_clear(void)
{
	register int	j;

	crt_reset ();
	crt__ED (1,1);
	for (j = 0; j < lpp; *crtvec[j++] = EOS);
}

/* <crt_reset>:
 * Reset scrolling margins and graphics-rendition for ANSI terminals (which
 * may be left in funny modes by other programs).  This code is called on
 * entry to FLIST/BROWSE, and also on return from spawned subprocesses.
 */
void	crt_reset (void)
{
	int	new_w, new_l,
		adj_l, j;

	lastx = lasty = 999;	/* We don't really know where cursor is !! */
	termsize (0, &new_w, &new_l);	/* Interrogate current screen size */
	new_w	= min(v_width, new_w);
	new_l	= crt__lpp0 (min(new_l, CRT_LINES));
	if (new_w != width || new_l != lpp)
	{
		if (new_w < width)
			for (j = 0; j < lpp; crtvec[j++][new_w] = EOS);

		adj_l	= new_l - lpp;
		if (new_l > v_lpp)
		{
			bigvec = realloc(bigvec, new_l * (1+v_width));
			for (j = 0; j <= new_l; j++)
			{
				if (j >= lpp) sgrvec[j] = 0;
				crtvec[j] = &bigvec[(1+v_width) * j];
			}
		}
		for (j = lpp1; j < new_l; j++)
		{
			sgrvec[j] = 0;
			*crtvec[j] = EOS;
		}
		width	= new_w;
		lpp	= new_l;
		end_margin += adj_l;  end_margin = max(top_margin, end_margin);
		end_line   += adj_l;  end_line   = max(top_line, end_line);
	}

	crt_margin (top_margin, end_margin);	/* Scrolling margins	*/
	if (crt_ansi())
		putraw ("\033[0m\033[20l");	/* SGR, new line */
	if (crt_vt100())
		putraw ("\033[?4l");		/* smooth-scroll */
	putraw ("\033=");		/* Enter alternate Keypad mode */
}

/* <crt_refresh>:
 * Refresh the contents of the screen without requiring that the caller code
 * re-generate the data to drive the display.
 */
void	crt_refresh (void)
{
	register
	int	j,
		save_x	= lastx,	/* This is where we think cursor was */
		save_y	= lasty;
	char	sline[CRT_COLS];

	lastx = lasty = 999;	/* We don't really know where cursor is !! */
	crt_reset ();		/* Another process may have redefined TERM */
	crt__ED (1,1);		/* clear the physical screen	*/
	for (j = 0; j < lpp; j++)
	{
		strcpy (sline, crtvec[j]);
		*crtvec[j] = EOS;
		crt_text (sline, j, sgrvec[j]);
	}
	crt_move (save_y, save_x);
}

/* <crt_move>:
 * Set the cursor to the next position from which to manipulate text.
 * Note that the VMS screen routines do not (cannot really) do filtering
 * to reduce the number of cursor movement sequences emitted.  This
 * routine MUST be used for ALL cursor movement to permit it to do the
 * optimization.
 */
void	crt_move (
	int	y,			/* 1 <= y <= lpp	*/
	int	x)			/* 1 <= x <= width	*/
{
	x = max(1, min(width, x));
	y = max(1, min(lpp,   y));
	if (lasty != y || lastx != x)
	{
		register
		int	len,
			jumpx = lastx - x,
			jumpy = y - (lasty ? lasty : (y + 1)),
			crlfs = (NLflag + jumpy) << 1;
		char	sequence[20];

		/*
		 * The VMS-library routines are very dumb, not even
		 * defaulting parameters.  Use as my threshold an
		 * ANSI x3.64 cursor movement:
		 */
		if ((y >  end_margin && lasty >  end_margin)
		||  (y <= end_margin && lasty <= end_margin))
		{
			sprintf (sequence, "?[%d;%d?", y-1, x-1);
			len = strlen (sequence);
		}
		else	/* Disable CRLF if past scrolling margins */
			len = crlfs;

		if ((lasty == y) && (jumpx > 0) && (jumpx < len))
		{
			while (jumpx-- > 0)
				putraw ("\b");
		}
		else if ((1 == x) && (jumpy > 0) && (crlfs < len))
		{
			crt__NL0 (FALSE);
			while (jumpy-- > 0)
				putraw ("\r\n");
		}
		else
			scr$set_cursor (y, x);
	}
	lasty = y;
	lastx = x;
}

/* <crt__lpp0>:
 * We need at least four lines on the screen to run BROWSE.  For the same
 * reason, the screen-length must be even.
 */
static
int	crt__lpp0 (int length)
{
	if (length < 4)
		error (0, "TERMINAL/PAGE must be at least four lines\n");
	if (length & 1)	length--;
	return (length);
}

/*
 * One peculiarity (which I work around) is that if I use '\n' to substitute
 * for cursor movement, the first '\n' after a LIB$SCREEN_INFO() is absorbed.
 * The flag 'NLflag' is intended to track this, so I can provide an extra
 * '\n'.
 *
 * This entry point is needed because the Bitgraph init (see BROWSE) cancels
 * the absorption.
 */
void	crt__NL0 (int flg)
{
	NLflag = flg;
}

/*
 * ANSI x3.64 defines erase functions:
 */
void	crt__ED (int y, int x)		/* Erase to end of Display	*/
{
	crt_move (y, x);
	scr$erase_page ();
}

void	crt__EL (int y, int x)		/* Erase to end of Line	*/
{
	crt_move (y, x);
	scr$erase_line ();
}

/* <crt_help>:
 * We assume that the calling program uses a HELP-library!!
 * This procedure interfaces to the HELP-dialog by clearing the screen,
 * calling HELP, and restoring the screen on exit.
 *
 * If 'library' is null, this program ultimately will be installed, and use the
 * standard VMS help-library.  However, if it is not currently run from the
 * system-directory, then we must assume this is a test-run, from a private
 * directory.  In this case, assume the help-library is in the same directory
 * as the program is run from.
 */
void	crt_help (char *library, char *program)
{
	char	pathname[MAX_PATH], *c_;

	if (! library)
	{
		if (! whoami (pathname, 4))
		{
			strcpy (strnull(pathname), "HELPLIB.HLB");
			if (canopen (library = pathname))
			{
				warn ("Cannot open %s", pathname);
				return;
			}
		}
	}

	crt_quit (FALSE);
	crt__ED (1,1);
	help (library, program, crt_width());
	crt_refresh();
}

/* <crt_text>:
 * Display the given text 'bfr', at the specified line (range 0..lpp-1),
 * with the 'mark' code for graphics-rendition (1=highlight, 0=normal).
 *
 * Use the 'crtvec[]' array to store the previous state of the given line,
 * to trade string copy/compare against screen I/O.
 *
 * 'crtvec[]' uses bit-7 as the flag for highlighting, allowing the display
 * driver to highlight part of any line.
 *
 * Do the blanking based on 'col_x' after all other changes have been made
 * to the line, to avoid an extra cursor movement.
 */
void	crt_text(
	char*	bfr,		/* "new" text to display		*/
	int	line,		/* index (0 to lpp-1) of line on screen	*/
	int	mode)		/* sgr-code if "highlighting"		*/
{
	static $DESCRIPTOR(DSC_line,"");

	register int	col_l;
	register int	col_r;
	register int	col_x;
	register char	*c1_	= bfr;
	register char	*c2_;
	register int	column = 0;
	register int	eql;
	register int	chg;

	char	bfr1	[CRT_COLS];
	char	bfr2	[CRT_COLS];
	static
	BYTE	to_chg[CRT_COLS],	/* distance til changed-column	*/
		to_eql[CRT_COLS];	/* distance til difference	*/

	line = max(0, min(lpp1, line));
	sgrvec[line] = mode;

	/*
	 * Copy the input string to a temporary buffer, suppressing nongraphic
	 * characters, and then trimming trailing blanks.  Explicit test for
	 * space is used to avoid problems with highlighting.
	 */
	for (c1_ = bfr1, c2_ = bfr; *c2_; c2_++)
	{
		if (isprint(toascii(*c2_)))	*c1_++ = *c2_;
		else if (toascii(*c2_) == '\t')	*c1_++ = *c2_ + (' ' - '\t');
		if ((c1_-bfr1) >= (width-1))	break;
	}
	*c1_ = EOS;
	for (c1_ = bfr1 + strlen(bfr1) - 1;
		(c1_ > bfr1) && (*c1_ == ' '); *c1_-- = EOS);

	/*
	 * Set highlighting to default, if caller uses defaults:
	 */
	if (mode == 0)
		mode	= (line >= lpp1) ? sgrBOLD : sgrREVERSE;

	/*
	 * Do string comparison to determine the limits of difference between
	 * the latest line and the prior contents.  If the older text is longer
	 * than the newer, also set index 'col_x' to show the erase-point.
	 *
	 * If the new text is longer than the old, skip leading spaces in the
	 * special case that no difference has been encountered (e.g., writing
	 * a word in the right margin on a blank line).
	 */

	col_l = col_r = col_x = 0;
	for (c1_ = bfr1, c2_ = crtvec[line]; *c1_ && *c2_; c1_++, c2_++)
	{
		if (*c1_ != *c2_)
		{
			column = c1_ - bfr1 + 1;
			if (col_l)	col_r = column;
			else		col_l = column;
		}
	}

	if (*c2_)		/* Was the old line longer than new ?	*/
		col_x = c1_ - bfr1 + 1;
	else if (*c1_)		/* If new is longer, extend right-col	*/
	{
		if (col_l == 0)	/* If no difference yet, skip leading spaces */
		{
			while (*c1_ == ' ')	c1_++;
			col_l = c1_ - bfr1 + 1;
		}
		col_r = strlen(bfr1);
	}
	if (col_l && (col_r == 0))
		col_r = col_l;

	/*
	 * If we have differences, compute a distance-vector, so that if
	 * there are long gaps of similarity, then we can set up a jump.
	 */
	if (col_l | col_r)
	{
		int	oldlen = strlen(c2_ = crtvec[line]);

		/*
		 * Pad the old string out to the length of the new part,
		 * so that the difference loop below will work.  (The
		 * column-index ranges from 1 to the length of the string.)
		 */
		c1_ = bfr1 - 1;
		c2_--;
		while (oldlen < col_r)	c2_[++oldlen] = ' ';

		/*
		 * First pass: find the length of gaps in the difference,
		 * (to_chg) and the length of the difference-sections (to_eql).
		 *
		 * Example:
		 *	'xxx...yyy' - new
		 *	'xxx!!!yyy' - old
		 *	 321000321  - chg
		 *	 000321000  - eql
		 */
		for (column = col_r, chg = eql = 0; column >= col_l; column--)
		{
			if (c1_[column] == c2_[column])
			{
				to_chg[column] = ++chg;
				to_eql[column] = eql = 0;
			}
			else
			{
				to_chg[column] = chg = 0;
				to_eql[column] = ++eql;
			}
		}

		/*
		 * Second pass: consolidate difference-sections which are
		 * too close together to jump economically.
		 * (patch: should make JUMP bigger still if there is a change
		 * in graphic-rendition on the seams.)
		 */
#define	JUMP	10
		for (column = col_l; column <= col_r;)
		{
loop:			if ((eql = column + to_eql[column]) > col_r)
				break;
			if ((chg = to_chg[eql]) < JUMP)
			{
				chg += eql;
				to_eql[column] += to_chg[eql] + to_eql[chg];
				goto loop;
			}
			else
				column = eql + to_chg[eql];
		}

		/*
		 * Finally, update the actual screen, where it has changed.
		 */
		strcpy (crtvec[line], bfr1);	/* Update internal buffer */
		strcpy (bfr2, bfr1);		/* strip parity bit */
		{ register char *s = bfr2; while (*s) *s++ &= 0x7f; }
		while (col_l <= col_r)	/* Update partial highlighting */
		{
			int	inx	= col_l - 1,
				right	= to_eql[col_l] + inx,
				mark;

			while (col_l <= right)
			{
#define	MARK	ishigh(bfr1[inx])
				inx	= col_l - 1;
				DSC_line.dsc$a_pointer = &bfr2[inx];
				for (mark = MARK;
					(inx < right) && (mark == MARK);
						inx++);
				DSC_line.dsc$w_length  = inx - col_l + 1;
				crt_move (line+1, col_l);
				lastx	+= DSC_line.dsc$w_length;
				mark	= (mark ? mode : 0);
				lib$put_screen (&DSC_line, 0, 0, &mark);
				col_l	= inx + 1;
			}
			col_l	= right + 1;
			if (col_l < col_r)	col_l += to_chg[col_l];
		}
	}
	else if (col_x)		/* (entire line was blanked) */
		strcpy (crtvec[line], bfr1);

	if (col_x)	crt__EL (line+1, col_x);
}

/* <crt_high>:
 * Mark the indicated character(s) with the highlighting bit (assume all ANSI
 * devices do some form of graphic-rendition):
 */
void	crt_high (char *s_, int len)
{
	if (!crt_vt52())
	{
		while (len > 0)
		{
			if (!s_[--len])	break;
			s_[len] |= 0x80;
		}
	}
}

/* <crt_scroll>:
 * Scroll the screen up (down) to permit the index to lie within the screen.
 * If the amount to scroll is less than the screen size, do the scrolling by
 * single-line indexing.  This code works on VT52 and VT100(-compatible) by
 * using a prevailing set of scrolling margins.
 *
 * Return the resulting line in the screen (1..endscroll).
 */
static
void	crt__null(int inx)
{
	/*nothing*/
}

int	crt_scroll (int iline, int numlines, void (*func)(int))
{
	register
	int	dif, j;
	int	close	= end_margin - top_margin - 1;

	iline = min(numlines-1, max(0, iline));

	if (func == 0)	func = crt__null;

	if (iline < top_line)
	{
		if ((dif = (top_line - iline)) < close)
		{
			while (dif-- > 0)
			{
				crt_move (top_margin, 1);
				top_line--;
				end_line = min(top_line+lpp2, end_line);
				scr$down_scroll ();
				for (j = end_margin-1; j >= top_margin; j--)
					strcpy (crtvec[j], crtvec[j-1]);
				*crtvec[0]    = EOS;
				(*func)(top_line);
			}
		}
		else
		{
			top_line -= dif;
			end_line = min(top_line+lpp2, numlines-1);
			for (j = top_margin-1; j < end_margin; j++)
				(*func)(top_line+j);
		}
	}
	else if (iline > end_line)
	{
		if ((dif = (iline - end_line)) < close)
		{
			while (dif-- > 0)
			{
				top_line++, end_line++;
				crt_move (end_margin, 1);
				scr$up_scroll ();
				for (j = top_margin-1; j < end_margin-1; j++)
					strcpy (crtvec[j], crtvec[j+1]);
				*crtvec[end_margin-1] = EOS;
				(*func)(end_line);
			}
		}
		else
		{
			top_line += dif;
			end_line += dif;
			for (j = top_margin-1; j < end_margin; j++)
				(*func)(top_line+j);
		}
	}
	return (iline-top_line+1);
}
