/**
 * @file ui_akgl.c
 * @brief Wires the UI backend record to libakgl's clay-backed widget helpers.
 *
 * The three helpers -- akgl_ui_dialog, akgl_ui_label and akgl_ui_menu -- and the
 * frame bracket around them. Nothing here touches clay directly: a BASIC program
 * gets the widgets, not the layout engine, and `CLAY()` blocks are the route a
 * game written in C takes instead.
 *
 * **This file is where immediate mode meets a language that is not.** libakgl
 * wants the widget set declared inside a bracket every frame and borrows the
 * text until the bracket closes; a BASIC program says `MENU 1, "START"` on line
 * 100 and expects it up on line 900. So the record's entry points are setters
 * that copy into akbasic_AkglUi, and akbasic_ui_akgl_render() replays the whole
 * set once a frame. The verbs never see a frame and the frame never sees a BASIC
 * string.
 */

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

#include <akgl/error.h>
#include <akgl/registry.h>
#include <akgl/text.h>
#include <akgl/ui.h>

#include <akbasic/akgl.h>
#include <akbasic/error.h>

/**
 * @brief Registry name the UI font is loaded under.
 *
 * Its own name rather than sharing the sink's, because the sink does not use the
 * font registry at all -- it holds a `TTF_Font *` it opened itself -- and because
 * a size is baked into a registered handle, so the UI's font is a different
 * entry even when it is the same file.
 */
#define UI_FONT_NAME		"akbasic.ui"

/**
 * @brief Fail the build if the two menu ceilings ever disagree.
 *
 * include/akbasic/ui.h restates AKGL_UI_MENU_MAX_ITEMS rather than including an
 * akgl header, which is correct and is also exactly the kind of restatement that
 * rots. A mismatch would overflow akgl_UiMenu::items, so it is a negative array
 * size here rather than a memory error at frame one.
 */
typedef char ui_menu_ceilings_agree[
    (AKBASIC_UI_MAX_MENU_ITEMS == AKGL_UI_MENU_MAX_ITEMS) ? 1 : -1];

/** @brief Recover the backend's own state, or say that it has none. */
static akerr_ErrorContext *state_of(akbasic_UiBackend *self, akbasic_AkglUi **dest)
{
    PREPARE_ERROR(errctx);

    FAIL_ZERO_RETURN(errctx, (self != NULL && dest != NULL), AKERR_NULLPOINTER,
		     "NULL argument in akgl UI backend");
    *dest = (akbasic_AkglUi *)self->self;
    FAIL_ZERO_RETURN(errctx, (*dest != NULL), AKERR_NULLPOINTER,
		     "akgl UI backend has no state");
    SUCCEED_RETURN(errctx);
}

/** @brief The style every widget draws with, or NULL for libakgl's own default. */
static akgl_UiStyle *look_of(akbasic_AkglUi *state)
{
    return state->styled ? &state->style : NULL;
}

/** @brief The colour conversion, which is the whole impedance mismatch. */
static SDL_Color to_sdl(akbasic_Color color)
{
    SDL_Color out;

    out.r = color.r;
    out.g = color.g;
    out.b = color.b;
    out.a = color.a;
    return out;
}

/* ------------------------------------------------- the record's setters --- */

static akerr_ErrorContext *ui_dialog(akbasic_UiBackend *self, const char *text)
{
    PREPARE_ERROR(errctx);
    akbasic_AkglUi *state = NULL;

    PASS(errctx, state_of(self, &state));
    if ( text == NULL || text[0] == '\0' ) {
	state->dialogopen = false;
	state->dialogtext[0] = '\0';
	SUCCEED_RETURN(errctx);
    }
    PASS(errctx, aksl_strcpy(state->dialogtext, sizeof(state->dialogtext), text));
    state->dialogopen = true;
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext *ui_label(akbasic_UiBackend *self, int slot, int anchor, const char *text)
{
    PREPARE_ERROR(errctx);
    akbasic_AkglUi *state = NULL;

    PASS(errctx, state_of(self, &state));
    FAIL_ZERO_RETURN(errctx, (slot >= 0 && slot < AKBASIC_UI_MAX_LABELS),
		     AKERR_OUTOFBOUNDS, "HUD slot %d is outside the backend's range", slot);
    if ( text == NULL ) {
	state->labelset[slot] = false;
	state->labeltext[slot][0] = '\0';
	SUCCEED_RETURN(errctx);
    }
    PASS(errctx, aksl_strcpy(state->labeltext[slot], sizeof(state->labeltext[slot]), text));
    state->labelanchor[slot] = anchor;
    state->labelset[slot] = true;
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext *ui_menu(akbasic_UiBackend *self, int slot, const char *const *items, int count)
{
    PREPARE_ERROR(errctx);
    akbasic_AkglUi *state = NULL;
    int i = 0;

    PASS(errctx, state_of(self, &state));
    FAIL_ZERO_RETURN(errctx, (slot >= 0 && slot < AKBASIC_UI_MAX_MENUS),
		     AKERR_OUTOFBOUNDS, "MENU slot %d is outside the backend's range", slot);
    FAIL_ZERO_RETURN(errctx, (count >= 0 && count <= AKBASIC_UI_MAX_MENU_ITEMS),
		     AKERR_OUTOFBOUNDS, "MENU entry count %d is outside 0..%d",
		     count, AKBASIC_UI_MAX_MENU_ITEMS);
    FAIL_NONZERO_RETURN(errctx, (count > 0 && items == NULL), AKERR_NULLPOINTER,
			"MENU was given %d entries and no list", count);

    /*
     * Redefining resets the selection and the latch. The entries have just
     * changed meaning, so an index into the old list is not worth carrying over
     * and an unread activation of an entry that no longer exists is worse.
     */
    for ( i = 0; i < count; i++ ) {
	FAIL_ZERO_RETURN(errctx, (items[i] != NULL), AKERR_NULLPOINTER,
			 "MENU entry %d is NULL", i + 1);
	PASS(errctx, aksl_strcpy(state->menuitems[slot][i], sizeof(state->menuitems[slot][i]),
				 items[i]));
	state->menus[slot].items[i] = state->menuitems[slot][i];
    }
    for ( i = count; i < AKBASIC_UI_MAX_MENU_ITEMS; i++ ) {
	state->menus[slot].items[i] = NULL;
    }
    state->menus[slot].count = count;
    state->menus[slot].selected = 0;
    state->menus[slot].activated = false;
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext *ui_menu_state(akbasic_UiBackend *self, int slot, int *selected, bool *activated, bool clear)
{
    PREPARE_ERROR(errctx);
    akbasic_AkglUi *state = NULL;

    PASS(errctx, state_of(self, &state));
    FAIL_ZERO_RETURN(errctx, (selected != NULL && activated != NULL), AKERR_NULLPOINTER,
		     "NULL destination in menu_state");
    FAIL_ZERO_RETURN(errctx, (slot >= 0 && slot < AKBASIC_UI_MAX_MENUS),
		     AKERR_OUTOFBOUNDS, "MENU slot %d is outside the backend's range", slot);

    *selected = (int)state->menus[slot].selected;
    *activated = state->menus[slot].activated;
    if ( clear ) {
	state->menus[slot].activated = false;
    }
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext *ui_style(akbasic_UiBackend *self, akbasic_Color *fill, akbasic_Color *edge, akbasic_Color *ink, double padding, double radius)
{
    PREPARE_ERROR(errctx);
    akbasic_AkglUi *state = NULL;

    PASS(errctx, state_of(self, &state));
    /*
     * No colours means the library's default, and the way to say that is to stop
     * having a style rather than to copy libakgl's values into ours. Copying them
     * would make UISTYLE-with-no-arguments a snapshot of whatever the default was
     * on the day this was written.
     */
    if ( fill == NULL || edge == NULL || ink == NULL ) {
	state->styled = false;
	SUCCEED_RETURN(errctx);
    }
    state->style.fill = to_sdl(*fill);
    state->style.edge = to_sdl(*edge);
    state->style.ink = to_sdl(*ink);
    state->style.padding = (float32_t)padding;
    state->style.corner_radius = (float32_t)radius;
    state->style.fontid = state->fontid;
    state->styled = true;
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext *ui_clear(akbasic_UiBackend *self)
{
    PREPARE_ERROR(errctx);
    akbasic_AkglUi *state = NULL;
    int i = 0;

    PASS(errctx, state_of(self, &state));
    state->dialogopen = false;
    state->dialogtext[0] = '\0';
    state->styled = false;
    for ( i = 0; i < AKBASIC_UI_MAX_LABELS; i++ ) {
	state->labelset[i] = false;
	state->labeltext[i][0] = '\0';
    }
    for ( i = 0; i < AKBASIC_UI_MAX_MENUS; i++ ) {
	state->menus[i].count = 0;
	state->menus[i].selected = 0;
	state->menus[i].activated = false;
    }
    SUCCEED_RETURN(errctx);
}

/* ------------------------------------------------------------- the frame --- */

/**
 * @brief Declare every retained widget into the open frame.
 *
 * Its own function because it is three loops, and CATCH inside one escapes only
 * the loop -- PASS is the only thing that may appear in here. The caller CATCHes
 * this single call, which is what lets it close the bracket on the way out.
 *
 * Order is deliberate: labels, then the dialog, then the menus. A menu is the
 * thing the player is being asked to act on, so nothing else should be able to
 * overlap it.
 */
static akerr_ErrorContext AKERR_NOIGNORE *declare_widgets(akbasic_AkglUi *state)
{
    PREPARE_ERROR(errctx);
    akgl_UiStyle *look = look_of(state);
    int i = 0;

    for ( i = 0; i < AKBASIC_UI_MAX_LABELS; i++ ) {
	if ( state->labelset[i] ) {
	    PASS(errctx, akgl_ui_label(state->labelid[i], state->labeltext[i],
				       (akgl_UiAnchor)state->labelanchor[i], look));
	}
    }
    if ( state->dialogopen ) {
	PASS(errctx, akgl_ui_dialog("dialog", state->dialogtext, look));
    }
    for ( i = 0; i < AKBASIC_UI_MAX_MENUS; i++ ) {
	if ( state->menus[i].count > 0 ) {
	    state->menus[i].style = look;
	    PASS(errctx, akgl_ui_menu(&state->menus[i]));
	}
    }
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akbasic_ui_akgl_render(akbasic_UiBackend *obj)
{
    PREPARE_ERROR(errctx);
    akbasic_AkglUi *state = NULL;
    bool opened = false;

    PASS(errctx, state_of(obj, &state));
    if ( !state->ready ) {
	SUCCEED_RETURN(errctx);
    }

    ATTEMPT {
	CATCH(errctx, akgl_ui_frame_begin());
	opened = true;
	CATCH(errctx, declare_widgets(state));
	/*
	 * Cleared before frame_end rather than after, because frame_end closes
	 * the bracket whether it succeeds or fails -- so a CLEANUP that closed it
	 * again would be closing somebody else's next frame.
	 */
	opened = false;
	CATCH(errctx, akgl_ui_frame_end(state->renderer));
    } CLEANUP {
	if ( opened ) {
	    IGNORE(akgl_ui_frame_end(state->renderer));
	}
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akbasic_ui_akgl_handle_event(akbasic_UiBackend *obj, SDL_Event *event, bool *consumed)
{
    PREPARE_ERROR(errctx);
    akbasic_AkglUi *state = NULL;
    int i = 0;

    PASS(errctx, state_of(obj, &state));
    FAIL_ZERO_RETURN(errctx, (event != NULL && consumed != NULL), AKERR_NULLPOINTER,
		     "NULL argument in ui_akgl_handle_event");
    *consumed = false;
    if ( !state->ready ) {
	SUCCEED_RETURN(errctx);
    }

    /*
     * The subsystem first: it is mouse-only and never consumes a keystroke, so
     * this is safe whatever else is going on.
     */
    PASS(errctx, akgl_ui_handle_event(state, event, consumed));
    if ( *consumed ) {
	SUCCEED_RETURN(errctx);
    }

    /*
     * Then each menu that has entries -- and these *do* take Up, Down and
     * Return. That is why the loop is over menus with a count rather than over
     * all four: a program with no menu up leaves the cursor keys and Return to
     * the line editor, which is what makes the REPL usable at all.
     */
    for ( i = 0; i < AKBASIC_UI_MAX_MENUS; i++ ) {
	if ( state->menus[i].count == 0 ) {
	    continue;
	}
	PASS(errctx, akgl_ui_menu_handle_event(&state->menus[i], event, consumed));
	if ( *consumed ) {
	    SUCCEED_RETURN(errctx);
	}
    }
    SUCCEED_RETURN(errctx);
}

/* ------------------------------------------------------------- lifecycle --- */

akerr_ErrorContext *akbasic_ui_init_akgl(akbasic_UiBackend *obj, akbasic_AkglUi *state, akgl_RenderBackend *renderer, const char *fontpath, int fontsize, int width, int height)
{
    PREPARE_ERROR(errctx);
    int count = 0;
    int i = 0;

    FAIL_ZERO_RETURN(errctx, (obj != NULL && state != NULL && fontpath != NULL),
		     AKERR_NULLPOINTER, "NULL argument in ui_init_akgl");
    FAIL_ZERO_RETURN(errctx, (renderer != NULL), AKERR_NULLPOINTER,
		     "NULL renderer in ui_init_akgl: the host creates it, not this");
    FAIL_ZERO_RETURN(errctx, (fontsize > 0 && width > 0 && height > 0), AKBASIC_ERR_VALUE,
		     "A %dx%d UI at %d points is not a UI", width, height, fontsize);
    PASS(errctx, akgl_error_init());

    PASS(errctx, aksl_memset(state, 0, sizeof(*state)));
    state->renderer = renderer;
    PASS(errctx, aksl_strcpy(state->fontname, sizeof(state->fontname), UI_FONT_NAME));

    /*
     * The element ids, once. clay identifies an element by its string and keeps
     * hover and scroll state against it between frames, so these have to be the
     * same every frame -- which means they cannot be built on the stack of the
     * function that declares them.
     */
    for ( i = 0; i < AKBASIC_UI_MAX_LABELS; i++ ) {
	PASS(errctx, aksl_snprintf(&count, state->labelid[i], sizeof(state->labelid[i]),
				   "hud%d", i + 1));
    }
    for ( i = 0; i < AKBASIC_UI_MAX_MENUS; i++ ) {
	PASS(errctx, aksl_snprintf(&count, state->menuid[i], sizeof(state->menuid[i]),
				   "menu%d", i + 1));
	state->menus[i].id = state->menuid[i];
    }

    /*
     * **akgl_registry_init() does not initialize the font registry** -- its own
     * header says so -- and nothing else in this repository has needed it,
     * because the text sink opens its font with a bare TTF_OpenFont and holds
     * the handle. akgl_ui_font_register() resolves a *registry name* per use, so
     * the font has to go in there. It is idempotent.
     */
    PASS(errctx, akgl_registry_init_font());
    PASS(errctx, akgl_text_loadfont(state->fontname, (char *)fontpath, fontsize));
    PASS(errctx, akgl_ui_init(width, height));
    PASS(errctx, akgl_ui_font_register(state->fontname, &state->fontid));
    state->ready = true;

    obj->self		= state;
    obj->dialog		= ui_dialog;
    obj->label		= ui_label;
    obj->menu		= ui_menu;
    obj->menu_state	= ui_menu_state;
    obj->style		= ui_style;
    obj->clear		= ui_clear;
    SUCCEED_RETURN(errctx);
}

void akbasic_ui_akgl_shutdown(akbasic_UiBackend *obj)
{
    akbasic_AkglUi *state = NULL;

    if ( obj == NULL || obj->self == NULL ) {
	return;
    }
    state = (akbasic_AkglUi *)obj->self;
    if ( !state->ready ) {
	return;
    }
    /*
     * The subsystem first, then the font: anything still holding a clay layout
     * would be talking to a disowned context after the shutdown, and the font is
     * what its text commands resolve through. Both are ignorable -- this is
     * called from teardown paths that are already unwinding, and a failure to
     * close something down has nowhere useful to go.
     */
    IGNORE(akgl_ui_shutdown());
    IGNORE(akgl_text_unloadfont(state->fontname));
    state->ready = false;
}
