/* Terminal control based on terminfo capabilities.

   Copyright (C) 1985, 1986, 1987 Free Software Foundation, Inc.
    Originally part of GNU Emacs.
    Vastly edited and modified for use within ne.

   Copyright (C) 1993 Sebastiano Vigna

    This file is part of ne, the nice editor.

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 1, or (at your option)
    any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.

In other words, you are welcome to use, share and improve this program.
You are forbidden to forbid anyone else to use, share and improve
what you give them.   Help stamp out software-hoarding!  */


#include <stdio.h>
#include <ctype.h>
#include <curses.h>
#include <term.h>
#include "termchar.h"
#include "cm.h"


/* This is the real instantiation of the cm structure used by cm.c to hold
the cursor motion strings. */

struct cm Wcm;


#define max(a, b) ((a) > (b) ? (a) : (b))
#define min(a, b) ((a) < (b) ? (a) : (b))


/* PORTABILITY PROBLEM: this macro is responsible for filtering nonprintable
characters. On systems with a wider system character set, it could be redefined,
for instance, in order to allow characters after 160 to be printed. */

#define DECONTROL(c)		{ if ((c) >= 127 && (c) < 160) c = '?'; if ((c) < ' ') (c) += '@'; }



#define OUTPUT(a) tputs (a, lines - curY, cmputc)
#define OUTPUT1(a) tputs (a, 1, cmputc)
#define OUTPUTL(a, lines) tputs (a, lines, cmputc)
#define OUTPUT_IF(a) { if (a) tputs (a, lines - curY, cmputc); }
#define OUTPUT1_IF(a) { if (a) tputs (a, 1, cmputc); }



/* Terminal charateristics that higher levels want to look at.
   These are all extern'd in termchar.h */

int	line_ins_del_ok;		/* Terminal can insert and delete lines */
int	char_ins_del_ok;		/* Terminal can insert and delete chars */
int	scroll_region_ok;		/* Terminal supports setting the scroll window */
int	standout_ok;			/* Terminal supports standout without magic cookies */


static int	RPov;		/* Least number of chars to start a TS_repeat.
								Less wouldn't be worth. */

static int	delete_in_insert_mode;	/* delete mode == insert mode */
static int	save_cursor_ok;			/* Terminal supports save and restore of cursor position */
static int	se_is_so;					/* 1 if same string both enters and leaves standout mode */

static int	standout_requested;	/* Nonzero when supposed to write text in standout mode. */
static int	insert_mode;			/* Nonzero when in insert mode. */
static int	standout_mode;			/* Nonzero when in standout mode. */


/* Size of window specified by higher levels. This is the number of lines,
starting from top of screen, to participate in ins/del line operations.
Effectively it excludes the bottom lines - specified_window_size lines from
those operations.  */

int	specified_window;



/* These functions ring a bell or flash the screen. If the service
is not available, the other one is tried. */


void ring_bell(void) {
	OUTPUT1_IF (bell ? bell : flash_screen);
}


void do_flash(void) {
	OUTPUT1_IF (flash_screen ? flash_screen : bell);
}



/* This function sets correctly the scroll region. Note that the cursor
is lost.  This function assumes scroll_region_ok == TRUE.  The cursor
position is lost, and from the terminfo specs. */

static void set_scroll_region (int start, int stop) {

	unsigned char *buf;

	assert(scroll_region_ok);

	if (save_cursor_ok) OUTPUT1(save_cursor);

	if (change_scroll_region) {
		buf = tparm (change_scroll_region, start, stop - 1);
	}
	else {
		buf = tparm (set_window, start, stop - 1, 0, columns - 1);
	}

	OUTPUT1(buf);
	free (buf);

	if (save_cursor_ok) OUTPUT1(restore_cursor);
	else losecursor();
}


static void turn_on_insert (void) {
	if (!insert_mode)
		OUTPUT1(enter_insert_mode);
	insert_mode = TRUE;
}


static void turn_off_insert (void) {
	if (insert_mode)
		OUTPUT1(exit_insert_mode);
	insert_mode = FALSE;
}


/* These functions are called on all terminals in order to handle highlighting,
but do nothing on terminals with a magic cookie (or without standout).  */

static void turn_off_highlight (void) {
	if (standout_ok) {
		if (standout_mode)
			OUTPUT1(exit_standout_mode);
		standout_mode = FALSE;
	}
}


static void turn_on_highlight (void) {
	if (standout_ok) {
		if (!standout_mode)
			OUTPUT1(enter_standout_mode);
		standout_mode = TRUE;
	}
}


/* This function prepare the terminal for interactive I/O. It
initializes the terminal, prepares the cursor address mode, and
activates the keypad and the meta key. */

void set_terminal_modes(void) {

	/* Note that presently we do not support if and iprog, the program
	and the file which should be used, if present, to initialize the
	terminal. */

	OUTPUT1_IF(init_1string);
	OUTPUT1_IF(init_2string);
	OUTPUT1_IF(init_3string);

	OUTPUT1_IF(enter_ca_mode);
	OUTPUT1_IF(keypad_xmit);

	if (has_meta_key) OUTPUT1_IF(meta_on);

	losecursor();
}


/* This function puts again the terminal in its normal state. */

void reset_terminal_modes (void) {

	turn_off_highlight ();
	turn_off_insert ();

	OUTPUT1_IF (keypad_local);
	OUTPUT1_IF (exit_ca_mode);
}


/* This function sets the variable specified_window. Following
line insert/delete operations will be limited to lines 0 to (size-1). */

void set_terminal_window(int size) {

	specified_window = size ? size : lines;

}




/* These functions are the external interface to standout activation. */

void standout_on (void) {
	standout_requested = TRUE;
}


void standout_off (void) {
	standout_requested = FALSE;
}


/* Set standout mode to the mode specified for the text to be output.  */

static void highlight_if_desired (void) {

	if (standout_requested) turn_on_highlight ();
	else turn_off_highlight ();
}


/* Move to absolute position, specified origin 0 */

void move_cursor (int row, int col) {

	if (curY == row && curX == col) return;

	if (!move_standout_mode) turn_off_highlight ();

	if (!move_insert_mode) turn_off_insert ();

	cmgoto (row, col);
}


/* This function clears from the cursor position to the end of line. It assumes
that the line is already clear starting at column first_unused_hpos. Note that
the cursor may be moved, on terminals lacking a `ce' string.  */

void clear_end_of_line(int first_unused_hpos) {

	register int	i;

	if (curX >= first_unused_hpos) return;

	if (clr_eol) {
		OUTPUT1 (clr_eol);
	}
	else {

		/* We have to do it the hard way. */

		turn_off_insert ();

		for (i = curX; i < first_unused_hpos; i++) putchar (' ');

		cmplus (first_unused_hpos - curX);
	}
}


/* Shorthand; use this if you don't know anything about the state
of the line. */

void clear_to_eol(void) {
	clear_end_of_line(columns);
}


/* This function clears from the cursor position to the end of screen */

void clear_to_end (void) {

	register int	i;

	if (clr_eos) {
		OUTPUT(clr_eos);
	}
	else {
		for (i = curY; i < lines; i++) {
			move_cursor (i, 0);
			clear_to_eol();
		}
	}
}


/* This function clears the entire screen */

void clear_entire_screen (void) {

	if (clear_screen) {
		OUTPUTL(clear_screen, lines);
		cmat (0, 0);
	}
	else {
		move_cursor (0, 0);
		clear_to_end();
	}
}


/* This function outputs len characters pointed at by string. The characters
will be truncated to the end of the current line. Passing a NULL for string
results in outputting spaces. A len of 0 causes no action. */

void output_chars (const unsigned char *string, int len) {

	register const unsigned char *p;
	register unsigned char *buf;
	register int	c;
	const unsigned char *first_check;

	if (len == 0) return;

	highlight_if_desired ();
	turn_off_insert ();

	if (curX + len > columns) len = columns - curX;

	/* Don't dare write in last column of bottom line, if AutoWrap,
	since that would scroll the whole screen on some terminals.  */

	if (AutoWrap && curY + 1 == lines && curX + len == columns) len--;

	cmplus (len);

	if (string == NULL) {
		if (len > RPov) {
			buf = tparm (repeat_char, ' ', len);
			OUTPUT1(buf);
			free (buf);
		}
		else {
			while(--len >= 0) putchar(' ');
		}
		return;
	}

	first_check = string;

	if (RPov > len && !transparent_underline && !tilde_glitch) {
		while(--len >= 0) {
			c = *string++;
			DECONTROL(c);
			putchar(c);
		}
	} else
		while (--len >= 0) {
			c = *string;
			if (RPov + 1 < len && string >= first_check) {
				int	repeat_count;

				p = string;

				/* Now, len is number of chars left starting at p */
				while (++p - string <= len && *p == c);

				repeat_count = p - string;

				if (repeat_count > RPov) {
					buf = tparm (repeat_char, c, repeat_count);
					OUTPUT1(buf);
					free (buf);
					string = p;
					len -= repeat_count - 1;
					continue;
				}
				else {
					/* If all N identical chars are too few,
		 			don't even consider the last N-1, the last N-2,...  */

					first_check = p;
				}
			}

			if (c == '_' && transparent_underline) {
				putchar (' ');
				OUTPUT1(Left);
			}

			if (tilde_glitch && c == '~')
				c = '`';

			DECONTROL(c);
			putchar (c);
			string++;
		}
}



/* Same as output_chars(), but inserts instead. */

void insert_chars (const unsigned char *start, int len) {

	register unsigned char *buf;
	register int c;

	if (len == 0) return;

	highlight_if_desired ();

	if (parm_ich) {

		buf = tparm (parm_ich, len);
		OUTPUT1 (buf);
		free (buf);

		if (start) output_chars (start, len);

		return;
	}

	if (curX + len > columns) len = columns - curX;

	/* Don't dare write in last column of bottom line, if AutoWrap,
	since that would scroll the whole screen on some terminals.  */

	if (AutoWrap && curY + 1 == lines && curX + len == columns) len--;

	turn_on_insert ();
	cmplus (len);

	if (!transparent_underline && !tilde_glitch && start
	     && insert_padding == NULL && insert_character == NULL) {
		while(--len >= 0) {
			c = *start++;
			DECONTROL(c);
			putchar(c);
		}
	}
	else
		while (--len >= 0) {

			OUTPUT1_IF (insert_character);

			if (!start) c = ' ';
			else {
				c = *start++;
				if (tilde_glitch && c == '~') c = '`';
			}

			DECONTROL(c);
			putchar (c);
			OUTPUT1_IF(insert_padding);
		}
}



/* This function deletes n characters at the current cursor position. */

void delete_chars (int n) {

	unsigned char *buf;

	if (delete_in_insert_mode) {
		turn_on_insert();
	}
	else {
		turn_off_insert();
		OUTPUT1_IF(enter_delete_mode);
	}

	if (parm_dch) {
		buf = tparm(parm_dch, n);
		OUTPUT1(buf);
		free (buf);
	}
	else
		while(--n>=0) OUTPUT1(delete_character);

	if (!delete_in_insert_mode)
		OUTPUT_IF(exit_delete_mode);
}


/* This internal function will do an insertion or deletion
for n lines, given a parametrized and/or a one-line capability
for that purpose. */

static void do_multi_ins_del(char *multi, char *single, int n) {

	if (multi) {

		unsigned char *buf;

		buf = tparm(multi, n);
		OUTPUT(buf);
		free(buf);
	}
	else {
		while(--n>=0) OUTPUT(single);
	}
}


/* This function inserts n lines at vertical position vpos. If n is negative,
it deletes -n lines. specified_window is taken into account. This function
assumes line_ins_del_ok == TRUE. */

void ins_del_lines (int vpos, int n) {

	register int	i = n > 0 ? n : -n;

	assert(line_ins_del_ok);
	assert(i != 0);
	assert(vpos < specified_window);

	if (scroll_region_ok && vpos + i >= specified_window)
		return;

	if (!memory_below && vpos + i >= lines)
		return;

	if (scroll_region_ok) {
		if (specified_window != lines)
			set_scroll_region(vpos, specified_window);

		if (n < 0) {
			move_cursor(specified_window - 1, 0);
			while (--i >= 0)
				OUTPUTL(scroll_forward, specified_window-vpos+1);
		}
		else {
			move_cursor(vpos, 0);
			while (--i >= 0)
				OUTPUTL(scroll_reverse, specified_window-vpos+1);
		}

		if (specified_window != lines)
			set_scroll_region(0, lines);
	}
	else {
		if (n > 0) {
			if (specified_window != lines) {
				move_cursor(specified_window - i, 0);
				do_multi_ins_del(parm_delete_line, delete_line, i);
			}

			move_cursor(vpos, 0);
			do_multi_ins_del(parm_insert_line, insert_line, i);
		}
		else {
			move_cursor(vpos, 0);
			do_multi_ins_del(parm_delete_line, delete_line, i);

			if (specified_window != lines) {
				move_cursor(specified_window-i, 0);
				do_multi_ins_del(parm_insert_line, insert_line, i);
			}
			else if (memory_below) {
				move_cursor(lines + n, 0);
				clear_to_end ();
			}
		}
	}
}


extern int cost;		/* In cm.c */
extern int evalcost ();


/* This function performs the cursor motion cost setup, and sets the
variable RPov to the number of characters (with padding) which are
really output when repeating one character. */

static void calculate_costs (void) {

	if (repeat_char) {

		unsigned char *buf = tparm (repeat_char, ' ', 1);

		cost = 0;
		tputs(buf, 1, evalcost);

		RPov = cost+1;
		free(buf);
	}
	else RPov = columns * 2;

	cmcostinit();
}


/* This is the main terminal initialization function. It sets up Wcm,
patches here and there the terminfo database, calculates the costs, and
initializes the terminal characteristics variables. Note that this function
can exit(). */

void term_init (void) {

	/* First of all we initialize the terminfo database. */

	setupterm(0,1,0);

	ColPosition = column_address;
	RowPosition = row_address;
	AbsPosition = cursor_address;
	CR = carriage_return;
	Home = cursor_home;
	LastLine = cursor_to_ll;
	Right = cursor_right;
	Down = cursor_down;
	Left = cursor_left;
	Up = cursor_up;
	AutoWrap = auto_right_margin;
	MagicWrap = eat_newline_glitch;
	ScreenRows = lines;
	ScreenCols = columns;

	if (!bell) bell = "\07";

	if (!scroll_forward) scroll_forward = Down;
	if (!scroll_reverse) scroll_reverse = Up;

	if (key_backspace && key_left && !strcmp(key_backspace, key_left)) {
		/* In case left and backspace produce the same sequence,
		we want to get key_left. */

		key_backspace = NULL;
	}

	specified_window = lines;

	if (Wcm_init ())	{

		/* We can't do cursor motion */

		if (generic_type) {
			printf("Your terminal type is a generic terminal, not a real\nterminal, and it lacks the ability to position the cursor.\nPlease check that the variable TERM is set correctly, and that\nyour terminfo database is up to date.\n");
		}
		else {
			printf("Your terminal type is not powerful enough to run ne:\nit lacks the ability to position the cursor.\nPlease check that the variable TERM is set correctly, and that\nyour terminfo database is up to date.\n");
		}

		resetterm();
		exit(1);
	}

	calculate_costs();

	delete_in_insert_mode
	     = enter_delete_mode && enter_insert_mode
	     && !strcmp (enter_delete_mode, enter_insert_mode);

	se_is_so = enter_standout_mode && exit_standout_mode
	     && !strcmp (enter_standout_mode, exit_standout_mode);

	scroll_region_ok = set_window || change_scroll_region;

	line_ins_del_ok = (((insert_line || parm_insert_line)
	     && (delete_line || parm_delete_line))
	     || (scroll_region_ok
	     && scroll_forward
	     && scroll_reverse));

	char_ins_del_ok = ((insert_character || enter_insert_mode ||
	    insert_padding || parm_ich)
	     && (delete_character || parm_dch));

	save_cursor_ok = (save_cursor && restore_cursor);

	standout_ok = (enter_standout_mode && exit_standout_mode && magic_cookie_glitch < 0);
}
