/**
 * @file runtime_ui.c
 * @brief The group K verbs: MENU, GETMENU, RMENU, DIALOG, HUD and UISTYLE.
 *
 * Nothing here includes an SDL or a libakgl header. Every call goes through the
 * akbasic_UiBackend record the host attached, which is what lets the whole group
 * be tested in a build with no SDL on the machine.
 *
 * None of these is in the Go reference and none is in Commodore BASIC 7.0 --
 * a C128 had no widget toolkit to wrap. They exist because libakgl 0.9.0 grew
 * one, and their shapes come from the verbs already here rather than from
 * anywhere else: MENU retires the way SOLID does, GETMENU holds the step loop
 * the way GETKEY does, and RMENU reads-and-clears the way BUMP does.
 *
 * The backend is a set of *setters*, not a set of draw calls. libakgl's UI is
 * immediate mode -- widgets are re-declared inside a frame bracket every frame
 * and clay borrows the text until the bracket closes -- and a BASIC program is
 * not. See include/akbasic/ui.h for the whole of that reasoning.
 */

#include <inttypes.h>

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

#include <akbasic/args.h>
#include <akbasic/error.h>
#include <akbasic/runtime.h>
#include <akbasic/ui.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 Refuse politely when the host lent us no UI device.
 *
 * Every verb in this file opens with it, and it has to name the verb: "no UI
 * device" on its own tells a program author nothing about which line to look at.
 */
static akerr_ErrorContext *require_ui(akbasic_Runtime *obj, const char *verb)
{
    PREPARE_ERROR(errctx);

    FAIL_ZERO_RETURN(errctx, (obj != NULL && verb != NULL), AKERR_NULLPOINTER,
		     "NULL argument in require_ui");
    FAIL_ZERO_RETURN(errctx, (obj->ui != NULL), AKBASIC_ERR_DEVICE,
		     "%s needs a UI device and this runtime has none", verb);
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Turn a BASIC menu number into a zero-based slot, or refuse.
 *
 * Menus are numbered from one, the way sprites and colour sources are. Nothing
 * in this dialect is numbered from zero and a UI group is a poor place to start.
 */
static akerr_ErrorContext *menu_index(int64_t n, const char *verb, int *dest)
{
    PREPARE_ERROR(errctx);

    FAIL_ZERO_RETURN(errctx, (n >= 1 && n <= AKBASIC_UI_MAX_MENUS), AKBASIC_ERR_BOUNDS,
		     "%s: menu %" PRId64 " is outside 1..%d", verb, n, AKBASIC_UI_MAX_MENUS);
    *dest = (int)(n - 1);
    SUCCEED_RETURN(errctx);
}

/** @brief The same for a HUD slot. */
static akerr_ErrorContext *label_index(int64_t n, const char *verb, int *dest)
{
    PREPARE_ERROR(errctx);

    FAIL_ZERO_RETURN(errctx, (n >= 1 && n <= AKBASIC_UI_MAX_LABELS), AKBASIC_ERR_BOUNDS,
		     "%s: slot %" PRId64 " is outside 1..%d", verb, n, AKBASIC_UI_MAX_LABELS);
    *dest = (int)(n - 1);
    SUCCEED_RETURN(errctx);
}

/**
 * @brief The nth argument of a command or function, or NULL past the end.
 *
 * A plain walk rather than a wrapper around akbasic_args_numbers(), because
 * every verb in this file mixes numbers and strings in one list and that helper
 * refuses a string by design -- correctly, for the groups it was written for.
 */
static akbasic_ASTLeaf *nth_argument(akbasic_ASTLeaf *expr, int n)
{
    akbasic_ASTLeaf *arg = NULL;
    int i = 0;

    if ( expr == NULL ) {
	return NULL;
    }
    arg = akbasic_leaf_first_argument(expr);
    for ( i = 0; i < n && arg != NULL; i++ ) {
	arg = arg->next;
    }
    return arg;
}

/** @brief How many arguments the verb was given. */
static int argument_count(akbasic_ASTLeaf *expr)
{
    akbasic_ASTLeaf *arg = NULL;
    int count = 0;

    if ( expr == NULL ) {
	return 0;
    }
    for ( arg = akbasic_leaf_first_argument(expr); arg != NULL; arg = arg->next ) {
	count += 1;
    }
    return count;
}

/** @brief Evaluate argument @p n and insist it is a number. */
static akerr_ErrorContext *nth_number(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, const char *verb, int n, int64_t *dest)
{
    PREPARE_ERROR(errctx);
    akbasic_ASTLeaf *arg = nth_argument(expr, n);
    akbasic_Value *value = NULL;

    FAIL_ZERO_RETURN(errctx, (arg != NULL), AKBASIC_ERR_SYNTAX,
		     "%s is missing argument %d", verb, n + 1);
    PASS(errctx, akbasic_runtime_evaluate(obj, arg, &value));
    FAIL_NONZERO_RETURN(errctx, (value->valuetype == AKBASIC_TYPE_STRING), AKBASIC_ERR_TYPE,
			"%s expected a number in argument %d", verb, n + 1);
    *dest = (value->valuetype == AKBASIC_TYPE_FLOAT)
	? (int64_t)value->floatval : value->intval;
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Evaluate argument @p n, insist it is a string, and copy it out.
 *
 * Copied rather than pointed at. The value it came from lives in the per-line
 * pool, and a caller collecting sixteen of these has no promise that sixteen
 * separate slots were used -- so a table of pointers into the pool could quietly
 * be a table of sixteen pointers to the same string.
 */
static akerr_ErrorContext *nth_string(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, const char *verb, int n, char *dest, size_t len)
{
    PREPARE_ERROR(errctx);
    akbasic_ASTLeaf *arg = nth_argument(expr, n);
    akbasic_Value *value = NULL;

    FAIL_ZERO_RETURN(errctx, (arg != NULL), AKBASIC_ERR_SYNTAX,
		     "%s is missing argument %d", verb, n + 1);
    PASS(errctx, akbasic_runtime_evaluate(obj, arg, &value));
    FAIL_ZERO_RETURN(errctx, (value->valuetype == AKBASIC_TYPE_STRING), AKBASIC_ERR_TYPE,
		     "%s expected a string in argument %d", verb, n + 1);
    PASS(errctx, aksl_strcpy(dest, len, value->stringval));
    SUCCEED_RETURN(errctx);
}

/** @brief Hand back an integer from a function. */
static akerr_ErrorContext *integer_result(akbasic_Runtime *obj, int64_t value, akbasic_Value **dest)
{
    PREPARE_ERROR(errctx);
    akbasic_Value *out = NULL;

    PASS(errctx, akbasic_environment_new_value(obj->environment, &out));
    PASS(errctx, akbasic_value_zero(out));
    out->valuetype = AKBASIC_TYPE_INTEGER;
    out->intval = value;
    *dest = out;
    SUCCEED_RETURN(errctx);
}

/* ---------------------------------------------------------------- MENU --- */

akerr_ErrorContext *akbasic_cmd_menu(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
    PREPARE_ERROR(errctx);
    /*
     * The entries are copied here rather than pointed at, so that what reaches
     * the backend is sixteen distinct strings whatever the value pool did. Four
     * kilobytes of stack, once, in a verb that is not on any hot path.
     */
    char items[AKBASIC_UI_MAX_MENU_ITEMS][AKBASIC_MAX_STRING_LENGTH];
    const char *pointers[AKBASIC_UI_MAX_MENU_ITEMS];
    int count = 0;
    int entries = 0;
    int slot = 0;
    int i = 0;
    int64_t n = 0;

    (void)lval; (void)rval;
    PASS(errctx, require_ui(obj, "MENU"));
    count = argument_count(expr);

    /* No arguments retires them all, the way a bare SOLID does. */
    if ( count == 0 ) {
	for ( i = 0; i < AKBASIC_UI_MAX_MENUS; i++ ) {
	    PASS(errctx, obj->ui->menu(obj->ui, i, NULL, 0));
	    obj->ui_state.menudefined[i] = false;
	}
	SUCCEED_TRUE(obj, dest);
	SUCCEED_RETURN(errctx);
    }

    PASS(errctx, nth_number(obj, expr, "MENU", 0, &n));
    PASS(errctx, menu_index(n, "MENU", &slot));

    /* A number and nothing else retires that one. */
    if ( count == 1 ) {
	PASS(errctx, obj->ui->menu(obj->ui, slot, NULL, 0));
	obj->ui_state.menudefined[slot] = false;
	SUCCEED_TRUE(obj, dest);
	SUCCEED_RETURN(errctx);
    }

    entries = count - 1;
    FAIL_ZERO_RETURN(errctx, (entries <= AKBASIC_UI_MAX_MENU_ITEMS), AKBASIC_ERR_SYNTAX,
		     "MENU takes at most %d entries and was given %d",
		     AKBASIC_UI_MAX_MENU_ITEMS, entries);
    for ( i = 0; i < entries; i++ ) {
	PASS(errctx, nth_string(obj, expr, "MENU", i + 1, items[i], sizeof(items[i])));
	pointers[i] = items[i];
    }

    PASS(errctx, obj->ui->menu(obj->ui, slot, pointers, entries));
    obj->ui_state.menudefined[slot] = true;
    SUCCEED_TRUE(obj, dest);
    SUCCEED_RETURN(errctx);
}

/* ------------------------------------------------------------- GETMENU --- */

/**
 * @brief Identify the numeric variable a GETMENU assigns into.
 *
 * Numeric only. The answer is an entry number, and a program that wanted the
 * entry's *text* already has it -- it wrote the MENU.
 */
static akerr_ErrorContext *target_variable(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Variable **dest, char *name, size_t len)
{
    PREPARE_ERROR(errctx);
    akbasic_ASTLeaf *arg = nth_argument(expr, 1);

    FAIL_ZERO_RETURN(errctx, (arg != NULL), AKBASIC_ERR_SYNTAX,
		     "GETMENU expected a menu number and a variable");
    /*
     * Either numeric spelling -- `C%` or `C!` -- and not a bare `C`, which in
     * this dialect is a *label* rather than a variable and cannot be assigned to
     * at all. GETKEY refuses one for the same reason. A string is refused
     * because the answer is an entry *number*: a program that wanted the entry's
     * text already has it, since it wrote the MENU.
     */
    FAIL_ZERO_RETURN(errctx,
		     (arg->leaftype == AKBASIC_LEAF_IDENTIFIER_INT ||
		      arg->leaftype == AKBASIC_LEAF_IDENTIFIER_FLOAT),
		     AKBASIC_ERR_TYPE, "GETMENU expected a numeric variable such as C%%");
    PASS(errctx, akbasic_environment_get(obj->environment, arg->identifier, dest));
    FAIL_ZERO_RETURN(errctx, (*dest != NULL), AKBASIC_ERR_UNDEFINED,
		     "GETMENU could not reach the variable %s", arg->identifier);
    PASS(errctx, aksl_strcpy(name, len, arg->identifier));
    SUCCEED_RETURN(errctx);
}

/** @brief Write an entry number -- or 0 for "released without a choice" -- into a variable. */
static akerr_ErrorContext *store_entry(akbasic_Variable *variable, int64_t entry)
{
    PREPARE_ERROR(errctx);
    int64_t zerosubscript[1] = { 0 };

    PASS(errctx, akbasic_variable_set_integer(variable, entry, zerosubscript, 1));
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akbasic_cmd_getmenu(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
    PREPARE_ERROR(errctx);
    akbasic_Variable *variable = NULL;
    char name[64];
    int slot = 0;
    int selected = 0;
    bool activated = false;
    int64_t n = 0;

    (void)lval; (void)rval;
    PASS(errctx, require_ui(obj, "GETMENU"));
    PASS(errctx, nth_number(obj, expr, "GETMENU", 0, &n));
    PASS(errctx, menu_index(n, "GETMENU", &slot));
    PASS(errctx, target_variable(obj, expr, &variable, name, sizeof(name)));

    FAIL_ZERO_RETURN(errctx, obj->ui_state.menudefined[slot], AKBASIC_ERR_STATE,
		     "GETMENU: menu %" PRId64 " has no entries; define it with MENU first", n);

    PASS(errctx, obj->ui->menu_state(obj->ui, slot, &selected, &activated, true));
    if ( activated ) {
	PASS(errctx, store_entry(variable, (int64_t)selected + 1));
	SUCCEED_TRUE(obj, dest);
	SUCCEED_RETURN(errctx);
    }

    /*
     * Nobody has chosen yet. A C128 would sit here; the library may not, so the
     * step loop is told to stop advancing the program counter instead. The
     * program stays on this line, the host keeps its frame rate, and the sprite,
     * audio and collision services -- which run before this one -- keep running
     * underneath. See akbasic_ui_service().
     */
    obj->ui_state.waiting = true;
    obj->ui_state.waitmenu = slot;
    PASS(errctx, aksl_strcpy(obj->ui_state.variable, sizeof(obj->ui_state.variable), name));
    SUCCEED_TRUE(obj, dest);
    SUCCEED_RETURN(errctx);
}

/* --------------------------------------------------------------- RMENU --- */

akerr_ErrorContext *akbasic_fn_rmenu(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
    PREPARE_ERROR(errctx);
    int slot = 0;
    int selected = 0;
    bool activated = false;
    int64_t n = 0;
    int64_t field = 0;

    (void)lval; (void)rval;
    FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
		     "NULL argument in RMENU");
    PASS(errctx, require_ui(obj, "RMENU"));
    PASS(errctx, nth_number(obj, expr, "RMENU", 0, &n));
    PASS(errctx, nth_number(obj, expr, "RMENU", 1, &field));
    PASS(errctx, menu_index(n, "RMENU", &slot));
    FAIL_ZERO_RETURN(errctx, (field == 0 || field == 1), AKBASIC_ERR_BOUNDS,
		     "RMENU: field %" PRId64 " is outside 0..1", field);

    /*
     * Field 1 clears the latch and field 0 does not, which is the whole
     * difference between them: reading which entry is highlighted must not
     * consume the fact that one was chosen, and reading that one was chosen must
     * consume it or a polling loop sees the same choice forever. Same contract
     * BUMP() carries, and for the same reason.
     */
    PASS(errctx, obj->ui->menu_state(obj->ui, slot, &selected, &activated, (field == 1)));
    if ( field == 1 ) {
	/*
	 * An integer -1 or 0, not a BOOLEAN. A BOOLEAN value prints as "true" or
	 * "false" -- the Go reference's %t, kept deliberately and recorded in
	 * src/value.c -- and `PRINT RMENU(1,1)` saying "false" where every other
	 * predicate in this dialect says 0 would be a surprise with no upside.
	 * Both work in `IF RMENU(1,1) THEN`.
	 */
	PASS(errctx, integer_result(obj, activated ? AKBASIC_TRUE : 0, dest));
	SUCCEED_RETURN(errctx);
    }
    PASS(errctx, integer_result(obj, (int64_t)selected + 1, dest));
    SUCCEED_RETURN(errctx);
}

/* -------------------------------------------------------------- DIALOG --- */

akerr_ErrorContext *akbasic_cmd_dialog(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
    PREPARE_ERROR(errctx);
    char text[AKBASIC_MAX_STRING_LENGTH];
    int count = 0;

    (void)lval; (void)rval;
    PASS(errctx, require_ui(obj, "DIALOG"));
    count = argument_count(expr);

    /*
     * No argument takes the panel down. libakgl has no dismiss contract at all
     * -- a declarative frame is the visibility flag -- so this verb is both the
     * only way up and the only way down.
     */
    if ( count == 0 ) {
	PASS(errctx, obj->ui->dialog(obj->ui, NULL));
	SUCCEED_TRUE(obj, dest);
	SUCCEED_RETURN(errctx);
    }
    FAIL_ZERO_RETURN(errctx, (count == 1), AKBASIC_ERR_SYNTAX,
		     "DIALOG takes one string, or nothing at all");
    PASS(errctx, nth_string(obj, expr, "DIALOG", 0, text, sizeof(text)));
    PASS(errctx, obj->ui->dialog(obj->ui, text));
    SUCCEED_TRUE(obj, dest);
    SUCCEED_RETURN(errctx);
}

/* ----------------------------------------------------------------- HUD --- */

akerr_ErrorContext *akbasic_cmd_hud(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
    PREPARE_ERROR(errctx);
    char text[AKBASIC_MAX_STRING_LENGTH];
    int count = 0;
    int slot = 0;
    int i = 0;
    int64_t n = 0;
    int64_t anchor = 0;

    (void)lval; (void)rval;
    PASS(errctx, require_ui(obj, "HUD"));
    count = argument_count(expr);

    if ( count == 0 ) {
	for ( i = 0; i < AKBASIC_UI_MAX_LABELS; i++ ) {
	    PASS(errctx, obj->ui->label(obj->ui, i, AKBASIC_UI_ANCHOR_TOP_LEFT, NULL));
	}
	SUCCEED_TRUE(obj, dest);
	SUCCEED_RETURN(errctx);
    }

    PASS(errctx, nth_number(obj, expr, "HUD", 0, &n));
    PASS(errctx, label_index(n, "HUD", &slot));

    if ( count == 1 ) {
	PASS(errctx, obj->ui->label(obj->ui, slot, AKBASIC_UI_ANCHOR_TOP_LEFT, NULL));
	SUCCEED_TRUE(obj, dest);
	SUCCEED_RETURN(errctx);
    }
    FAIL_ZERO_RETURN(errctx, (count == 3), AKBASIC_ERR_SYNTAX,
		     "HUD expected a slot, an anchor and a string");

    PASS(errctx, nth_number(obj, expr, "HUD", 1, &anchor));
    FAIL_ZERO_RETURN(errctx, (anchor >= 0 && anchor < AKBASIC_UI_ANCHOR_LIMIT),
		     AKBASIC_ERR_BOUNDS, "HUD: anchor %" PRId64 " is outside 0..%d",
		     anchor, (int)AKBASIC_UI_ANCHOR_LIMIT - 1);
    PASS(errctx, nth_string(obj, expr, "HUD", 2, text, sizeof(text)));
    PASS(errctx, obj->ui->label(obj->ui, slot, (int)anchor, text));
    SUCCEED_TRUE(obj, dest);
    SUCCEED_RETURN(errctx);
}

/* ------------------------------------------------------------- UISTYLE --- */

akerr_ErrorContext *akbasic_cmd_uistyle(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
    PREPARE_ERROR(errctx);
    double args[5];
    akbasic_Color fill;
    akbasic_Color edge;
    akbasic_Color ink;
    int count = 0;
    double padding = 0.0;
    double radius = 0.0;

    (void)lval; (void)rval;
    PASS(errctx, require_ui(obj, "UISTYLE"));
    PASS(errctx, akbasic_args_numbers(obj, expr, "UISTYLE", args, 5, &count));

    /*
     * No arguments hands the widgets back to libakgl's own default, which is a
     * dark textbox with parchment ink. Three NULLs say "the library's", not
     * "black": a style is not a thing this interpreter can spell in full, and
     * inventing values here would make UISTYLE with no arguments mean something
     * different from never having called it.
     */
    if ( count == 0 ) {
	PASS(errctx, obj->ui->style(obj->ui, NULL, NULL, NULL, 0.0, 0.0));
	SUCCEED_TRUE(obj, dest);
	SUCCEED_RETURN(errctx);
    }
    FAIL_ZERO_RETURN(errctx, (count >= 3), AKBASIC_ERR_SYNTAX,
		     "UISTYLE expected a fill, an edge and an ink colour");

    PASS(errctx, akbasic_graphics_palette((int)args[0], &fill));
    PASS(errctx, akbasic_graphics_palette((int)args[1], &edge));
    PASS(errctx, akbasic_graphics_palette((int)args[2], &ink));
    padding = (count >= 4) ? args[3] : 8.0;
    radius = (count >= 5) ? args[4] : 0.0;
    FAIL_NONZERO_RETURN(errctx, (padding < 0.0 || radius < 0.0), AKBASIC_ERR_VALUE,
			"UISTYLE: padding and radius cannot be negative");

    PASS(errctx, obj->ui->style(obj->ui, &fill, &edge, &ink, padding, radius));
    SUCCEED_TRUE(obj, dest);
    SUCCEED_RETURN(errctx);
}

/* ------------------------------------------------------- the step loop --- */

akerr_ErrorContext *akbasic_ui_state_init(akbasic_UiState *obj)
{
    PREPARE_ERROR(errctx);

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

akerr_ErrorContext *akbasic_ui_service(akbasic_Runtime *obj, bool *blocked)
{
    PREPARE_ERROR(errctx);
    akbasic_Variable *variable = NULL;
    int selected = 0;
    bool activated = false;
    bool released = false;

    FAIL_ZERO_RETURN(errctx, (obj != NULL && blocked != NULL), AKERR_NULLPOINTER,
		     "NULL argument in ui_service");
    *blocked = false;
    if ( !obj->ui_state.waiting ) {
	SUCCEED_RETURN(errctx);
    }

    /*
     * Two ways out that are not a choice, and neither may wedge the script. A
     * host is allowed to change its mind about what it lends out, and a program
     * is allowed to retire the menu it is waiting on -- from an interrupt
     * handler, which is the only place it could -- so both release the hold and
     * assign 0. Same rule akbasic_input_service() keeps for a withdrawn keyboard.
     */
    if ( obj->ui == NULL ) {
	released = true;
    } else if ( !obj->ui_state.menudefined[obj->ui_state.waitmenu] ) {
	released = true;
    } else {
	PASS(errctx, obj->ui->menu_state(obj->ui, obj->ui_state.waitmenu,
					 &selected, &activated, true));
	if ( !activated ) {
	    *blocked = true;
	    SUCCEED_RETURN(errctx);
	}
    }

    obj->ui_state.waiting = false;
    PASS(errctx, akbasic_environment_get(obj->environment, obj->ui_state.variable, &variable));
    FAIL_ZERO_RETURN(errctx, (variable != NULL), AKBASIC_ERR_UNDEFINED,
		     "GETMENU could not reach the variable %s", obj->ui_state.variable);
    PASS(errctx, store_entry(variable, released ? 0 : (int64_t)selected + 1));
    SUCCEED_RETURN(errctx);
}
