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

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

MODULE_ID("$Id: sync.c,v 1.23 2025/04/29 08:14:28 tom Exp $")

/* terminal-synchronization and performance tests */

static void sync_home(TestList *, int *, int *);
static void sync_lines(TestList *, int *, int *);
static void sync_clear(TestList *, int *, int *);
static void sync_summary(TestList *, int *, int *);

static TestList sync_test_list[] =
{
    {MENU_NEXT, 0, NULL, NULL, "b) baud rate test", sync_home, NULL},
    {MENU_NEXT, 0, NULL, NULL, "l) scroll performance", sync_lines, NULL},
    {MENU_NEXT, 0, NULL, NULL, "c) clear screen performance", sync_clear, NULL},
    {MENU_NEXT, 0, NULL, NULL, "p) summary of results", sync_summary, NULL},
    {0, 0, NULL, NULL, txt_longer_test_time, longer_test_time, NULL},
    {0, 0, NULL, NULL, txt_shorter_test_time, shorter_test_time, NULL},
    {MENU_LAST, 0, NULL, NULL, NULL, NULL, NULL}
};

TestMenu sync_menu =
{
    0, 'n', NULL,
    "Performance tests", "perf", "n) run standard tests",
    sync_test, sync_test_list, NULL, 0, 0
};

int tty_can_sync;		/* TRUE if tty_sync_error() returned FALSE */
static int tty_newline_rate;	/* The number of newlines per second */
static int tty_clear_rate;	/* The number of clear-screens per second */
unsigned long tty_cps;		/* The number of characters per second */

static int ACK_terminator;	/* terminating ACK character */
static int ACK_length;		/* length of ACK string */
static const char *tty_ENQ;	/* enquire string */
static char tty_ACK[128];	/* ACK response, set by tty_sync_error() */

/*****************************************************************************
 *
 * Terminal synchronization.
 *
 *	These functions handle the messy business of enq-ack handshaking
 *	for timing purposes.
 *
 *****************************************************************************/

int
tty_sync_error(void)
{
    int trouble;

    trouble = FALSE;
    for (;;) {
	int ch, ack;

	tt_putp(tty_ENQ);	/* send ENQ */
	ch = getnext(STRIP_PARITY);
	event_start(TIME_SYNC);	/* start the timer */

	/*
	   The timer doesn't start until we get the first character.
	   After that I expect to get the remaining characters of
	   the acknowledge string in a short period of time.  If
	   that is not true then these characters are coming from
	   the user and we need to send the ENQ sequence out again.
	 */
	for (ack = 0;;) {
	    if (ack < (int) (sizeof(tty_ACK) - 2)) {
		tty_ACK[ack] = (char) ch;
		tty_ACK[ack + 1] = '\0';
	    }
	    if (ch == ACK_terminator) {
		return trouble;
	    }
	    if (++ack >= ACK_length) {
		return trouble;
	    }
	    ch = getnext(STRIP_PARITY);
	    if (event_time(TIME_SYNC) > 400000) {
		break;
	    }
	}

	set_attr(0);		/* just in case */
	put_crlf();
	if (trouble) {
	    /* The terminal won't sync.  Life is not good. */
	    return TRUE;
	}
	put_str(" -- sync -- ");
	trouble = TRUE;
    }
}

/*
**	flush_input()
**
**	Throw away any output.
*/
void
flush_input(void)
{
    if (tty_can_sync == SYNC_TESTED && ACK_terminator >= 0) {
	(void) tty_sync_error();
    } else {
	spin_flush();
    }
}

/*
**	probe_enq_ok()
**
**	does the terminal do enq/ack handshaking?
*/
static void
probe_enq_ok(void)
{
    int len;

    put_str("Testing ENQ/ACK, standby...");
    fflush(stdout);
    can_test("u8 u9", FLAG_TESTED);

#ifdef user9
    tty_ENQ = user9 ? user9 : "\005";
#else
    tty_ENQ = "\005";
#endif
    tc_putp(tty_ENQ);
    event_start(TIME_SYNC);	/* start the timer */
    read_key(tty_ACK, (sizeof(tty_ACK) - 1));

    if (event_time(TIME_SYNC) > 400000 || tty_ACK[0] == '\0') {
	/* These characters came from the user.  Sigh. */
	tty_can_sync = SYNC_FAILED;
	ptext("\nThis program expects the ENQ sequence to be");
	ptext(" answered with the ACK character.  This will help");
	ptext(" the program reestablish synchronization when");
	ptextln(" the terminal is overrun with data.");
	ptext("\nENQ sequence from (u9): ");
	putln(expand(tty_ENQ));
	ptext("ACK received: ");
	putln(expand(tty_ACK));
#ifdef user8
	len = VALID_STRING(user8) ? (int) strlen(user8) : 0;
#else
	len = 0;
#endif
	sprintf(temp, "Length of ACK %d.", (int) strlen(tty_ACK));
	ptextln(temp);
#ifdef user8
	if (len) {
	    temp[0] = user8[len - 1];
	    temp[1] = '\0';
	    ptext("Terminating character found in (u8): ");
	    putln(expand(temp));
	}
#endif
	return;
    }

    tty_can_sync = SYNC_TESTED;
    if ((len = (int) strlen(tty_ACK)) == 1) {
	/* single character acknowledge string */
	ACK_terminator = tty_ACK[0];
	ACK_length = 4096;
	return;
    }
#ifdef user8
    if (user8) {
	int ulen = (int) strlen(user8);
	int tc = tty_ACK[len - 1];
	if (tc == user8[ulen - 1]) {
	    /* ANSI style acknowledge string */
	    ACK_terminator = tc;
	    ACK_length = 4096;
	    return;
	}
    }
#endif
    /* fixed length acknowledge string */
    ACK_length = len;
    ACK_terminator = -2;
}

/*
**	probe_response()
**
**	query the terminal for its response
*/
static int
probe_response(const char *command, const char *respond)
{
    NCURSES_CONST char *command_cap = safe_tgets(command);
    NCURSES_CONST char *respond_cap = safe_tgets(respond);
    int len;
    int code = 0;

    if (command_cap == NULL || respond_cap == NULL) {
	sprintf(temp, "Both %s and %s capabilities must be given.",
		command, respond);
	ptextln(temp);
    } else {
	sprintf(temp, "Testing %s/%s, standby...", command, respond);
	put_str(temp);
	fflush(stdout);
	/* TODO: can_test("u8 u9", FLAG_TESTED); */

	tc_putp(command_cap);
	event_start(TIME_SYNC);	/* start the timer */
	read_key(tty_ACK, (sizeof(tty_ACK) - 1));

	if (event_time(TIME_SYNC) > 400000 || tty_ACK[0] == '\0') {
	    tty_can_sync = SYNC_FAILED;
	    sprintf(temp, "\nResponse sequence from (%s): ", command);
	    ptext(temp);
	    putln(expand(command_cap));
	    ptext("ACK received: ");
	    putln(expand(tty_ACK));
	    sprintf(temp, "Length of %s %d.", respond, (int) strlen(tty_ACK));
	    ptextln(temp);
	} else {
	    tty_can_sync = SYNC_TESTED;
	    if ((len = (int) strlen(tty_ACK)) == 1) {
		/* single character acknowledge string */
		ACK_terminator = tty_ACK[0];
		ACK_length = 4096;
	    } else {
		int ulen = (int) strlen(respond_cap);
		int tc = tty_ACK[len - 1];
		if (tc == respond_cap[ulen - 1]) {
		    /* ANSI style acknowledge string */
		    ACK_terminator = tc;
		    ACK_length = 4096;
		} else {
		    /* fixed length acknowledge string */
		    ACK_length = len;
		    ACK_terminator = -2;
		}
	    }
	    code = 1;
	}
    }
    put_crlf();
    return code;
}

/*
**	verify_response()
*/
static void
verify_response(NCURSES_CONST char *command, NCURSES_CONST char *respond)
{
    if (tty_can_sync == SYNC_FAILED) {
	return;
    }
    if (probe_response(command, respond)) {
	put_crlf();
	if (ACK_terminator >= 0) {
	    if (!compare_regex(respond, safe_tgets(respond), tty_ACK)) {
		sprintf(temp, "Mismatched response (%s): ", respond);
		ptext(temp);
		strcpy(temp, tty_ACK);
		putln(expand(temp));
	    } else {
		ptext("Valid response: ");
		strcpy(temp, tty_ACK);
		ptextln(expand(temp));
	    }
	} else {
	    sprintf(temp, "Fixed length response, %d characters", ACK_length);
	    ptextln(temp);
	}
    } else {
	put_crlf();
    }
}

/*
**	verify_time()
**
**	verify that the time tests are ready to run.
**	If the baud rate is not set then compute it.
*/
void
verify_time(void)
{
    int status, ch;

    if (tty_can_sync == SYNC_FAILED) {
	return;
    }
    probe_enq_ok();
    put_crlf();
    if (tty_can_sync == SYNC_TESTED) {
	put_crlf();
	if (ACK_terminator >= 0) {
	    ptext("ACK terminating character: ");
	    temp[0] = (char) ACK_terminator;
	    temp[1] = '\0';
	    ptextln(expand(temp));
#ifdef user8
	    if (!compare_regex("u8", user8, tty_ACK)) {
		strcpy(temp, tty_ACK);
		ptext("Mismatched response (u8): ");
		putln(expand(temp));
	    }
#endif
	} else {
	    sprintf(temp, "Fixed length ACK, %d characters", ACK_length);
	    ptextln(temp);
	}
    }
    if (tty_baud_rate == 0) {
	sync_home(&sync_test_list[0], &status, &ch);
    }
}

/*****************************************************************************
 *
 * Terminal performance tests
 *
 *	Find out how fast the terminal can:
 *	  1) accept characters
 *	  2) scroll the screen
 *	  3) clear the screen
 *
 *****************************************************************************/

/*
**	sync_home(test_list, status, ch)
**
**	Baudrate test
*/
static void
sync_home(
	     TestList * t,
	     int *state,
	     int *ch)
{
    int j, k;
    unsigned long rate;

    if (!cursor_home && !cursor_address && !row_address) {
	ptext("Terminal can not home cursor.  ");
	generic_done_message(t, state, ch);
	return;
    }
    if (skip_pad_test(t, state, ch,
		      "(home) Start baudrate search")) {
	return;
    }
    pad_test_startup(1);
    do {
	go_home();
	for (j = 1; j < lines; j++) {
	    for (k = 0; k < columns; k++) {
		if (k & 0xF) {
		    put_this(letter);
		} else {
		    put_this('.');
		}
	    }
	    SLOW_TERMINAL_EXIT;
	}
	NEXT_LETTER;
    } while (still_testing());
    pad_test_shutdown(t, auto_right_margin == 0);
    /* note:  tty_frame_size is the real framesize times two.
       This takes care of half bits. */
    rate = ((tx_cps * (unsigned long) tty_frame_size) >> 1);
    if (rate > tty_baud_rate) {
	tty_baud_rate = (unsigned) rate;
    }
    if (tx_cps > tty_cps) {
	tty_cps = tx_cps;
    }
    sprintf(temp, "%lu characters per second.  Baudrate %d  ", tx_cps, j);
    ptext(temp);
    generic_done_message(t, state, ch);
}

/*
**	sync_lines(test_list, status, ch)
**
**	How many newlines/second?
*/
static void
sync_lines(
	      TestList * t,
	      int *state,
	      int *ch)
{
    int j;

    if (skip_pad_test(t, state, ch,
		      "(nel) Start scroll performance test")) {
	return;
    }
    pad_test_startup(0);
    repeats = 100;
    do {
	sprintf(temp, "%d", test_complete);
	put_str(temp);
	put_newlines(repeats);
    } while (still_testing());
    pad_test_shutdown(t, 0);
    j = sliding_scale(tx_count[0], 1000000, usec_run_time);
    if (j > tty_newline_rate) {
	tty_newline_rate = j;
    }
    sprintf(temp, "%d linefeeds per second.  ", j);
    ptext(temp);
    generic_done_message(t, state, ch);
}

/*
**	sync_clear(test_list, status, ch)
**
**	How many clear-screens/second?
*/
static void
sync_clear(
	      TestList * t,
	      int *state,
	      int *ch)
{
    int j;

    if (!clear_screen) {
	ptext("Terminal can not clear-screen.  ");
	generic_done_message(t, state, ch);
	return;
    }
    if (skip_pad_test(t, state, ch,
		      "(clear) Start clear-screen performance test")) {
	return;
    }
    pad_test_startup(0);
    repeats = 20;
    do {
	sprintf(temp, "%d", test_complete);
	put_str(temp);
	for (j = 0; j < repeats; j++) {
	    put_clear();
	}
    } while (still_testing());
    pad_test_shutdown(t, 0);
    j = sliding_scale(tx_count[0], 1000000, usec_run_time);
    if (j > tty_clear_rate) {
	tty_clear_rate = j;
    }
    sprintf(temp, "%d clear-screens per second.  ", j);
    ptext(temp);
    generic_done_message(t, state, ch);
}

/*
**	sync_summary(test_list, status, ch)
**
**	Print out the test results.
*/
static void
sync_summary(
		TestList * t,
		int *state,
		int *ch)
{
    char size[32];

    put_crlf();
    ptextln("Terminal  size    characters/sec linefeeds/sec  clears/sec");
    sprintf(size, "%dx%d", columns, lines);
    sprintf(temp, "%-10s%-11s%11lu   %11d %11d", tty_basename, size,
	    tty_cps, tty_newline_rate, tty_clear_rate);
    ptextln(temp);
    generic_done_message(t, state, ch);
}

/*
**	sync_test(menu)
**
**	Run at the beginning of the pad tests and function key tests
*/
void
sync_test(
	     TestMenu * menu)
{
    control_init();
    if (tty_can_sync == SYNC_NOT_TESTED) {
	verify_time();
    }
    if (menu->menu_title) {
	put_crlf();
	ptextln(menu->menu_title);
    }
}

/*****************************************************************************
 *
 * Terminal status tests
 *
 *****************************************************************************/

/*
**	sync_handshake(test_list, status, ch)
**
**	Test or retest the ENQ/ACK handshake
*/
void
sync_handshake(
		  TestList * t GCC_UNUSED,
		  int *state GCC_UNUSED,
		  int *ch GCC_UNUSED)
{
    tty_can_sync = SYNC_NOT_TESTED;
    verify_time();
}

/*
**	ask_DA2(test_list, status, ch)
**
**	Test the secondary device attributes.
*/
void
ask_DA2(
	   TestList * t GCC_UNUSED,
	   int *state GCC_UNUSED,
	   int *ch GCC_UNUSED)
{
    verify_response("RV", "rv");
}

/*
**	ask_version(test_list, status, ch)
**
**	Test the device version.
*/
void
ask_version(
	       TestList * t GCC_UNUSED,
	       int *state GCC_UNUSED,
	       int *ch GCC_UNUSED)
{
    verify_response("XR", "xr");
}
