/**
 * @file runtime_console.c
 * @brief The group E verbs: SLEEP, WAIT, KEY, WINDOW, and the TI clock.
 *
 * Everything here that waits does so by *holding the step loop*, the way GETKEY
 * already does (see akbasic_input_service). Section 1.6 forbids the library
 * blocking: a host calls akbasic_runtime_step() once a frame and it must always
 * come back, so "wait" means "do not advance this step" rather than "do not
 * return". A bounded akbasic_runtime_run() still returns on time, and a host
 * that wants to abandon the wait can change the mode.
 */

#include <inttypes.h>
#include <stdio.h>
#include <string.h>

#include <akerror.h>
#include <akstdlib.h>

#include <akbasic/args.h>
#include <akbasic/error.h>
#include <akbasic/runtime.h>

#include "verbs.h"

/* Most verbs answer "did something happen"; this is that answer. */
#define SUCCEED_TRUE(__obj, __dest)			\
    do {						\
	*(__dest) = &(__obj)->staticTrueValue;		\
    } while ( 0 )

/** @brief Jiffies per second. A Commodore counts time in sixtieths. */
#define JIFFIES_PER_SECOND	60

/* ------------------------------------------------------------------ SLEEP -- */

akerr_ErrorContext *akbasic_cmd_sleep(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
    PREPARE_ERROR(errctx);
    double args[1];
    int count = 0;

    (void)lval; (void)rval;
    FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
		     "NULL argument in SLEEP");
    PASS(errctx, akbasic_args_numbers(obj, expr, "SLEEP", args, 1, &count));
    FAIL_ZERO_RETURN(errctx, (count == 1), AKBASIC_ERR_SYNTAX, "Expected SLEEP (seconds)");
    FAIL_ZERO_RETURN(errctx, (args[0] >= 0.0), AKBASIC_ERR_VALUE,
		     "SLEEP cannot wait a negative number of seconds");

    /*
     * Records when to stop and returns. akbasic_console_service() holds the step
     * loop until then -- so a program that sleeps still lets its host draw
     * frames, and a sleeping program in an embedded game does not freeze the
     * game.
     *
     * A host that never calls akbasic_runtime_settime() leaves the clock at
     * zero, and a deadline computed from a clock that never advances is never
     * reached. So no clock means no sleep: the verb does nothing rather than
     * hanging the program, which is the same trade the audio durations make
     * (§5 deviation 20) and the same direction -- fail fast, not silently
     * forever.
     */
    if ( obj->timems <= 0 ) {
	SUCCEED_TRUE(obj, dest);
	SUCCEED_RETURN(errctx);
    }
    obj->console_state.sleepuntilms = obj->timems + (int64_t)(args[0] * 1000.0);
    obj->console_state.sleeping = true;
    SUCCEED_TRUE(obj, dest);
    SUCCEED_RETURN(errctx);
}

/* ------------------------------------------------------------------- WAIT -- */

akerr_ErrorContext *akbasic_cmd_wait(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
    PREPARE_ERROR(errctx);
    double args[3];
    int count = 0;

    (void)lval; (void)rval;
    FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
		     "NULL argument in WAIT");
    PASS(errctx, akbasic_args_numbers(obj, expr, "WAIT", args, 3, &count));
    FAIL_ZERO_RETURN(errctx, (count >= 2), AKBASIC_ERR_SYNTAX,
		     "Expected WAIT (address), (mask) [, (xor)]");

    /*
     * WAIT polls a byte until `(PEEK(addr) XOR xor) AND mask` is non-zero. On a
     * C128 that byte is a hardware register an interrupt is changing; here it is
     * ordinary process memory, and the only thing that can change it is the host
     * -- or another thread, or a POKE from a TRAP handler.
     *
     * So this is honest but rarely useful: a program that waits on memory
     * nothing writes waits forever, which is exactly what the same program does
     * on a C128 with the wrong address. It holds the step loop rather than
     * blocking, so the host keeps its frame rate and can stop the program.
     */
    obj->console_state.waitaddress = (uintptr_t)args[0];
    obj->console_state.waitmask = (uint8_t)args[1];
    obj->console_state.waitxor = (count >= 3 ? (uint8_t)args[2] : 0);
    obj->console_state.waiting = true;
    SUCCEED_TRUE(obj, dest);
    SUCCEED_RETURN(errctx);
}

/* -------------------------------------------------------------------- KEY -- */

akerr_ErrorContext *akbasic_cmd_key(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
    PREPARE_ERROR(errctx);
    akbasic_ASTLeaf *arg = NULL;
    akbasic_Value *value = NULL;
    /* Room for the longest macro plus `KEY n, ""` around it. */
    char line[AKBASIC_MAX_STRING_LENGTH + 32];
    int64_t number = 0;
    int written = 0;
    int i = 0;

    (void)lval; (void)rval;
    FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
		     "NULL argument in KEY");

    arg = akbasic_leaf_first_argument(expr);
    if ( arg == NULL ) {
	/* Bare KEY lists the definitions, which is what a C128 does. */
	for ( i = 0; i < AKBASIC_MAX_FUNCTION_KEYS; i++ ) {
	    PASS(errctx, aksl_snprintf(&written, line, sizeof(line), "KEY %d, \"%s\"",
				       i + 1, obj->console_state.keys[i]));
	    PASS(errctx, akbasic_runtime_println(obj, line));
	}
	SUCCEED_TRUE(obj, dest);
	SUCCEED_RETURN(errctx);
    }

    PASS(errctx, akbasic_runtime_evaluate(obj, arg, &value));
    FAIL_NONZERO_RETURN(errctx, (value->valuetype == AKBASIC_TYPE_STRING), AKBASIC_ERR_TYPE,
			"KEY expected a key number");
    number = value->intval;
    FAIL_ZERO_RETURN(errctx, (number >= 1 && number <= AKBASIC_MAX_FUNCTION_KEYS),
		     AKBASIC_ERR_BOUNDS, "KEY %" PRId64 " is outside 1..%d",
		     number, AKBASIC_MAX_FUNCTION_KEYS);

    arg = arg->next;
    FAIL_ZERO_RETURN(errctx, (arg != NULL), AKBASIC_ERR_SYNTAX,
		     "Expected KEY (number), (string)");
    PASS(errctx, akbasic_runtime_evaluate(obj, arg, &value));
    FAIL_NONZERO_RETURN(errctx, (value->valuetype != AKBASIC_TYPE_STRING), AKBASIC_ERR_TYPE,
			"KEY expected a string");
    PASS(errctx, aksl_snprintf(&written, obj->console_state.keys[number - 1],
			       sizeof(obj->console_state.keys[0]), "%s", value->stringval));
    SUCCEED_TRUE(obj, dest);
    SUCCEED_RETURN(errctx);
}

/* ----------------------------------------------------------------- WINDOW -- */

akerr_ErrorContext *akbasic_cmd_window(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
    PREPARE_ERROR(errctx);
    double args[5];
    int count = 0;

    (void)lval; (void)rval;
    FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
		     "NULL argument in WINDOW");
    PASS(errctx, akbasic_args_numbers(obj, expr, "WINDOW", args, 5, &count));
    FAIL_ZERO_RETURN(errctx, (count >= 4), AKBASIC_ERR_SYNTAX,
		     "Expected WINDOW (left), (top), (right), (bottom) [, (clear)]");
    /*
     * The geometry first, and deliberately: an inside-out rectangle is a mistake
     * in the program whether or not a device is attached, and reporting "no
     * device" for it would send the author looking in the wrong place.
     */
    FAIL_ZERO_RETURN(errctx, (args[0] <= args[2] && args[1] <= args[3]), AKBASIC_ERR_VALUE,
		     "WINDOW's bottom right must not be above or left of its top left");
    FAIL_ZERO_RETURN(errctx, (obj->sink != NULL), AKBASIC_ERR_DEVICE,
		     "WINDOW needs a text device and this runtime has none");
    FAIL_ZERO_RETURN(errctx, (obj->sink->window != NULL), AKBASIC_ERR_DEVICE,
		     "WINDOW needs a text device with a character grid, and this one has none");

    PASS(errctx, obj->sink->window(obj->sink, (int)args[0], (int)args[1],
				   (int)args[2], (int)args[3]));
    if ( count >= 5 && args[4] != 0.0 ) {
	PASS(errctx, obj->sink->clear(obj->sink));
    }
    SUCCEED_TRUE(obj, dest);
    SUCCEED_RETURN(errctx);
}

/* --------------------------------------------------------------- RWINDOW -- */

/**
 * @brief How big the current text window is, in characters.
 *
 * BASIC 7.0's own answer to the question, and it had never been implemented
 * here: `RWINDOW(0)` is the number of lines and `RWINDOW(1)` the number of
 * columns. Without it a program had no way to find out, so anything placing a
 * character *and* a sprite at the same spot had to hardcode a cell size measured
 * against whatever font the host loaded. The Breakout in `examples/` did exactly
 * that, and it was the one thing in that listing that broke on a different
 * window. TODO.md section 6 item 31.
 *
 * **`RWINDOW(2)` is refused by name.** On a C128 it answers 0 for 40-column mode
 * and 1 for 80-column, and this interpreter has no such mode -- the grid is
 * whatever the window divided by the font gives. Answering 0 would be a
 * plausible lie, which is worse than a refusal that says why.
 *
 * The cell size in pixels is `RGR(3)` and `RGR(4)` rather than a field here: it
 * is a question about the drawing surface, and it belongs beside the surface's
 * own dimensions.
 */
akerr_ErrorContext *akbasic_fn_rwindow(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
    PREPARE_ERROR(errctx);
    akbasic_Value *out = NULL;
    double args[1];
    int count = 0;
    int field = 0;
    int columns = 0;
    int rows = 0;
    int cellw = 0;
    int cellh = 0;

    (void)lval; (void)rval;
    FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
		     "NULL argument in RWINDOW");
    PASS(errctx, akbasic_args_numbers(obj, expr, "RWINDOW", args, 1, &count));
    FAIL_ZERO_RETURN(errctx, (count >= 1), AKBASIC_ERR_SYNTAX, "RWINDOW expected a field number");
    field = (int)args[0];

    FAIL_NONZERO_RETURN(errctx, (field == 2), AKBASIC_ERR_DEVICE,
			"RWINDOW(2) reports a C128's 40 or 80 column mode, and this interpreter has neither");
    FAIL_ZERO_RETURN(errctx, (field == 0 || field == 1), AKBASIC_ERR_BOUNDS,
		     "RWINDOW: field %d is outside 0..1", field);
    FAIL_ZERO_RETURN(errctx, (obj->sink != NULL), AKBASIC_ERR_DEVICE,
		     "RWINDOW needs a text device and this runtime has none");
    FAIL_ZERO_RETURN(errctx, (obj->sink->grid != NULL), AKBASIC_ERR_DEVICE,
		     "RWINDOW needs a text device with a character grid, and this one has none");
    PASS(errctx, obj->sink->grid(obj->sink, &columns, &rows, &cellw, &cellh));

    PASS(errctx, akbasic_environment_new_value(obj->environment, &out));
    PASS(errctx, akbasic_value_zero(out));
    out->valuetype = AKBASIC_TYPE_INTEGER;
    out->intval = (field == 0 ? (int64_t)rows : (int64_t)columns);
    *dest = out;
    SUCCEED_RETURN(errctx);
}

/* ---------------------------------------------------------------- service -- */

akerr_ErrorContext *akbasic_console_state_init(akbasic_ConsoleState *obj)
{
    PREPARE_ERROR(errctx);

    FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL console state in init");
    PASS(errctx, aksl_memset(obj, 0, sizeof(*obj)));
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akbasic_console_service(akbasic_Runtime *obj, bool *blocked)
{
    PREPARE_ERROR(errctx);

    FAIL_ZERO_RETURN(errctx, (obj != NULL && blocked != NULL), AKERR_NULLPOINTER,
		     "NULL argument in console_service");
    *blocked = false;

    if ( obj->console_state.sleeping ) {
	if ( obj->timems >= obj->console_state.sleepuntilms ) {
	    obj->console_state.sleeping = false;
	} else {
	    *blocked = true;
	    SUCCEED_RETURN(errctx);
	}
    }

    if ( obj->console_state.waiting ) {
	const volatile uint8_t *address = (const volatile uint8_t *)obj->console_state.waitaddress;
	uint8_t byte = 0;

	/*
	 * `volatile`, because the whole point is that something outside this
	 * program changes it. Without it the compiler is entitled to read the
	 * byte once and spin on the register.
	 */
	byte = *address;
	if ( ((byte ^ obj->console_state.waitxor) & obj->console_state.waitmask) != 0 ) {
	    obj->console_state.waiting = false;
	} else {
	    *blocked = true;
	    SUCCEED_RETURN(errctx);
	}
    }
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akbasic_console_update_clock(akbasic_Runtime *obj)
{
    PREPARE_ERROR(errctx);
    akbasic_Variable *variable = NULL;
    int64_t zerosubscript[1] = { 0 };
    int64_t jiffies = 0;
    int64_t seconds = 0;
    char text[16];
    int written = 0;

    FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in update_clock");
    if ( obj->environment == NULL ) {
	SUCCEED_RETURN(errctx);
    }

    /*
     * `TI` and `TI$` are `TI#` and `TI$` here, and they are written rather than
     * computed on read: this dialect has no bare variable names and no
     * pseudo-variable mechanism, so they are ordinary globals refreshed once per
     * step. Same decision as `ER#` and `EL#`, for the same reason.
     *
     * Counted in jiffies -- sixtieths of a second -- from the host's clock,
     * which is what `TI` means on a Commodore. A host that never calls
     * akbasic_runtime_settime() leaves both at zero, which is a stopped clock
     * rather than a wrong one.
     */
    jiffies = (obj->timems * JIFFIES_PER_SECOND) / 1000;
    PASS(errctx, akbasic_runtime_global(obj, "TI#", &variable));
    PASS(errctx, akbasic_variable_set_integer(variable, jiffies, zerosubscript, 1));

    seconds = obj->timems / 1000;
    PASS(errctx, aksl_snprintf(&written, text, sizeof(text),
			       "%02" PRId64 "%02" PRId64 "%02" PRId64,
			       (seconds / 3600) % 24, (seconds / 60) % 60, seconds % 60));
    PASS(errctx, akbasic_runtime_global(obj, "TI$", &variable));
    PASS(errctx, akbasic_variable_set_string(variable, text, zerosubscript, 1));
    SUCCEED_RETURN(errctx);
}
