/*
** Copyright 2017-2020,2025 Thomas E. Dickey
** Copyright 1997-2015,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>

#if HAVE_SYS_TIME_H
#include <sys/time.h>
#endif

MODULE_ID("$Id: control.c,v 1.22 2025/04/27 17:15:43 tom Exp $")

/* terminfo test program control subroutines */

#if HAVE_GETTIMEOFDAY
#define MY_TIMER struct timeval
#else
#define MY_TIMER time_t
#endif

/* globals */
int test_complete;		/* counts number of tests completed */

char txt_longer_test_time[80];	/* +) use longer time */
char txt_shorter_test_time[80];	/* -) use shorter time */
static int pad_test_duration = 1;	/* number of seconds for a pad test */
int auto_pad_mode;		/* run the time tests */
SIG_ATOMIC_T no_alarm_event;	/* TRUE if the alarm has not gone off yet */
unsigned long usec_run_time;	/* length of last test in microseconds */
static MY_TIMER stop_watch[MAX_TIMERS];		/* Hold the start timers */

char txt_longer_augment[80];	/* >) use bigger augment */
char txt_shorter_augment[80];	/* <) use smaller augment */

/* caps under test data base */
int tt_delay_max;		/* max number of milliseconds we can delay */
int tt_delay_used;		/* number of milliseconds consumed in delay */
const char *tt_cap[TT_MAX];	/* value of string */
int tt_affected[TT_MAX];	/* lines or columns effected (repetition factor) */
int tt_count[TT_MAX];		/* Number of times sent */
int tt_delay[TT_MAX];		/* Number of milliseconds delay */
int ttp;			/* number of entries used */

/* Saved value of the above data base */
const char *tx_cap[TT_MAX];	/* value of string */
int tx_affected[TT_MAX];	/* lines or columns effected (repetition factor) */
int tx_count[TT_MAX];		/* Number of times sent */
int tx_index[TT_MAX];		/* String index */
int tx_delay[TT_MAX];		/* Number of milliseconds delay */
int txp;			/* number of entries used */
int tx_characters;		/* printing characters sent by test */
unsigned long tx_cps;		/* characters per second */
static TestList *tx_source;	/* The test that generated this data */

#define RESULT_BLOCK		1024
static int blocks;		/* number of result blocks available */
static TestResults *results;	/* pointer to next available */
static TestResults **pads;	/* save pad results here */

static void
alloc_arrays(void)
{
    if (pads == NULL) {
	pads = (TestResults **) calloc((size_t) MAX_STRINGS,
				       sizeof(TestResults *));
    }
}

/*
**	event_start(number)
**
**	Begin the stopwatch at the current time-of-day.
*/
void
event_start(int n)
{
#if HAVE_GETTIMEOFDAY
    (void) gettimeofday(&stop_watch[n], (struct timezone *) 0);
#else
    stop_watch[n] = time((time_t *) 0);
#endif
}

/*
**	event_time(number)
**
**	Return the number of milliseconds since this stop watch began.
*/
long
event_time(int n)
{
#if HAVE_GETTIMEOFDAY
    MY_TIMER current_time;

    (void) gettimeofday(&current_time, (struct timezone *) 0);
    return ((current_time.tv_sec - stop_watch[n].tv_sec) * 1000000)
	+ current_time.tv_usec - stop_watch[n].tv_usec;
#else
    return (time((time_t *) 0) - stop_watch[n]) * 1000;
#endif
}

/*****************************************************************************
 *
 * Execution control for string capability tests
 *
 *****************************************************************************/

/*
**	get_next_block()
**
**	Get a results block for pad test data.
*/
static TestResults *
get_next_block(void)
{
    if (blocks <= 0) {
	results = (TestResults *)
	    malloc(sizeof(TestResults) * RESULT_BLOCK);
	if (!results) {
	    ptextln("Malloc failed");
	    return (TestResults *) 0;
	}
	blocks = RESULT_BLOCK;
    }
    blocks--;
    return results++;
}

/*
**	set_augment_txt()
**
**	Initialize the augment menu selections
*/
void
set_augment_txt(void)
{
    sprintf(txt_longer_augment,
	    ">) Change lines/characters effected to %d", augment << 1);
    sprintf(txt_shorter_augment,
	    "<) Change lines/characters effected to %d", augment >> 1);
}

void
control_init(void)
{
    sprintf(txt_longer_test_time, "+) Change test time to %d seconds",
	    pad_test_duration + 1);
    sprintf(txt_shorter_test_time, "-) Change test time to %d seconds",
	    pad_test_duration - 1);
    set_augment_txt();
}

/*
**	msec_cost(cap, affected-count)
**
**	Return the number of milliseconds delay needed by the cap.
*/
int
msec_cost(
	     const char *const cap,
	     int affcnt)
{
    int dec, value, total, star, ch;
    const char *cp;

    if (!cap) {
	return 0;
    }
    total = 0;
    for (cp = cap; *cp; cp++) {
	if (*cp == '$' && cp[1] == '<') {
	    star = 1;
	    value = dec = 0;
	    for (cp += 2; (ch = *cp); cp++) {
		if (ch >= '0' && ch <= '9') {
		    value = value * 10 + (ch - '0');
		    dec *= 10;
		} else if (ch == '.') {
		    dec = 1;
		} else if (ch == '*') {
		    star = affcnt;
		} else if (ch == '>') {
		    break;
		}
	    }
	    if (dec > 1) {
		total += (value * star) / dec;
	    } else {
		total += (value * star);
	    }
	}
    }
    return total;
}

/*
**	liberated(cap)
**
**	Return the cap without padding
*/
char *
liberated(const char *cap)
{
    static char cb[1024];
    char *ts, *ls;

    cb[0] = '\0';
    ls = NULL;
    if (cap) {
	for (ts = cb; (*ts = *cap); ++cap) {
	    if (*cap == '$' && cap[1] == '<') {
		ls = ts;
	    }
	    ++ts;
	    if (*cap == '>') {
		if (ls) {
		    ts = ls;
		    ls = NULL;
		}
	    }
	}
    }
    return cb;
}

/*
**	page_loop()
**
**	send CR/LF or go home and bump letter
*/
void
page_loop(void)
{
    if (line_count + 2 >= lines) {
	NEXT_LETTER;
	go_home();
    } else {
	put_crlf();
    }
}

/*
**	skip_pad_test(test-list-entry, state, ch, text)
**
**	Print the start test line.  Handle start up commands.
**	Return TRUE if a return is requested.
*/
int
skip_pad_test(
		 TestList * test,
		 int *state,
		 int *ch,
		 const char *text)
{
    char rep_text[16];

    while (1) {
	if (text) {
	    ptext(text);
	}
	if ((test->flags & MENU_LC_MASK)) {
	    sprintf(rep_text, " *%d", augment);
	    ptext(rep_text);
	}
	ptext(" [n] > ");
	*ch = wait_here();
	if (*ch == 's') {
	    /* Skip is converted to next */
	    *ch = 'n';
	    return TRUE;
	}
	if (*ch == 'q') {
	    /* Quit is converted to help */
	    *ch = '?';
	    return TRUE;
	}
	if (*ch == '\r' || *ch == '\n' || *ch == 'n' || *ch == 'r') {
	    /* this is the only response that allows the test to run */
	    *ch = 0;
	}
	if (subtest_menu(pad_test_list, state, ch)) {
	    continue;
	}
	return (*ch != 0);
    }
}

/*
**	pad_done_message(test_list)
**
**	Print the Done message and request input.
*/
void
pad_done_message(
		    TestList * test,
		    int *state,
		    int *ch)
{
    int default_action = 0;
    char done_message[128];
    char rep_text[16];

    while (1) {
	if ((test->flags & MENU_LC_MASK)) {
	    sprintf(rep_text, "*%d", augment);
	} else {
	    rep_text[0] = '\0';
	}
	if (test->caps_done) {
	    sprintf(done_message, "(%s)%s Done ", test->caps_done,
		    rep_text);
	    ptext(done_message);
	} else {
	    if (rep_text[0]) {
		ptext(rep_text);
		ptext(" ");
	    }
	    ptext("Done ");
	}
	if (debug_level & 2) {
	    dump_test_stats(test, state, ch);
	} else {
	    *ch = wait_here();
	}
	if (*ch == '\r' || *ch == '\n') {
	    *ch = default_action;
	    return;
	}
	if (*ch == 's' || *ch == 'n') {
	    *ch = 0;
	    return;
	}
	if ((strchr) (pad_repeat_test, *ch)) {
	    /* default action is now repeat */
	    default_action = 'r';
	}
	if (subtest_menu(pad_test_list, state, ch)) {
	    continue;
	}
	return;
    }
}

/*
**	sliding_scale(dividend, factor, divisor)
**
**	Return (dividend * factor) / divisor
*/
int
sliding_scale(
		 int dividend,
		 int factor,
		 unsigned long divisor)
{
    double d = dividend;

    if (divisor) {
	d = (d * (double) factor) / (double) divisor;
	return (int) (d + 0.5);
    }
    return 0;
}

/*
**	pad_test_startup()
**
**	Do the stuff needed to begin a test.
*/
void
pad_test_startup(
		    int do_clear)
{
    if (do_clear) {
	put_clear();
    }
    repeats = augment;
    raw_characters_sent = 0;
    test_complete = ttp = char_count = tt_delay_used = 0;
    letter = letters[letter_number = 0];
    if (pad_test_duration <= 0) {
	pad_test_duration = 1;
    }
    tt_delay_max = pad_test_duration * 1000;
    set_alarm_clock(pad_test_duration);
    event_start(TIME_TEST);
}

/*
**	still_testing()
**
**	This function is called to see if the test loop should be terminated.
*/
int
still_testing(void)
{
    fflush(stdout);
    test_complete++;
    return EXIT_CONDITION;
}

/*
**	pad_test_shutdown()
**
**	Do the stuff needed to end a test.
*/
void
pad_test_shutdown(
		     TestList * t,
		     int crlf)
{
    int i;
    int counts;			/* total counts */
    int ss;			/* Save string index */
    int cpo;			/* characters per operation */
    long delta;			/* difference in characters */
    int bogus;			/* Time is inaccurate */
    TestResults *r;		/* Results of current test */
    int ss_index[TT_MAX];	/* String index */

    alloc_arrays();
    if (tty_can_sync == SYNC_TESTED) {
	bogus = tty_sync_error();
    } else {
	bogus = 1;
    }
    usec_run_time = (unsigned long) event_time(TIME_TEST);
    tx_source = t;
    tx_characters = raw_characters_sent;
    tx_cps = (unsigned long) sliding_scale(tx_characters, 1000000, usec_run_time);

    /* save the data base */
    for (txp = ss = counts = 0; txp < ttp; txp++) {
	tx_cap[txp] = tt_cap[txp];
	tx_count[txp] = tt_count[txp];
	tx_delay[txp] = tt_delay[txp];
	tx_affected[txp] = tt_affected[txp];
	tx_index[txp] = get_string_cap_byvalue(tt_cap[txp]);
	if (tx_index[txp] >= 0) {
	    if (cap_match(t->caps_done, strnames[tx_index[txp]])) {
		ss_index[ss++] = txp;
		counts += tx_count[txp];
	    }
	}
    }

    if (crlf) {
	put_crlf();
    }
    if (counts == 0 || tty_cps == 0 || bogus) {
	/* nothing to do */
	return;
    }
    /* calculate the suggested pad times */
    delta = (long) (usec_run_time
		    - (unsigned long) sliding_scale(tx_characters,
						    1000000,
						    tty_cps));
    if (delta < 0) {
	/* probably should bump tx_characters */
	delta = 0;
    }
    cpo = (int) (delta / counts);
    for (i = 0; i < ss; i++) {
	if (!(r = get_next_block())) {
	    return;
	}
	r->next = pads[tx_index[ss_index[i]]];
	pads[tx_index[ss_index[i]]] = r;
	r->test = t;
	r->reps = tx_affected[ss_index[i]];
	r->delay = cpo;
    }
}

/*
**	show_cap_results(index)
**
**	Display the previous results
*/
static void
show_cap_results(
		    int x)
{
    TestResults *r;		/* a result */

    alloc_arrays();
    if ((r = pads[x])) {
	sprintf(temp, "(%s)", strnames[x]);
	ptext(temp);
	while (r) {
	    sprintf(temp, "$<%d>", r->delay / 1000);
	    put_columns(temp, (int) strlen(temp), 10);
	    r = r->next;
	}
	r = pads[x];
	while (r) {
	    if (r->reps > 1) {
		int delay = r->delay / (r->reps * 100);
		sprintf(temp, "$<%d.%d*>", delay / 10, delay % 10);
		put_columns(temp, (int) strlen(temp), 10);
	    }
	    r = r->next;
	}
	put_crlf();
    }
}

/*
**	dump_test_stats(test_list, status, ch)
**
**	Dump the statistics about the last test
*/
void
dump_test_stats(
		   TestList * t,
		   int *state,
		   int *ch)
{
    int i;
    char tbuf[32];

    put_crlf();
    if (tx_source && tx_source->caps_done) {
	int x[32];

	cap_index(tx_source->caps_done, x);
	if (x[0] >= 0) {
	    sprintf(temp, "Caps summary for (%s)",
		    tx_source->caps_done);
	    ptextln(temp);
	    for (i = 0; x[i] >= 0; i++) {
		show_cap_results(x[i]);
	    }
	    put_crlf();
	}
    }
    sprintf(tbuf, "%011lu", usec_run_time);
    sprintf(temp,
	    "Test time: %lu.%s, characters per second %lu, characters %d",
	    usec_run_time / 1000000UL, &tbuf[5], tx_cps, tx_characters);
    ptextln(temp);

    for (i = 0; i < txp; i++) {
	int j;
	if ((j = get_string_cap_byvalue(tx_cap[i])) >= 0) {
	    sprintf(tbuf, "(%s)", strnames[j]);
	} else {
	    strcpy(tbuf, "(?)");
	}
	sprintf(temp, "%8d  %3d  $<%3d>  %8s %s",
		tx_count[i], tx_affected[i], tx_delay[i],
		tbuf, expand(tx_cap[i]));
	putln(temp);
    }
    generic_done_message(t, state, ch);
}

/*
**	longer_test_time(test_list, status, ch)
**
**	Extend the number of seconds for each test.
*/
void
longer_test_time(
		    TestList * t GCC_UNUSED,
		    int *state GCC_UNUSED,
		    int *ch)
{
    pad_test_duration += 1;
    sprintf(txt_longer_test_time, "+) Change test time to %d seconds",
	    pad_test_duration + 1);
    sprintf(txt_shorter_test_time, "-) Change test time to %d seconds",
	    pad_test_duration - 1);
    sprintf(temp, "Tests will run for %d seconds", pad_test_duration);
    ptext(temp);
    *ch = REQUEST_PROMPT;
}

/*
**	shorter_test_time(test_list, status, ch)
**
**	Shorten the number of seconds for each test.
*/
void
shorter_test_time(
		     TestList * t GCC_UNUSED,
		     int *state GCC_UNUSED,
		     int *ch)
{
    if (pad_test_duration > 1) {
	pad_test_duration -= 1;
	sprintf(txt_longer_test_time, "+) Change test time to %d seconds",
		pad_test_duration + 1);
	sprintf(txt_shorter_test_time, "-) Change test time to %d seconds",
		pad_test_duration - 1);
    }
    sprintf(temp, "Tests will run for %d second%s", pad_test_duration,
	    pad_test_duration > 1 ? "s" : "");
    ptext(temp);
    *ch = REQUEST_PROMPT;
}

/*
**	longer_augment(test_list, status, ch)
**
**	Lengthen the number of lines/characters effected
*/
void
longer_augment(
		  TestList * t,
		  int *state GCC_UNUSED,
		  int *ch)
{
    augment <<= 1;
    set_augment_txt();
    if (augment_test) {
	t = augment_test;
    }
    sprintf(temp, "The pad tests will effect %d %s.", augment,
	    ((t->flags & MENU_LC_MASK) == MENU_lines) ?
	    "lines" : "characters");
    ptextln(temp);
    *ch = REQUEST_PROMPT;
}

/*
**	shorter_augment(test_list, status, ch)
**
**	Shorten the number of lines/characters effected
*/
void
shorter_augment(
		   TestList * t,
		   int *state GCC_UNUSED,
		   int *ch)
{
    if (augment > 1) {
	/* don't let the augment go to zero */
	augment >>= 1;
    }
    set_augment_txt();
    if (augment_test) {
	t = augment_test;
    }
    sprintf(temp, "The pad tests will effect %d %s.", augment,
	    ((t->flags & MENU_LC_MASK) == MENU_lines) ?
	    "lines" : "characters");
    ptextln(temp);
    *ch = REQUEST_PROMPT;
}
