/**
 * @file jrpg.c
 * @brief A small top-down JRPG: startup, the frame loop, and teardown.
 *
 * Chapter 20 of the manual quotes this program rather than restating it, so
 * what is here is what the chapter teaches. Run it with no arguments for a
 * window you can walk around in:
 *
 *     ./examples/jrpg/jrpg
 *
 * Arrow keys walk, space talks to whoever is standing next to you and dismisses
 * the box again. `--frames N` stops after N frames, which is what makes this
 * runnable as a headless smoke test; `--demo` drives the arrow keys from a
 * script and steps the physics clock by a fixed interval so the run is
 * deterministic and takes no wall-clock time.
 */

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

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

#include <akgl/controller.h>
#include <akgl/error.h>
#include <akgl/game.h>
#include <akgl/iterator.h>
#include <akgl/physics.h>
#include <akgl/registry.h>
#include <akgl/renderer.h>
#include <akgl/text.h>
#include <akgl/tilemap.h>

#include "jrpg.h"

/** @brief Nanoseconds in one 60 Hz step. What `--demo` advances the clock by. */
#define JRPG_FIXED_STEP_NS  (AKGL_TIME_ONESEC_NS / 60)
/** @brief Milliseconds one 60 Hz frame is allowed to take, for the interactive loop. */
#define JRPG_FRAME_BUDGET_MS 16

/*
 * The scripted walk `--demo` drives. The player starts at (224, 256) and the
 * elder stands at (272, 112), so this is: right for a second, up for two and a
 * bit, talk, try to walk while frozen, dismiss, walk away. Every one of those
 * is a path the interactive game has and a headless run would otherwise never
 * touch.
 *
 *  Frame   Key           Down     What it is for
 */
static const jrpg_ScriptStep JRPG_DEMO_SCRIPT[] = {
    {   10,  SDLK_RIGHT,  true  },  /* the per-facing walk animation */
    {   70,  SDLK_RIGHT,  false },
    {   75,  SDLK_UP,     true  },  /* up the map, past the buildings */
    {  205,  SDLK_UP,     false },
    {  215,  SDLK_SPACE,  true  },  /* the elder is in range: open the box */
    {  216,  SDLK_SPACE,  false },
    {  225,  SDLK_LEFT,   true  },  /* frozen: AKGL_ERR_LOGICINTERRUPT eats this */
    {  245,  SDLK_LEFT,   false },
    {  255,  SDLK_SPACE,  true  },  /* dismiss */
    {  256,  SDLK_SPACE,  false },
    {  265,  SDLK_DOWN,   true  },  /* and walk away */
    {  285,  SDLK_DOWN,   false }
};

#define JRPG_DEMO_STEPS (sizeof(JRPG_DEMO_SCRIPT) / sizeof(JRPG_DEMO_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 Replacement `akgl_game.lowfpsfunc`: say it once, not sixty times a second.
 *
 * akgl_game.fps is a completed-second average, so it reads 0 for the first
 * second of every process -- which is under the threshold, so the default hook
 * logs a line on every frame until the first second is up. The hook exists to
 * be replaced; a real game sheds work here rather than talking about it.
 */
static void lowfps_quiet(void)
{
    if ( lowfps_warned == false ) {
	lowfps_warned = true;
	SDL_Log("Frame rate is under 30 and this game does nothing about it");
    }
}

/**
 * @brief Read `--frames N` and `--demo` 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 `--frames` 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 frames = 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], &frames));
	    frame_limit = frames;
	} 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], &frames));
	    shotframe = frames;
	} else {
	    FAIL_RETURN(errctx, AKERR_VALUE,
			"usage: jrpg [--frames N] [--demo]"
			" [--screenshot PATH] [--screenshot-frame N]");
	}
    }
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Bring the library up, load the town, and bind the controls.
 *
 * The order below is the one game.h documents and the only one that works.
 * Three things in it are not obvious and are what chapter 20 spends its time
 * on: the properties have to be set between akgl_game_init and
 * akgl_render_2d_init, because that is what reads them; the physics backend has
 * to be initialized by hand, because akgl_game_init does not; and the assets
 * have to be loaded sprites-then-characters-then-map.
 *
 * @return `NULL` on success, otherwise an error context owned by the caller.
 */
static akerr_ErrorContext *startup(void)
{
    PREPARE_ERROR(errctx);
    akgl_Control talk;

    // Required before akgl_game_init, which refuses to run without all three:
    // the window title, SDL's application metadata and the savegame
    // compatibility check are built from them.
    PASS(errctx, aksl_strncpy(akgl_game.name, sizeof(akgl_game.name), "libakgl JRPG tutorial", 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.jrpg", sizeof(akgl_game.uri) - 1));

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

    PASS(errctx, akgl_set_property("game.screenwidth", JRPG_SCREEN_WIDTH));
    PASS(errctx, akgl_set_property("game.screenheight", JRPG_SCREEN_HEIGHT));
    PASS(errctx, akgl_render_2d_init(akgl_renderer));

    // akgl_game_init points akgl_physics at akgl_default_physics and stops
    // there. That storage is BSS, so all four of its method pointers are NULL,
    // and akgl_game_update calls `akgl_physics->simulate(...)` without checking
    // it: a NULL function pointer, and a segmentation fault on frame one rather
    // than an error context.
    //
    // town.tmj happens to declare its own physics, and jrpg_world_load switches
    // to the backend that builds -- so removing this line alone leaves *this*
    // program working, and removing it along with that switch is a SIGSEGV on
    // frame one. Measured, by removing both. This line is what makes the
    // program correct for a map that declares no physics, which is most of
    // them. Nothing calls the factory for you either way. A top-down game wants
    // the arcade backend with no gravity, which is what the property defaults
    // already give.
    PASS(errctx, akgl_physics_init_arcade(akgl_physics));

    PASS(errctx, akgl_text_loadfont(JRPG_FONT_NAME, JRPG_FONT_FILE, JRPG_FONT_SIZE));

    PASS(errctx, jrpg_world_load());
    PASS(errctx, jrpg_world_populate());

    // The arrow keys and the D-pad, wired to the akgl_actor_cmhf_* pairs, in
    // one call. Keyboard id 0 and gamepad id 0 mean "the first of each".
    PASS(errctx, akgl_controller_default(0, "player", 0, 0));

    // And one binding of our own on the same map. A binding is a struct, copied
    // in, so a stack local is fine. handler_off has to be non-NULL --
    // akgl_controller_handle_event does not check it before calling it -- and
    // the button is set to something no gamepad reports, because the gamepad
    // and keyboard arms of the match are evaluated together.
    PASS(errctx, aksl_memset((void *)&talk, 0x00, sizeof(talk)));
    talk.event_on = SDL_EVENT_KEY_DOWN;
    talk.event_off = SDL_EVENT_KEY_UP;
    talk.key = SDLK_SPACE;
    talk.button = (uint8_t)SDL_GAMEPAD_BUTTON_INVALID;
    talk.handler_on = &jrpg_cmhf_talk;
    talk.handler_off = &jrpg_cmhf_ignore;
    PASS(errctx, akgl_controller_pushmap(0, &talk));

    SUCCEED_RETURN(errctx);
}

/**
 * @brief Feed the scripted keystrokes due on this frame into the control maps.
 *
 * Synthesized `SDL_Event`s rather than direct calls to the handlers, so the
 * demo goes through akgl_controller_handle_event, the control map lookup and
 * the binding -- the same path a real key takes. A test that skipped that would
 * not be testing the thing that breaks.
 *
 * @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 < JRPG_DEMO_STEPS; i++ ) {
	if ( JRPG_DEMO_SCRIPT[i].frame != frameno ) {
	    continue;
	}
	PASS(errctx, aksl_memset((void *)&event, 0x00, sizeof(event)));
	if ( JRPG_DEMO_SCRIPT[i].down ) {
	    event.type = SDL_EVENT_KEY_DOWN;
	} else {
	    event.type = SDL_EVENT_KEY_UP;
	}
	event.key.which = 0;
	event.key.key = JRPG_DEMO_SCRIPT[i].key;
	PASS(errctx, akgl_controller_handle_event((void *)&akgl_game.state, &event));
    }
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Read the render target back and write it out as a PNG.
 *
 * Called between the box being drawn and the frame being presented, because
 * SDL_RenderPresent is where the target stops being readable. This is how the
 * figure in chapter 21 is generated: it is output from this program rather than
 * a picture somebody took once, so it cannot show a version of the game that no
 * longer exists.
 */
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, camera, the library's own update, the box, present.
 *
 * akgl_game_update is update-every-actor, step the physics, draw the world. It
 * does *not* clear or present the target, so the frame is bracketed by the
 * backend's own frame_start and frame_end, and anything drawn between the
 * update and frame_end lands on top of the world.
 *
 * @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;
    akgl_Iterator opflags = {
	.flags = AKGL_ITERATOR_OP_UPDATE,
	.layerid = 0
    };

    while ( SDL_PollEvent(&event) ) {
	if ( event.type == SDL_EVENT_QUIT ) {
	    running = false;
	}
	PASS(errctx, akgl_controller_handle_event((void *)&akgl_game.state, &event));
    }
    if ( demo ) {
	PASS(errctx, demo_step(frameno));
	// Drive the clock rather than sleeping on it. akgl_physics_simulate
	// measures dt from gravity_time, which is a public field, so setting it
	// one fixed step into the past makes every frame worth exactly 1/60 of
	// a second of simulated time no matter how fast the loop actually runs.
	// tests/physics_sim.c does the same, for the same reason: a simulated
	// second should not cost a real one.
	akgl_physics->gravity_time = SDL_GetTicksNS() - JRPG_FIXED_STEP_NS;
    }

    PASS(errctx, jrpg_camera_follow());

    PASS(errctx, akgl_renderer->frame_start(akgl_renderer));
    // Every failure path out of akgl_game_update returns with the game state
    // mutex still held. SDL's mutexes are recursive and this loop is single
    // threaded, so the next frame would not deadlock -- but it would be running
    // on top of a frame that did not finish. Treat a failed frame as terminal.
    PASS(errctx, akgl_game_update(&opflags));
    PASS(errctx, jrpg_textbox_draw());
    if ( (shotpath != NULL) && (frameno == shotframe) ) {
	PASS(errctx, save_screenshot(shotpath));
    }
    PASS(errctx, akgl_renderer->frame_end(akgl_renderer));

    SUCCEED_RETURN(errctx);
}

/**
 * @brief Give back what has to be given back, in the order that works.
 *
 * There is no `akgl_game_shutdown`; teardown is the caller's. Two of the three
 * lines here are load-bearing and the third is a deliberate omission:
 *
 * - akgl_text_unloadallfonts must run **before** `SDL_Quit`. Fonts are kept in
 *   an SDL property registry, and `SDL_Quit` destroys the registry -- taking
 *   the last reference to every font still in it with no way left to close
 *   them.
 * - akgl_tilemap_release is **not** called. Its layer loop destroys
 *   `tilesets[i].texture` rather than `layers[i].texture`, so it double-frees
 *   every tileset texture and never frees an image layer's, and it NULLs
 *   nothing, so a second call is a use-after-free. TODO.md, "Defects". `SDL_Quit` reclaims the textures correctly; calling the
 *   function that is supposed to would be worse than not.
 * - The pools are static storage. There is nothing to free and the process is
 *   about to exit.
 */
static void teardown(void)
{
    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);
    akgl_Actor *player = NULL;
    long frameno = 0;
    uint64_t started = 0;
    uint64_t spent = 0;

    ATTEMPT {
	CATCH(errctx, parse_args(argc, argv));
	CATCH(errctx, startup());
	// Taken here rather than after FINISH: the registry is an SDL property
	// set and `SDL_Quit` in teardown() destroys it, so a lookup afterwards
	// finds nothing. The actor itself is in a static pool and outlives both.
	player = SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, "player", NULL);

	// The loop is the last thing in this ATTEMPT block on purpose. CATCH
	// reports failure by `break`ing, and a `break` inside a loop binds to
	// the loop rather than to the block -- so a failing frame leaves the
	// loop and falls straight into CLEANUP, which is what is wanted. Put
	// anything after this loop and it would run after a failure instead.
	// src/tilemap.c carries the same note over the same shape.
	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 < JRPG_FRAME_BUDGET_MS ) {
		    SDL_Delay((uint32_t)(JRPG_FRAME_BUDGET_MS - spent));
		}
	    }
	}
    } CLEANUP {
	teardown();
    } PROCESS(errctx) {
    } HANDLE_DEFAULT(errctx) {
	LOG_ERROR_WITH_MESSAGE(errctx, "the JRPG example could not finish");
	// Set a flag and act on it after FINISH. A bare `return` from inside a
	// HANDLE block leaves before RELEASE_ERROR and leaks the context's pool
	// slot; AGENTS.md spells that out, and an example is a bad place to
	// teach it wrong.
	exitstatus = 1;
    } FINISH_NORETURN(errctx);

    // Enough for the smoke run to be read rather than merely passed. A run that
    // exits 0 having drawn nothing looks exactly like one that worked.
    if ( player != NULL ) {
	printf("jrpg: %ld frames, player at (%.0f, %.0f)\n", frameno, player->x, player->y);
    } else {
	printf("jrpg: %ld frames, no player\n", frameno);
    }
    return exitstatus;
}
