/*
** Copyright 2017-2020,2025 Thomas E. Dickey
** Copyright 1997-2012,2017 Free Software Foundation, Inc.
**
** This file is part of TACK.
**
** TACK 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, version 2.
**
** TACK 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 TACK; see the file COPYING.  If not, write to
** the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
** Boston, MA 02110-1301, USA
*/
/* screen formatting and I/O utility functions */

#include <tack.h>
#include <time.h>

MODULE_ID("$Id: output.c,v 1.32 2025/04/26 23:32:16 tom Exp $")

/* globals */
long char_sent;			/* number of characters sent */
int char_count;			/* counts characters */
int line_count;			/* counts line feeds */
int expand_chars;		/* length of expand() string */
int replace_mode;		/* used to output replace mode padding */
int can_go_home;		/* TRUE if we can fashion a home command */
int can_clear_screen;		/* TRUE if we can somehow clear the screen */
int raw_characters_sent;	/* Total output characters */
/* local */
static int log_column;		/* Number of characters on a log line */
static int in_logging;		/* Filter debug logging for clarity */

/* translate mode default strings */
#define TM_carriage_return	TM_string[0].value
#define TM_cursor_down		TM_string[1].value
#define TM_scroll_forward	TM_string[2].value
#define TM_newline		TM_string[3].value
#define TM_cursor_left		TM_string[4].value
#define TM_bell			TM_string[5].value
#define TM_form_feed		TM_string[6].value
#define TM_tab			TM_string[7].value

struct default_string_list TM_string[TM_last] =
{
    {"cr", "\r", 0},
    {"cud1", "\n", 0},
    {"ind", "\n", 0},
    {"nel", "\r\n", 0},
    {"cub1", "\b", 0},
    {"bel", "\007", 0},
    {"ff", "\f", 0},
    {"ht", "\t", 0}
};

static const char *c0[32] =
{
    "NUL", "SOH", "STX", "ETX", "EOT", "ENQ", "ACK",
    "BEL", "BS", "HT", "LF", "VT", "FF", "CR", "SO", "SI",
    "DLE", "DC1", "DC2", "DC3", "DC4", "NAK", "SYN", "ETB",
    "CAN", "EM", "SUB", "ESC", "FS", "GS", "RS", "US"
};

static const char *c1[32] =
{
    "", "", "", "", "IND", "NEL", "SSA", "ESA",
    "HTS", "HTJ", "VTS", "PLD", "PLU", "RI", "SS2", "SS3",
    "DCS", "PU1", "PU2", "STS", "CCH", "MW", "SPA", "EPA",
    "", "", "", "CSI", "ST", "OSC", "PM", "APC"
};

int
getnext(int mask)
{				/* get the next character without scan mode
				   conversion */
    int ch;
    unsigned char buf;

    if (debug_fp) {
	fprintf(debug_fp, "getnext:\n");
    }
    tc_putp(req_for_input);
    fflush(stdout);
    if (nodelay_read) {
	while (1) {
	    ch = (int) read(fileno(stdin), &buf, sizeof(buf));
	    if (ch == -1) {
		if (debug_fp) {
		    fprintf(debug_fp, "...getnext: EOF (nodelay)\n");
		}
		return EOF;
	    } else if (ch == 1) {
		if (debug_fp) {
		    fprintf(debug_fp, "...getnext: 0x%02X (nodelay)\n", ch);
		}
		return buf;
	    }
	}
    }
    ch = getchar();
    if (ch != EOF) {
	ch &= mask;
	if (debug_fp) {
	    fprintf(debug_fp, "...getnext: 0x%02X\n", ch);
	}
    } else {
	if (debug_fp) {
	    fprintf(debug_fp, "...getnext: EOF\n");
	}
    }
    return ch;
}

int
getchp(int mask)
{				/* read a character with scan mode conversion */
    if (scan_mode) {
	tc_putp(req_for_input);
	fflush(stdout);
	return scan_key();
    } else
	return getnext(mask);
}

int
log_chr(FILE *fp, int ch, int col)
{
    if (ch < 32) {
	fprintf(fp, "<%s>", c0[ch]);
	col += 5;
    } else if (ch < 127) {
	fprintf(fp, "%c", (char) ch);
	col += 1;
    } else {
	fprintf(fp, "<%02x>", ch);
	col += 4;
    }
    if ((ch == '\n' && col > 0) || col >= 80) {
	fprintf(fp, "\n");
	col = 0;
    }
    return col;
}

void
log_str(FILE *fp, const char *value)
{
    if (value == NULL) {
	fputs("<null>", fp);
    } else if (value == (const char *) -1) {
	fputs("<cancel>", fp);
    } else {
	int ch;
	while ((ch = UChar(*value++)) != '\0') {
	    log_chr(fp, ch, -10);
	}
    }
}

/*
**	tc_putch(c)
**
**	Output one character
*/
int
tc_putch(TC_PUTCH c)
{
    char_sent++;
    raw_characters_sent++;
    putchar(c);
    if ((raw_characters_sent & 31) == 31) {
	fflush(stdout);
    }
    if (log_fp) {
	log_column = log_chr(log_fp, UChar(c), log_column);
    }
    return (c);
}

/*
**	tt_tputs(string, reps)
**
**	Output a string with tputs() translation.
**	Use this function inside timing tests.
*/
void
tt_tputs(const char *string, int reps)
{
    ++in_logging;
    if (debug_fp && (in_logging == 1)) {
	fprintf(debug_fp, "tt_tputs: string=");
	log_str(debug_fp, string);
	fprintf(debug_fp, ", reps=%d\n", reps);
    }
    if (string) {
	int i;

	for (i = 0; i < TT_MAX; i++) {
	    if (i >= ttp) {
		tt_cap[i] = string;
		tt_affected[i] = reps;
		tt_count[i] = 1;
		tt_delay[i] = msec_cost(string, reps);
		ttp++;
		break;
	    }
	    if (string == tt_cap[i] && reps == tt_affected[i]) {
		tt_count[i]++;
		tt_delay_used += tt_delay[i];
		break;
	    }
	}
	(void) tputs(string, reps, tc_putch);
    }
    --in_logging;
}

/*
**	tt_putp(string)
**
**	Output a string with tputs() translation.
**	Use this function inside timing tests.
*/
void
tt_putp(const char *string)
{
    ++in_logging;
    if (debug_fp && (in_logging == 1)) {
	fprintf(debug_fp, "tt_putp: string=");
	log_str(debug_fp, string);
	fprintf(debug_fp, "\n");
    }
    tt_tputs(string, 1);
    --in_logging;
}

/*
**	tt_putparm(string, reps, arg1, arg2)
**
**	Send tt_tputs(tparm(string, args1, arg2), reps)
**	Use this function inside timing tests.
*/
void
tt_putparm(
	      NCURSES_CONST char *string,
	      int reps,
	      int arg1,
	      int arg2)
{
    if (string) {
	int i;

	++in_logging;
	if (debug_fp && (in_logging == 1)) {
	    fprintf(debug_fp, "tc_putparm: string=");
	    log_str(debug_fp, string);
	    fprintf(debug_fp, ", reps=%d\n", reps);
	}
	for (i = 0; i < TT_MAX; i++) {
	    if (i >= ttp) {
		tt_cap[i] = string;
		tt_affected[i] = reps;
		tt_count[i] = 1;
		tt_delay[i] = msec_cost(string, reps);
		ttp++;
		break;
	    }
	    if (string == tt_cap[i] && reps == tt_affected[i]) {
		tt_count[i]++;
		tt_delay_used += tt_delay[i];
		break;
	    }
	}
	(void) tputs(TPARM_2((NCURSES_CONST char *) string, arg1, arg2),
		     reps,
		     tc_putch);
	--in_logging;
    }
}

/*
**	tc_putp(string)
**
**	Output a string with tputs() translation.
**	Use this function instead of putp() so we can track
**	the actual number of characters sent.
*/
int
tc_putp(const char *string)
{
    int rc = -1;
    if (VALID_STRING(string)) {
	++in_logging;
	rc = tputs(string, 1, tc_putch);
	if (debug_fp && (in_logging == 1)) {
	    fprintf(debug_fp, "tc_putp: string=");
	    log_str(debug_fp, string);
	    fprintf(debug_fp, ", rc=%d\n", rc);
	}
	--in_logging;
    }
    return rc;
}

void
put_this(int c)
{				/* output one character (with padding) */
    tc_putch((TC_PUTCH) c);
    if (char_padding && replace_mode)
	tt_putp(char_padding);
}

void
put_cr(void)
{
    if (translate_mode && carriage_return) {
	tt_putp(carriage_return);
    } else {
	tt_putp(TM_carriage_return);
    }
    char_count = 0;
}

void
put_lf(void)
{				/* send a linefeed (only works in RAW or
				   CBREAK mode) */
    if (translate_mode && cursor_down) {
	tt_putp(cursor_down);
    } else {
	tt_putp(TM_cursor_down);
    }
    line_count++;
}

void
put_ind(void)
{				/* scroll forward (only works in RAW or
				   CBREAK mode) */
    if (translate_mode && scroll_forward) {
	tt_putp(scroll_forward);
    } else {
	tt_putp(TM_scroll_forward);
    }
    line_count++;
}

/*
**	put_crlf()
**
**	Send (nel)  or <cr> <lf>
*/
void
put_crlf(void)
{
    if (translate_mode && newline) {
	tt_putp(newline);
    } else {
	tt_putp(TM_newline);
    }
    char_count = 0;
    line_count++;
}

/*
**	put_new_lines(count)
**
**	Send a number of newlines. (nel)
*/
void
put_newlines(int n)
{
    while (n-- > 0) {
	put_crlf();
    }
}

/*
**	putchp(character)
**
**	Send one character to the terminal.
**	This function does translation of control characters.
*/
void
putchp(int c)
{
    switch (c) {
    case '\b':
	if (translate_mode && cursor_left) {
	    tt_putp(cursor_left);
	} else {
	    tt_putp(TM_cursor_left);
	}
	char_count--;
	break;
    case 7:
	if (translate_mode && bell) {
	    tt_putp(bell);
	} else {
	    tt_putp(TM_bell);
	}
	break;
    case '\f':
	if (translate_mode && form_feed) {
	    tt_putp(form_feed);
	} else {
	    tt_putp(TM_form_feed);
	}
	char_count = 0;
	line_count++;
	break;
    case '\n':
	put_crlf();
	break;
    case '\r':
	put_cr();
	break;
    case '\t':
	if (translate_mode && tab) {
	    tt_putp(tab);
	} else {
	    tt_putp(TM_tab);
	}
	char_count = ((char_count / 8) + 1) * 8;
	break;
    default:
	put_this(c);
	char_count++;
	break;
    }
}

void
put_str(const char *s)
{				/* send the string to the terminal */
    for (; *s; putchp(*s++)) ;
}

void
putln(const char *s)
{				/* output a string followed by a CR LF */
    for (; *s; putchp(*s++)) ;
    put_crlf();
}

void
put_columns(const char *s, int len, int w)
{				/* put out s in column format */
    int l;

    if (char_count + w > columns) {
	put_crlf();
    }
    l = char_count % w;
    if (l) {
	while (l < w) {
	    putchp(' ');
	    l++;
	}
    } else if (char_count) {
	putchp(' ');
    }
    if (char_count && char_count + len >= columns) {
	put_crlf();
    }
    l = char_count;
    put_str(s);
    char_count = l + len;
}

/*
**	ptext(string)
**
**	Output a string but do not assume the terminal will wrap to a
**	new line.  Break the line at a word boundary then send a CR LF.
**	This is more esthetic on 40 column terminals.
*/
void
ptext(const char *s)
{
    const char *t;

    while (*s) {
	for (t = s + 1; *t > ' '; t++) ;
	if ((char_count != 0) && ((t - s) + char_count >= columns)) {
	    put_crlf();
	    while (*s == ' ')
		s++;
	}
	while (s < t) {
	    putchp(*s++);
	}
    }
}

void
ptextln(const char *s)
{				/* print the text using ptext() then add a CR
				   LF */
    ptext(s);
    put_crlf();
}

static void
expand_one(int ch, char **v)
{				/* expand one character */
    char *t = *v;

    if (ch & 0x80) {		/* dump it in octal (yuck) */
	*t++ = '\\';
	*t++ = (char) ('0' + ((ch >> 6) & 3));
	*t++ = (char) ('0' + ((ch >> 3) & 7));
	*t++ = (char) ('0' + (ch & 7));
	expand_chars += 4;
    } else if (ch == 127) {	/* DEL */
	*t++ = '^';
	*t++ = '?';
	expand_chars += 2;
    } else if (ch >= ' ') {
	*t++ = (char) ch;
	expand_chars++;
    } else {			/* control characters */
	*t++ = '^';
	*t++ = (char) (ch + '@');
	expand_chars += 2;
    }
    *v = t;
}

char *
expand(const char *s)
{				/* convert the string to printable form */
    static char buf[4096];
    char *t;
    const char *v;

    if (magic_cookie_glitch <= 0 && exit_attribute_mode) {
	v = enter_reverse_mode;
    } else {
	v = NULL;
    }

    expand_chars = 0;
    t = buf;

    if (s) {
	int ch;
	for (; (ch = *s); s++) {
	    if ((ch & 0x80) && v) {	/* print it in reverse video
					   mode */
		strcpy(t, liberated(TPARM_0(v)));
		for (; *t; t++) ;
		expand_one(ch & 0x7f, &t);
		strcpy(t, liberated(TPARM_0(exit_attribute_mode)));
		for (; *t; t++) ;
	    } else {
		expand_one(ch, &t);
	    }
	}
    }
    *t = '\0';
    return buf;
}

char *
print_expand(char *s)
{				/* convert the string to 7-bit printable form */
    static char buf[4096];
    char *t;

    expand_chars = 0;
    t = buf;

    if (s) {
	int ch;
	for (; (ch = *s); s++) {
	    expand_one(ch, &t);
	}
    }
    *t = '\0';
    return buf;
}

char *
expand_to(char *s, int l)
{				/* expand s to length l */
    char *t;

    for (s = t = expand(s); *t; t++) ;
    for (; expand_chars < l; expand_chars++) {
	*t++ = ' ';
    }
    *t = '\0';
    return s;
}

char *
hex_expand_to(char *s, int l)
{				/* expand s to length l in hex */
    static char buf[4096];
    char *t;

    for (t = buf; *s; s++) {
	sprintf(t, "%02X ", UChar(*s));
	t += 3;
	if (t - buf > (int) sizeof(buf) - 4) {
	    break;
	}
    }
    for (; t - buf < l;) {
	*t++ = ' ';
    }
    *t = '\0';
    expand_chars = (int) (t - buf);
    return buf;
}

char *
expand_command(const char *c)
{				/* expand an ANSI escape sequence */
    static char buf[256];
    int i, j, ch;
    char *s;

    s = buf;
    for (i = FALSE; (ch = UChar(*c)) != 0; c++) {
	if (i) {
	    *s++ = ' ';
	}
	i = TRUE;
	if (ch < 32) {
	    j = UChar(c[1]);
	    if (ch == '\033' && j >= '@' && j <= '_') {
		ch = j - '@';
		c++;
		for (j = 0; (*s = c1[ch][j++]); s++) ;
	    } else
		for (j = 0; (*s = c0[ch][j++]); s++) ;
	} else {
	    *s++ = (char) ch;
	    j = UChar(c[1]);
	    if (ch >= '0' && ch <= '9' &&
		j >= '0' && j <= '9') {
		i = FALSE;
	    }
	}
    }
    *s = '\0';
    return buf;
}

/*
**	go_home()
**
**	Move the cursor to the home position
*/
void
go_home(void)
{
    int i;

    if (cursor_home)
	tt_putp(cursor_home);
    else if (cursor_address)
	tt_putparm(cursor_address, lines, 0, 0);
    else if (row_address) {	/* use (vpa) */
	put_cr();
	tt_putparm(row_address, 1, 0, 0);
    } else if (cursor_up && cursor_to_ll) {
	tt_putp(cursor_to_ll);
	for (i = 1; i < lines; i++) {
	    tt_putp(cursor_up);
	}
    } else {
	can_go_home = FALSE;
	return;
    }
    char_count = line_count = 0;
    can_go_home = TRUE;
}

void
home_down(void)
{				/* move the cursor to the lower left hand
				   corner */
    int i;

    if (cursor_to_ll)
	tt_putp(cursor_to_ll);
    else if (cursor_address)
	tt_putparm(cursor_address, lines, lines - 1, 0);
    else if (row_address) {	/* use (vpa) */
	put_cr();
	tt_putparm(row_address, 1, lines - 1, 0);
    } else if (cursor_down && cursor_home) {
	tt_putp(cursor_home);
	for (i = 1; i < lines; i++)
	    tt_putp(cursor_down);
    } else
	return;
    char_count = 0;
    line_count = lines - 1;
}

void
put_clear(void)
{				/* clear the screen */
    int i;

    if (debug_fp) {
	fprintf(debug_fp, "put_clear:\n");
    }
    if (clear_screen)
	tt_tputs(clear_screen, lines);
    else if (clr_eos && can_go_home) {
	go_home();
	tt_tputs(clr_eos, lines);
    } else if (scroll_forward && !over_strike && (can_go_home || cursor_up)) {
	/* clear the screen by scrolling */
	put_cr();
	if (cursor_to_ll) {
	    tt_putp(cursor_to_ll);
	} else if (cursor_address) {
	    tt_putparm(cursor_address, lines, lines - 1, 0);
	} else if (row_address) {
	    tt_putparm(row_address, 1, lines - 1, 0);
	} else {
	    for (i = 1; i < lines; i++) {
		tt_putp(scroll_forward);
	    }
	}
	for (i = 1; i < lines; i++) {
	    tt_putp(scroll_forward);
	}
	if (can_go_home) {
	    go_home();
	} else {
	    for (i = 1; i < lines; i++) {
		tt_putp(cursor_up);
	    }
	}
    } else {
	can_clear_screen = FALSE;
	if (debug_fp) {
	    fprintf(debug_fp, "...put_clear: ERR\n");
	}
	return;
    }
    char_count = line_count = 0;
    can_clear_screen = TRUE;
    if (debug_fp) {
	fprintf(debug_fp, "...put_clear: OK\n");
    }
}

/*
**	wait_here()
**
**	read one character from the input stream
**	If the terminal is not in RAW mode then this function will
**	wait for a <cr> or <lf>.
*/
int
wait_here(void)
{
    char cc[64];
    char message[16];
    int i, j;

    for (i = 0; i < (int) sizeof(cc); i++) {
	int ch = getchp(STRIP_PARITY);
	if (ch == EOF)
	    return ch;
	cc[i] = (char) ch;
	if (ch == '\r' || ch == '\n') {
	    put_crlf();
	    char_sent = 0;
	    return cc[i ? i - 1 : 0];
	}
	if (ch >= ' ') {
	    if (stty_query(TTY_CHAR_MODE)) {
		put_crlf();
		char_sent = 0;
		return ch;
	    }
	    continue;
	}
	if (ch == 023) {	/* Control S */
	    /* ignore control S, but tell me about it */
	    while (ch == 023 || ch == 021) {
		ch = getchp(STRIP_PARITY);
		if (ch == EOF)
		    break;
		if (i + 1 < (int) sizeof(cc))
		    cc[++i] = (char) ch;
	    }
	    put_str("\nThe terminal sent a ^S -");
	    for (j = 0; j <= i; j++) {
		sprintf(message, " %02X", cc[j] & 0xFF);
		put_str(message);
	    }
	    put_crlf();
	    i = -1;
	} else if (ch != 021) {	/* Not Control Q */
	    /* could be abort character */
	    spin_flush();
	    if (tty_can_sync == SYNC_TESTED) {
		(void) tty_sync_error();
	    } else {
		put_str("\n? ");
	    }
	}
    }
    return '?';
}

/*
**	read_string(buffer, length)
**
**	Read a string of characters from the input stream.
*/
void
read_string(char *buf,
	    size_t length)
{
    size_t i;

    if (length > 1) {
	for (i = 0; i < length - 1;) {
	    int ch = getchp(STRIP_PARITY);
	    if (ch == '\r' || ch == '\n' || ch == EOF) {
		break;
	    }
	    if (ch == '\b' || ch == 127) {
		if (i) {
		    putchp('\b');
		    putchp(' ');
		    putchp('\b');
		    i--;
		}
	    } else {
		buf[i++] = (char) ch;
		putchp(ch);
	    }
	}
    } else {
	i = 0;
    }
    buf[i] = '\0';
    put_crlf();
    char_sent = 0;
}

/*
**	maybe_wait(lines)
**
**	wait if near the end of the screen, then clear screen
*/
void
maybe_wait(int n)
{
    if (line_count + n >= lines) {
	if (char_sent != 0) {
	    ptext("Go? ");
	    (void) wait_here();
	}
	put_clear();
    } else {
	put_crlf();
    }
}
