/**
 * @file uidemo.c
 * @brief The UI demo: a title menu, an options screen, and a HUD, three ways.
 *
 * The chapter on the UI subsystem quotes this program rather than restating
 * it. Run it with no arguments for a window:
 *
 *     ./examples/uidemo/uidemo
 *
 * The title menu answers to the arrow keys, Return, the D-pad, and the mouse.
 * Start opens a stand-in play screen -- a checkerboard where a game would be
 * -- with a score counter ticking in a HUD label; Space opens and closes a
 * dialog panel there, and Escape backs out. Options is a screen written in
 * raw CLAY() declarations, because the widgets are a convenience, not a
 * boundary. `--frames N` bounds the run and `--demo` drives the whole tour
 * from a script, which is what makes this runnable as a headless smoke test.
 *
 * What is deliberately absent: a tilemap, actors, physics. The world here is
 * one draw call, so everything left is the subject -- what a UI costs and
 * where it goes in a frame.
 */

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

#include <SDL3/SDL.h>
#include <SDL3_image/SDL_image.h>
#include <akerror.h>
#include <akstdlib.h>

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

#include "uidemo.h"

/** @brief Milliseconds one 60 Hz frame is allowed to take, for the interactive loop. */
#define UIDEMO_FRAME_BUDGET_MS 16

/*
 * The scripted tour `--demo` drives. Every route through the demo, by every
 * input device it supports: the mouse clicks the title menu open (dead centre
 * lands on the middle row by symmetry -- the menu is centred and Options is
 * its middle entry), the keyboard walks back out, into the play screen,
 * through the dialog, and finally down to Quit.
 *
 *  Frame   Event                        Key / position    What it proves
 */
static const uidemo_ScriptStep UIDEMO_SCRIPT[] = {
    {   5,  SDL_EVENT_MOUSE_MOTION,      0,           320.0f, 240.0f },  /* hover the menu     */
    {  10,  SDL_EVENT_MOUSE_BUTTON_DOWN, 0,           320.0f, 240.0f },  /* consumed click     */
    {  11,  SDL_EVENT_MOUSE_BUTTON_UP,   0,           320.0f, 240.0f },  /* -> Options screen  */
    {  40,  SDL_EVENT_KEY_DOWN,          SDLK_ESCAPE, 0.0f,   0.0f   },  /* back to the title  */
    {  50,  SDL_EVENT_KEY_DOWN,          SDLK_UP,     0.0f,   0.0f   },  /* select Start       */
    {  60,  SDL_EVENT_KEY_DOWN,          SDLK_RETURN, 0.0f,   0.0f   },  /* -> play screen     */
    {  80,  SDL_EVENT_KEY_DOWN,          SDLK_SPACE,  0.0f,   0.0f   },  /* open the dialog    */
    { 120,  SDL_EVENT_KEY_DOWN,          SDLK_SPACE,  0.0f,   0.0f   },  /* close it again     */
    { 140,  SDL_EVENT_KEY_DOWN,          SDLK_ESCAPE, 0.0f,   0.0f   },  /* back to the title  */
    { 150,  SDL_EVENT_KEY_DOWN,          SDLK_DOWN,   0.0f,   0.0f   },  /* down to Options    */
    { 160,  SDL_EVENT_KEY_DOWN,          SDLK_DOWN,   0.0f,   0.0f   },  /* down to Quit       */
    { 170,  SDL_EVENT_KEY_DOWN,          SDLK_RETURN, 0.0f,   0.0f   }   /* and out            */
};

#define UIDEMO_SCRIPT_STEPS (sizeof(UIDEMO_SCRIPT) / sizeof(UIDEMO_SCRIPT[0]))

static long frame_limit = 0;
/** @brief Where `--screenshot` writes, and on which frame. NULL means never. */
static char *shotpath = NULL;
static long shotframe = 0;
static bool demo = false;
static bool running = true;
static int exitstatus = 0;
static bool lowfps_warned = false;

/** @brief Which screen the frame declares. */
static uidemo_State state = UIDEMO_STATE_TITLE;

/**
 * @brief The title menu. Caller-owned state, zero machinery: this struct and
 * the akgl_ui_menu call each frame are the whole main menu.
 */
static akgl_UiMenu title_menu = {
    .id = "title",
    .items = { "Start", "Options", "Quit" },
    .count = 3,
};

/** @brief The options screen's two settings. What the toggles flip. */
static bool opt_music = true;
static bool opt_sound = true;

/** @brief The play screen's HUD numbers. The score ticks so the label visibly updates. */
static int score = 0;
static int lives = 3;
/** @brief Whether the play screen's dialog is up. Space flips it. */
static bool dialog_open = false;

/**
 * @brief A left click arrived this frame, wherever it landed.
 *
 * The raw CLAY() options screen needs a press edge to pair with
 * Clay_Hovered(), and the application is the right owner of it: it sees
 * every event before the UI does. Set in route_event, cleared at the end of
 * each frame. The widget helpers keep their own edge internally -- this one
 * exists precisely because the options screen does not use them.
 */
static bool clicked = false;

/** @brief Replacement `akgl_game.lowfpsfunc`: say it once, not sixty times a second. */
static void lowfps_quiet(void)
{
    if ( lowfps_warned == false ) {
	lowfps_warned = true;
	SDL_Log("Frame rate is under 30 and this demo does nothing about it");
    }
}

/**
 * @brief Read `--frames N`, `--demo` and the screenshot flags off the command line.
 *
 * @param argc Argument count, from `main`.
 * @param argv Argument vector, from `main`. Required.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKERR_NULLPOINTER If @p argv is `NULL`.
 * @throws AKERR_VALUE If a flag needing an argument is last, or its argument
 *         is not a number.
 */
static akerr_ErrorContext *parse_args(int argc, char *argv[])
{
    PREPARE_ERROR(errctx);
    int i = 0;
    int number = 0;

    FAIL_ZERO_RETURN(errctx, argv, AKERR_NULLPOINTER, "argv");

    for ( i = 1; i < argc; i++ ) {
	if ( strcmp(argv[i], "--demo") == 0 ) {
	    demo = true;
	} else if ( strcmp(argv[i], "--frames") == 0 ) {
	    if ( (i + 1) >= argc ) {
		FAIL_RETURN(errctx, AKERR_VALUE, "--frames needs a frame count");
	    }
	    i += 1;
	    PASS(errctx, aksl_atoi(argv[i], &number));
	    frame_limit = number;
	} else if ( strcmp(argv[i], "--screenshot") == 0 ) {
	    if ( (i + 1) >= argc ) {
		FAIL_RETURN(errctx, AKERR_VALUE, "--screenshot needs a path");
	    }
	    i += 1;
	    shotpath = argv[i];
	} else if ( strcmp(argv[i], "--screenshot-frame") == 0 ) {
	    if ( (i + 1) >= argc ) {
		FAIL_RETURN(errctx, AKERR_VALUE, "--screenshot-frame needs a number");
	    }
	    i += 1;
	    PASS(errctx, aksl_atoi(argv[i], &number));
	    shotframe = number;
	} else {
	    FAIL_RETURN(errctx, AKERR_VALUE,
			"usage: uidemo [--frames N] [--demo]"
			" [--screenshot PATH] [--screenshot-frame N]");
	}
    }
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Bring the library and the UI subsystem up.
 *
 * The UI half is three calls: load a font, akgl_ui_init at the screen size,
 * and register the font for a clay fontId. The id comes back 0 because it is
 * the first registered, which is what the widgets' default style uses -- so
 * nothing here ever mentions a fontId again.
 *
 * @return `NULL` on success, otherwise an error context owned by the caller.
 */
static akerr_ErrorContext *startup(void)
{
    PREPARE_ERROR(errctx);
    uint16_t fontid = 0;

    PASS(errctx, aksl_strncpy(akgl_game.name, sizeof(akgl_game.name), "libakgl UI demo", sizeof(akgl_game.name) - 1));
    PASS(errctx, aksl_strncpy(akgl_game.version, sizeof(akgl_game.version), "1.0.0", sizeof(akgl_game.version) - 1));
    PASS(errctx, aksl_strncpy(akgl_game.uri, sizeof(akgl_game.uri), "net.aklabs.libakgl.examples.uidemo", sizeof(akgl_game.uri) - 1));

    PASS(errctx, akgl_game_init());
    akgl_game.lowfpsfunc = &lowfps_quiet;

    PASS(errctx, akgl_set_property("game.screenwidth", UIDEMO_SCREEN_WIDTH));
    PASS(errctx, akgl_set_property("game.screenheight", UIDEMO_SCREEN_HEIGHT));
    PASS(errctx, akgl_render_2d_init(akgl_renderer));

    PASS(errctx, akgl_text_loadfont(UIDEMO_FONT_NAME, UIDEMO_FONT_FILE, UIDEMO_FONT_SIZE));
    PASS(errctx, akgl_ui_init(UIDEMO_WIDTH, UIDEMO_HEIGHT));
    PASS(errctx, akgl_ui_font_register(UIDEMO_FONT_NAME, &fontid));

    SUCCEED_RETURN(errctx);
}

/**
 * @brief Route one event: the UI first, then whatever the current screen wants.
 *
 * This is the call-order contract from akgl/ui.h in the flesh. The UI gets
 * first refusal; a consumed event goes no further, which is what keeps a
 * click on a menu from also being a click in the game. Only then does the
 * current screen read the keyboard -- and *that* routing is the whole focus
 * model: the title menu hears keys because this function sends them there
 * while the title screen is up, not because anything owns "focus".
 *
 * @param event The event to route. Required.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 */
static akerr_ErrorContext *route_event(SDL_Event *event)
{
    PREPARE_ERROR(errctx);
    bool consumed = false;

    FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "event");

    if ( event->type == SDL_EVENT_QUIT ) {
	running = false;
	SUCCEED_RETURN(errctx);
    }
    // The options screen's press edge, recorded before the UI can consume
    // the event -- a click on a toggle row is *always* consumed (the row is
    // UI), so waiting until afterwards would record nothing.
    if ( (event->type == SDL_EVENT_MOUSE_BUTTON_DOWN)
	 && (event->button.button == SDL_BUTTON_LEFT) ) {
	clicked = true;
    }

    PASS(errctx, akgl_ui_handle_event((void *)&akgl_game.state, event, &consumed));
    if ( consumed ) {
	SUCCEED_RETURN(errctx);
    }

    switch ( state ) {
    case UIDEMO_STATE_TITLE:
	PASS(errctx, akgl_ui_menu_handle_event(&title_menu, event, &consumed));
	break;
    case UIDEMO_STATE_OPTIONS:
	if ( (event->type == SDL_EVENT_KEY_DOWN) && (event->key.key == SDLK_ESCAPE) ) {
	    state = UIDEMO_STATE_TITLE;
	}
	break;
    case UIDEMO_STATE_PLAY:
	if ( event->type == SDL_EVENT_KEY_DOWN ) {
	    if ( event->key.key == SDLK_ESCAPE ) {
		state = UIDEMO_STATE_TITLE;
	    } else if ( event->key.key == SDLK_SPACE ) {
		dialog_open = !dialog_open;
	    }
	}
	break;
    default:
	break;
    }
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Feed the scripted events due on this frame through the real routing.
 *
 * Synthesized SDL_Events, not direct state changes: the demo run proves the
 * event chain -- consumed and not, menu and screen -- because it travels it.
 *
 * @param frameno The frame about to be drawn.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 */
static akerr_ErrorContext *demo_step(long frameno)
{
    PREPARE_ERROR(errctx);
    SDL_Event event;
    size_t i = 0;

    for ( i = 0; i < UIDEMO_SCRIPT_STEPS; i++ ) {
	if ( UIDEMO_SCRIPT[i].frame != frameno ) {
	    continue;
	}
	PASS(errctx, aksl_memset((void *)&event, 0x00, sizeof(event)));
	event.type = UIDEMO_SCRIPT[i].type;
	switch ( UIDEMO_SCRIPT[i].type ) {
	case SDL_EVENT_KEY_DOWN:
	    event.key.key = UIDEMO_SCRIPT[i].key;
	    break;
	case SDL_EVENT_MOUSE_MOTION:
	    event.motion.x = UIDEMO_SCRIPT[i].x;
	    event.motion.y = UIDEMO_SCRIPT[i].y;
	    break;
	case SDL_EVENT_MOUSE_BUTTON_DOWN:
	case SDL_EVENT_MOUSE_BUTTON_UP:
	    event.button.button = SDL_BUTTON_LEFT;
	    event.button.x = UIDEMO_SCRIPT[i].x;
	    event.button.y = UIDEMO_SCRIPT[i].y;
	    break;
	default:
	    break;
	}
	PASS(errctx, route_event(&event));
    }
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Declare the title screen: one widget call.
 *
 * @return `NULL` on success, otherwise an error context owned by the caller.
 */
static akerr_ErrorContext *declare_title(void)
{
    PREPARE_ERROR(errctx);
    PASS(errctx, akgl_ui_label("heading", "libakgl UI demo", AKGL_UI_ANCHOR_TOP_LEFT, NULL));
    PASS(errctx, akgl_ui_menu(&title_menu));
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Declare the options screen in raw CLAY(), no widgets anywhere.
 *
 * This screen exists to show the other way in: a centred panel, a column of
 * rows, hover highlighting via Clay_Hovered() *inside* the declaration, and
 * clicks paired with the application's own press edge. Everything the widgets
 * do is done with these same pieces; a screen the widgets cannot express is
 * written like this rather than waiting for a widget to exist.
 *
 * The row labels live in statics because clay borrows the pointer until
 * frame_end -- a local buffer here would be dangling by the time the text is
 * drawn.
 *
 * @return `NULL` on success, otherwise an error context owned by the caller.
 */
static akerr_ErrorContext *declare_options(void)
{
    PREPARE_ERROR(errctx);
    static char musicrow[32];
    static char soundrow[32];
    int count = 0;

    PASS(errctx, aksl_snprintf(&count, musicrow, sizeof(musicrow), "Music: %s", opt_music ? "ON" : "OFF"));
    PASS(errctx, aksl_snprintf(&count, soundrow, sizeof(soundrow), "Sound: %s", opt_sound ? "ON" : "OFF"));

    CLAY({
	    .id = CLAY_ID("options"),
	    .layout = {
		.padding = { 16, 16, 16, 16 },
		.childGap = 8,
		.layoutDirection = CLAY_TOP_TO_BOTTOM
	    },
	    .backgroundColor = { 24, 20, 37, 235 },
	    .border = { .color = { 240, 236, 214, 255 }, .width = { 1, 1, 1, 1, 0 } },
	    .floating = {
		.attachPoints = {
		    .element = CLAY_ATTACH_POINT_CENTER_CENTER,
		    .parent = CLAY_ATTACH_POINT_CENTER_CENTER
		},
		.attachTo = CLAY_ATTACH_TO_ROOT
	    }
	}) {
	CLAY_TEXT(CLAY_STRING("OPTIONS"), CLAY_TEXT_CONFIG({
		    .textColor = { 240, 236, 214, 255 },
		    .fontId = 0
		}));

	CLAY({
		.id = CLAY_ID("options-music"),
		.layout = { .padding = { 8, 8, 4, 4 } },
		.backgroundColor = Clay_Hovered()
		? (Clay_Color){ 64, 58, 88, 255 }
		: (Clay_Color){ 0, 0, 0, 0 }
	    }) {
	    if ( Clay_Hovered() && clicked ) {
		opt_music = !opt_music;
	    }
	    CLAY_TEXT(((Clay_String){ .length = (int32_t)strlen(musicrow), .chars = musicrow }),
		      CLAY_TEXT_CONFIG({ .textColor = { 240, 236, 214, 255 }, .fontId = 0 }));
	}

	CLAY({
		.id = CLAY_ID("options-sound"),
		.layout = { .padding = { 8, 8, 4, 4 } },
		.backgroundColor = Clay_Hovered()
		? (Clay_Color){ 64, 58, 88, 255 }
		: (Clay_Color){ 0, 0, 0, 0 }
	    }) {
	    if ( Clay_Hovered() && clicked ) {
		opt_sound = !opt_sound;
	    }
	    CLAY_TEXT(((Clay_String){ .length = (int32_t)strlen(soundrow), .chars = soundrow }),
		      CLAY_TEXT_CONFIG({ .textColor = { 240, 236, 214, 255 }, .fontId = 0 }));
	}

	CLAY({
		.id = CLAY_ID("options-back"),
		.layout = { .padding = { 8, 8, 4, 4 } },
		.backgroundColor = Clay_Hovered()
		? (Clay_Color){ 64, 58, 88, 255 }
		: (Clay_Color){ 0, 0, 0, 0 }
	    }) {
	    if ( Clay_Hovered() && clicked ) {
		state = UIDEMO_STATE_TITLE;
	    }
	    CLAY_TEXT(CLAY_STRING("Back (Esc)"), CLAY_TEXT_CONFIG({
			.textColor = { 240, 236, 214, 255 },
			.fontId = 0
		    }));
	}
    }
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Declare the play screen's HUD: two labels, and the dialog while it is up.
 *
 * The score buffer is static for the same borrowed-until-frame_end reason as
 * the options rows. Note what is *not* here: no visible flag, no draw call,
 * no geometry. The dialog is open because this frame declares it.
 *
 * @return `NULL` on success, otherwise an error context owned by the caller.
 */
static akerr_ErrorContext *declare_play(void)
{
    PREPARE_ERROR(errctx);
    static char scoretext[32];
    static char livestext[32];
    int count = 0;

    PASS(errctx, aksl_snprintf(&count, scoretext, sizeof(scoretext), "SCORE %05d", score));
    PASS(errctx, aksl_snprintf(&count, livestext, sizeof(livestext), "LIVES %d", lives));
    PASS(errctx, akgl_ui_label("score", scoretext, AKGL_UI_ANCHOR_TOP_RIGHT, NULL));
    PASS(errctx, akgl_ui_label("lives", livestext, AKGL_UI_ANCHOR_TOP_LEFT, NULL));
    if ( dialog_open ) {
	PASS(errctx, akgl_ui_dialog("dialog",
				    "This panel is one call. Space dismisses it; "
				    "compare examples/jrpg/textbox.c.",
				    NULL));
    }
    SUCCEED_RETURN(errctx);
}

/** @brief Read the render target back and write it out as a PNG, for the chapter figure. */
static akerr_ErrorContext *save_screenshot(char *path)
{
    SDL_Surface *shot = NULL;
    PREPARE_ERROR(errctx);

    FAIL_ZERO_RETURN(errctx, path, AKERR_NULLPOINTER, "path");

    shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
    FAIL_ZERO_RETURN(errctx, shot, AKGL_ERR_SDL, "SDL_RenderReadPixels: %s", SDL_GetError());

    ATTEMPT {
	FAIL_ZERO_BREAK(errctx, IMG_SavePNG(shot, path), AKGL_ERR_SDL,
			"IMG_SavePNG(%s): %s", path, SDL_GetError());
    } CLEANUP {
	SDL_DestroySurface(shot);
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SDL_Log("Wrote %s", path);
    SUCCEED_RETURN(errctx);
}

/**
 * @brief One frame: events, the world stand-in, the UI bracket, present.
 *
 * The shape to compare with the JRPG's frame(): where that program calls
 * akgl_game_update between frame_start and frame_end, this one paints a
 * checkerboard -- and the UI bracket sits in the same overlay slot the
 * hand-rolled textbox draw did.
 *
 * @param frameno The frame number, for the demo script.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 */
static akerr_ErrorContext *frame(long frameno)
{
    PREPARE_ERROR(errctx);
    SDL_Event event;

    while ( SDL_PollEvent(&event) ) {
	PASS(errctx, route_event(&event));
    }
    if ( demo ) {
	PASS(errctx, demo_step(frameno));
    }

    PASS(errctx, akgl_renderer->frame_start(akgl_renderer));
    if ( state == UIDEMO_STATE_PLAY ) {
	// Where a game would call akgl_game_update. The score ticking is the
	// stand-in for play, so the HUD label visibly earns its redraw.
	PASS(errctx, akgl_draw_background(akgl_renderer, UIDEMO_WIDTH, UIDEMO_HEIGHT));
	score += 1;
    }

    PASS(errctx, akgl_ui_frame_begin());
    switch ( state ) {
    case UIDEMO_STATE_TITLE:
	PASS(errctx, declare_title());
	break;
    case UIDEMO_STATE_OPTIONS:
	PASS(errctx, declare_options());
	break;
    case UIDEMO_STATE_PLAY:
	PASS(errctx, declare_play());
	break;
    default:
	break;
    }
    PASS(errctx, akgl_ui_frame_end(akgl_renderer));

    if ( (shotpath != NULL) && (frameno == shotframe) ) {
	PASS(errctx, save_screenshot(shotpath));
    }
    PASS(errctx, akgl_renderer->frame_end(akgl_renderer));

    // The press edge lives exactly one frame: the declarations above have
    // seen it, so the next frame starts clean.
    clicked = false;

    // Menu activation is read after the frame, so it catches both routes in:
    // Return through akgl_ui_menu_handle_event before the declarations, and a
    // click through the declaration itself.
    if ( title_menu.activated ) {
	title_menu.activated = false;
	switch ( title_menu.selected ) {
	case 0:
	    state = UIDEMO_STATE_PLAY;
	    score = 0;
	    dialog_open = false;
	    break;
	case 1:
	    state = UIDEMO_STATE_OPTIONS;
	    break;
	case 2:
	default:
	    running = false;
	    break;
	}
    }
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Give back what has to be given back, in the order that works.
 *
 * akgl_ui_shutdown first because it is the newest thing up, then the fonts
 * **before** SDL_Quit -- the registry the fonts live in is an SDL property
 * set, and SDL_Quit destroys it with them inside. The pools and the clay
 * arena are static storage; there is nothing to free.
 */
static void teardown(void)
{
    IGNORE(akgl_ui_shutdown());
    IGNORE(akgl_text_unloadallfonts());
    if ( akgl_window != NULL ) {
	SDL_DestroyWindow(akgl_window);
	akgl_window = NULL;
    }
    SDL_Quit();
}

int main(int argc, char *argv[])
{
    PREPARE_ERROR(errctx);
    long frameno = 0;
    uint64_t started = 0;
    uint64_t spent = 0;

    ATTEMPT {
	CATCH(errctx, parse_args(argc, argv));
	CATCH(errctx, startup());

	// The loop is the last thing in this ATTEMPT block on purpose: CATCH
	// reports failure by `break`ing out of the loop, which falls straight
	// into CLEANUP. See examples/jrpg/jrpg.c for the long form of this
	// note.
	while ( running ) {
	    started = SDL_GetTicks();
	    CATCH(errctx, frame(frameno));
	    frameno += 1;
	    if ( (frame_limit > 0) && (frameno >= frame_limit) ) {
		running = false;
	    }
	    if ( demo == false ) {
		spent = SDL_GetTicks() - started;
		if ( spent < UIDEMO_FRAME_BUDGET_MS ) {
		    SDL_Delay((uint32_t)(UIDEMO_FRAME_BUDGET_MS - spent));
		}
	    }
	}
    } CLEANUP {
	teardown();
    } PROCESS(errctx) {
    } HANDLE_DEFAULT(errctx) {
	LOG_ERROR_WITH_MESSAGE(errctx, "the UI demo could not finish");
	exitstatus = 1;
    } FINISH_NORETURN(errctx);

    // Enough for the smoke run to be read rather than merely passed: the
    // script quits from the title menu, so a run that finished its tour says
    // so here.
    printf("uidemo: %ld frames, score %d, music %s, sound %s\n",
	   frameno, score, opt_music ? "on" : "off", opt_sound ? "on" : "off");
    return exitstatus;
}
