/**
 * @file game.c
 * @brief Implements the game subsystem.
 */

#include <SDL3/SDL.h>
#include <SDL3_image/SDL_image.h>
#include <SDL3_mixer/SDL_mixer.h>
#include <SDL3_ttf/SDL_ttf.h>
#include <stdio.h>
#include <akerror.h>
#include <semver.h>

#include <akstdlib.h>
#include <akgl/game.h>

#include <akgl/controller.h>
#include <akgl/tilemap.h>
#include <akgl/sprite.h>
#include <akgl/heap.h>
#include <akgl/registry.h>
#include <akgl/staticstring.h>
#include <akgl/iterator.h>
#include <akgl/physics.h>
#include <akgl/error.h>
#include <akgl/SDL_GameControllerDB.h>

SDL_Window *akgl_window = NULL;

// Currently active objects
akgl_RenderBackend *akgl_renderer;
akgl_PhysicsBackend *akgl_physics;
SDL_FRect *akgl_camera;
akgl_Tilemap *akgl_gamemap;

// Default objects
akgl_RenderBackend akgl_default_renderer;
akgl_PhysicsBackend akgl_default_physics;
SDL_FRect akgl_default_camera;
akgl_Tilemap akgl_default_gamemap;

MIX_Audio *akgl_bgm = NULL;
MIX_Mixer *akgl_mixer = NULL;
MIX_Track *akgl_tracks[AKGL_GAME_AUDIO_MAX_TRACKS];
akgl_Game akgl_game;

static akerr_ErrorContext *write_exact(const void *ptr, size_t size, size_t nmemb, FILE *fp)
{
    size_t transferred;

    return aksl_fwrite(ptr, size, nmemb, fp, &transferred);
}

/*
 * The four savegame name tables are written and read at each object's own
 * maximum name length -- AKGL_ACTOR_MAX_NAME_LENGTH for actors,
 * AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH for spritesheets, and so on.
 *
 * **The writer and the reader have to name the same constant per table.** The
 * tables carry no length prefix, so the reader finds each next entry by
 * stepping that many bytes. They disagreed until 0.5.0: the writer used each
 * object's own length and the reader used AKGL_ACTOR_MAX_NAME_LENGTH for all
 * four. The other three are 128 as well, so only the spritesheet table was
 * wrong -- and that was enough. See the EOF check at the end of akgl_game_load,
 * which is what turns a disagreement into an error instead of a corruption.
 */

/**
 * @brief Widest name field any of the save tables writes.
 *
 * The spritesheet table is the widest at #AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH;
 * the other three are name lengths half that or less.
 */
#define AKGL_GAME_SAVE_MAX_NAME_FIELD   AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH

/**
 * @brief Fail the build if a table ever declares a field wider than the staging buffer.
 *
 * A negative array size is a compile error, so widening one of these constants
 * without widening #AKGL_GAME_SAVE_MAX_NAME_FIELD cannot get past the compiler
 * and turn back into the overread this buffer exists to prevent.
 */
typedef char akgl_game_save_name_field_fits[
    ((AKGL_ACTOR_MAX_NAME_LENGTH <= AKGL_GAME_SAVE_MAX_NAME_FIELD) &&
     (AKGL_SPRITE_MAX_NAME_LENGTH <= AKGL_GAME_SAVE_MAX_NAME_FIELD) &&
     (AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH <= AKGL_GAME_SAVE_MAX_NAME_FIELD) &&
     (AKGL_CHARACTER_MAX_NAME_LENGTH <= AKGL_GAME_SAVE_MAX_NAME_FIELD)) ? 1 : -1];

/**
 * @brief Write a registry key as a fixed-width, zero-padded field.
 *
 * The name tables are read back without a length prefix, so every entry has to
 * occupy exactly @p width bytes. Writing @p width bytes *from the key itself*
 * is what the four iterators used to do, and the key is an SDL allocation sized
 * to the name -- 40 bytes for `benchactor0`. That read past the end of it on
 * every entry, and put whatever was next to it in the heap into the save file:
 * up to half a kilobyte of this process's memory per registered object, in a
 * file a player might send to somebody else.
 *
 * Staging through a zeroed buffer costs one `memset` per entry and makes the
 * padding deterministic, which the reader wanted anyway.
 *
 * @param name  The registry key. Required, and NUL-terminated -- it comes from
 *              SDL's property table, which stores C strings.
 * @param width Field width in bytes, including the padding. Required to be at
 *              most #AKGL_GAME_SAVE_MAX_NAME_FIELD; the compile-time check
 *              above is what keeps that true for the four call sites.
 * @param fp    Open output stream. Required.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKERR_NULLPOINTER If @p name or @p fp is `NULL`.
 * @throws AKERR_OUTOFBOUNDS If @p width exceeds the staging buffer.
 */
static akerr_ErrorContext *write_name_field(const char *name, size_t width, FILE *fp)
{
    char field[AKGL_GAME_SAVE_MAX_NAME_FIELD];

    PREPARE_ERROR(errctx);
    FAIL_ZERO_RETURN(errctx, name, AKERR_NULLPOINTER, "NULL name");
    FAIL_ZERO_RETURN(errctx, fp, AKERR_NULLPOINTER, "NULL file pointer");
    FAIL_NONZERO_RETURN(
	errctx,
	(width > sizeof(field)),
	AKERR_OUTOFBOUNDS,
	"Name field width %zu exceeds the staging buffer (%zu)",
	width,
	sizeof(field));

    memset(&field, 0x00, sizeof(field));
    PASS(errctx, aksl_strncpy((char *)&field, sizeof(field), name, (width - 1)));
    PASS(errctx, write_exact((char *)&field, 1, width, fp));
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext *read_exact(void *ptr, size_t size, size_t nmemb, FILE *fp)
{
    size_t transferred;

    return aksl_fread(ptr, size, nmemb, fp, &transferred);
}

/**
 * @brief Require that @p fp has nothing left in it.
 *
 * A helper rather than three lines at the call site, because "read a byte and
 * succeed only if that fails in one specific way" inverts the sense of every
 * other call in this file, and doing it inline needs a nested `ATTEMPT` whose
 * `break` binds to the wrong `switch`.
 *
 * `fgetc(3)` is not used here. It spells "end of file" and "the read failed"
 * with the same `EOF`, so a disk error at this point read as a clean end and
 * passed the check it was supposed to fail. `aksl_fgetc` tells the two apart:
 * `AKERR_EOF` is the outcome this wants and is the only one swallowed.
 *
 * @param fp Open input stream. Required.
 * @return `NULL` at end of file, otherwise an error context owned by the caller.
 * @throws AKERR_NULLPOINTER If @p fp is `NULL`.
 * @throws AKERR_IO If a byte was read, naming it.
 * @throws Whatever `aksl_fgetc` raises on a read error.
 */
static akerr_ErrorContext *require_at_eof(FILE *fp)
{
    int trailing = 0;

    PREPARE_ERROR(errctx);
    FAIL_ZERO_RETURN(errctx, fp, AKERR_NULLPOINTER, "NULL file pointer");

    ATTEMPT {
	CATCH(errctx, aksl_fgetc(fp, &trailing));
	FAIL_BREAK(
	    errctx,
	    AKERR_IO,
	    "Savegame has data left after its name tables (first trailing byte 0x%02x); "
	    "a name field width does not match the writer's",
	    (trailing & 0xFF));
    } CLEANUP {
    } PROCESS(errctx) {
    } HANDLE(errctx, AKERR_EOF) {
	// The whole point: nothing left to read is what this asks for.
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

void akgl_game_lowfps(void)
{
    SDL_Log("Low FPS! %d", akgl_game.fps);
    return;
}

akerr_ErrorContext *akgl_game_init(void)
{
    int i = 0;
    PREPARE_ERROR(errctx);
    // First, before anything that can raise: everything below reports through
    // AKGL_ERR_* codes, and a code raised before its name is registered prints
    // as "Unknown Error" in the stack trace the caller is left holding.
    PASS(errctx, akgl_error_init());
    PASS(errctx, aksl_strncpy(
	     (char *)&akgl_game.libversion,
	     sizeof(akgl_game.libversion),
	     AKGL_VERSION,
	     sizeof(akgl_game.libversion) - 1));
    akgl_game.gameStartTime = SDL_GetTicksNS();
    akgl_game.lastIterTime = akgl_game.gameStartTime;
    akgl_game.lastFPSTime = akgl_game.gameStartTime;
    akgl_game.lowfpsfunc = &akgl_game_lowfps;
    akgl_game.statelock = SDL_CreateMutex();
    FAIL_ZERO_RETURN(errctx, akgl_game.statelock, AKGL_ERR_SDL, "%s", SDL_GetError());
    PASS(errctx, akgl_game_state_lock());
    FAIL_ZERO_RETURN(errctx, strlen((char *)&akgl_game.name), AKERR_NULLPOINTER, "Must provide game name");
    FAIL_ZERO_RETURN(errctx, strlen((char *)&akgl_game.version), AKERR_NULLPOINTER, "Must provide game version");
    FAIL_ZERO_RETURN(errctx, strlen((char *)&akgl_game.uri), AKERR_NULLPOINTER, "Must provide game uri");
    PASS(errctx, akgl_heap_init());
    PASS(errctx, akgl_registry_init_actor());
    PASS(errctx, akgl_registry_init_sprite());
    PASS(errctx, akgl_registry_init_spritesheet());
    PASS(errctx, akgl_registry_init_character());
    PASS(errctx, akgl_registry_init_font());
    PASS(errctx, akgl_registry_init_music());
    PASS(errctx, akgl_registry_init_properties());
    PASS(errctx, akgl_registry_init_actor_state_strings());

    SDL_SetAppMetadata(akgl_game.name, akgl_game.version, akgl_game.uri);

    for ( i = 0 ; i < AKGL_MAX_CONTROL_MAPS; i++ ) {
	memset(&akgl_controlmaps[i], 0x00, sizeof(akgl_ControlMap));
    }

    FAIL_ZERO_RETURN(
	errctx,
	SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMEPAD | SDL_INIT_AUDIO),
	AKGL_ERR_SDL,
	"Couldn't initialize SDL: %s",
	SDL_GetError());

    // Load the Game Controller DB

    for ( i = 0; i < AKGL_SDL_GAMECONTROLLER_DB_LEN ; i++ ) {
	if ( SDL_AddGamepadMapping(SDL_GAMECONTROLLER_DB[i]) == -1 ) {
	    FAIL_ZERO_RETURN(errctx, 0, AKGL_ERR_SDL, "%s", SDL_GetError());
	}
    }
    PASS(errctx, akgl_controller_open_gamepads());

    FAIL_ZERO_RETURN(
	errctx,
	MIX_Init(),
	AKGL_ERR_SDL,
	"Couldn't initialize audio: %s",
	SDL_GetError());
    akgl_mixer = MIX_CreateMixerDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, 0);
    FAIL_ZERO_RETURN(
	errctx,
	akgl_mixer,
	AKGL_ERR_SDL,
	"Unable to create mixer device: %s",
	SDL_GetError());

    FAIL_ZERO_RETURN(
	errctx,
	TTF_Init(),
	AKGL_ERR_SDL,
	"Couldn't initialize front engine: %s",
	SDL_GetError());

    akgl_camera = &akgl_default_camera;
    akgl_renderer = &akgl_default_renderer;
    akgl_physics = &akgl_default_physics;
    akgl_gamemap = &akgl_default_gamemap;

    PASS(errctx, akgl_game_state_unlock());
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_game_state_lock(void)
{
    PREPARE_ERROR(errctx);
    // Milliseconds, which is what SDL_Delay takes. This used to count against
    // AKGL_TIME_ONESEC_MS, a constant whose name said "one second in
    // milliseconds" and whose value was a thousand times that, so the budget
    // was roughly sixteen minutes rather than the one second documented.
    Uint32 waitedms = 0;

    while ( waitedms < AKGL_GAME_STATE_LOCK_BUDGET_MS ) {
	if ( SDL_TryLockMutex(akgl_game.statelock) == true ) {
	    SUCCEED_RETURN(errctx);
	}
	waitedms += AKGL_GAME_STATE_LOCK_RETRY_MS;
	SDL_Delay(AKGL_GAME_STATE_LOCK_RETRY_MS);
    }
    // SDL_GetError() after a failed SDL_TryLockMutex is stale or empty -- the
    // call reports contention by returning false, not by setting an error -- so
    // say what actually happened and keep SDL's text as trailing context.
    FAIL_RETURN(
	errctx,
	AKGL_ERR_SDL,
	"Gave up waiting %d ms for the game state lock: %s",
	AKGL_GAME_STATE_LOCK_BUDGET_MS,
	SDL_GetError());
}

akerr_ErrorContext *akgl_game_state_unlock(void)
{
    PREPARE_ERROR(errctx);
    SDL_UnlockMutex(akgl_game.statelock);
    SUCCEED_RETURN(errctx);
}

void akgl_game_update_fps(void)
{
    SDL_Time curTime;
    curTime = SDL_GetTicksNS();
    if ( (curTime - akgl_game.lastFPSTime) > AKGL_TIME_ONESEC_NS ) {
	akgl_game.fps = akgl_game.framesSinceUpdate;
	akgl_game.framesSinceUpdate = 0;
	akgl_game.lastFPSTime = curTime;
    }
    if ( akgl_game.fps < 30 ) {
	// Install the default if nobody has. Only akgl_game_init sets this, and
	// renderer.h documents the other path on purpose -- a host that owns
	// its own window calls akgl_render_2d_bind instead. Such an embedder
	// used to crash on frame one, because fps is 0 for the first second and
	// 0 is under the threshold.
	if ( akgl_game.lowfpsfunc == NULL ) {
	    akgl_game.lowfpsfunc = &akgl_game_lowfps;
	}
	akgl_game.lowfpsfunc();
    }
    akgl_game.framesSinceUpdate += 1;
    akgl_game.lastIterTime = curTime;
}

/*
 * entity name -> pointer map tables
 */

/**
 * @brief Write one actor's name and save-time address to the name table.
 *
 * An `SDL_EnumerateProperties` callback. The name is written at a fixed
 * #AKGL_ACTOR_MAX_NAME_LENGTH bytes so the reader can walk the table without a
 * length prefix, and the pointer is written raw -- it is not a pointer the
 * loader will ever dereference, only a key it maps back to the object that has
 * the same name next run.
 *
 * @param userdata The open output stream, as a `FILE *`. Required.
 * @param props    #AKGL_REGISTRY_ACTOR, supplied by SDL.
 * @param name     The actor's registry key. Read for
 *                 #AKGL_ACTOR_MAX_NAME_LENGTH bytes regardless of its actual
 *                 length, so a shorter key is read past its end.
 *
 * @warning Ends in `FINISH_NORETURN`: an unhandled error here logs a stack trace
 *          and then calls libakerror's unhandled-error handler, which by default
 *          **exits the process**. A failed write during a save terminates the
 *          game rather than returning an error.
 */
static void save_actorname_iterator(void *userdata, SDL_PropertiesID props, const char *name)
{
    FILE *fp = (FILE *)userdata;
    akgl_Actor *actor = NULL;
    PREPARE_ERROR(errctx);
    ATTEMPT {
	FAIL_ZERO_BREAK(errctx, fp, AKERR_NULLPOINTER, "NULL file pointer");
	CATCH(errctx, write_name_field(name, AKGL_ACTOR_MAX_NAME_LENGTH, fp));
	actor = SDL_GetPointerProperty(props, name, NULL);
	CATCH(errctx, write_exact(&actor, 1, sizeof(akgl_Actor *), fp));
    } CLEANUP {
    } PROCESS(errctx) {
    } FINISH_NORETURN(errctx);
}

/**
 * @brief Write one sprite's name and save-time address to the name table.
 *
 * As save_actorname_iterator, but the name field is
 * #AKGL_SPRITE_MAX_NAME_LENGTH wide.
 *
 * @param userdata The open output stream, as a `FILE *`. Required.
 * @param props    #AKGL_REGISTRY_SPRITE, supplied by SDL.
 * @param name     The sprite's registry key, written at fixed width.
 *
 * @warning Terminates the process on an unhandled error. See
 *          save_actorname_iterator.
 */
static void save_spritename_iterator(void *userdata, SDL_PropertiesID props, const char *name)
{
    FILE *fp = (FILE *)userdata;
    akgl_Sprite *sprite = NULL;
    PREPARE_ERROR(errctx);
    ATTEMPT {
	FAIL_ZERO_BREAK(errctx, fp, AKERR_NULLPOINTER, "NULL file pointer");
	sprite = SDL_GetPointerProperty(props, name, NULL);
	CATCH(errctx, write_name_field(name, AKGL_SPRITE_MAX_NAME_LENGTH, fp));
	CATCH(errctx, write_exact(&sprite, 1, sizeof(akgl_Sprite *), fp));
    } CLEANUP {
    } PROCESS(errctx) {
    } FINISH_NORETURN(errctx);
}

/**
 * @brief Write one spritesheet's name and save-time address to the name table.
 *
 * As save_actorname_iterator, but the name field is
 * #AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH wide -- the widest of the four, since
 * a spritesheet is keyed by its resolved path.
 *
 * @param userdata The open output stream, as a `FILE *`. Required.
 * @param props    #AKGL_REGISTRY_SPRITESHEET, supplied by SDL.
 * @param name     The spritesheet's registry key, written at fixed width.
 *
 * @warning Terminates the process on an unhandled error. See
 *          save_actorname_iterator.
 * @note This is the table the loader disagrees with: it reads every table at
 *       #AKGL_ACTOR_MAX_NAME_LENGTH, so a save containing any spritesheet cannot
 *       be read back. TODO.md, "Defects".
 */
static void save_spritesheetname_iterator(void *userdata, SDL_PropertiesID props, const char *name)
{
    FILE *fp = (FILE *)userdata;
    akgl_SpriteSheet *spritesheet = NULL;
    PREPARE_ERROR(errctx);
    ATTEMPT {
	FAIL_ZERO_BREAK(errctx, fp, AKERR_NULLPOINTER, "NULL file pointer");
	spritesheet = SDL_GetPointerProperty(props, name, NULL);
	CATCH(errctx, write_name_field(name, AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH, fp));
	CATCH(errctx, write_exact(&spritesheet, 1, sizeof(akgl_SpriteSheet *), fp));
    } CLEANUP {
    } PROCESS(errctx) {
    } FINISH_NORETURN(errctx);
}

/**
 * @brief Write one character's name and save-time address to the name table.
 *
 * As save_actorname_iterator, but the name field is
 * #AKGL_CHARACTER_MAX_NAME_LENGTH wide.
 *
 * @param userdata The open output stream, as a `FILE *`. Required.
 * @param props    #AKGL_REGISTRY_CHARACTER, supplied by SDL.
 * @param name     The character's registry key, written at fixed width.
 *
 * @warning Terminates the process on an unhandled error. See
 *          save_actorname_iterator.
 */
static void save_charactername_iterator(void *userdata, SDL_PropertiesID props, const char *name)
{
    FILE *fp = (FILE *)userdata;
    PREPARE_ERROR(errctx);
    akgl_Character *character = NULL;
    ATTEMPT {
	FAIL_ZERO_BREAK(errctx, fp, AKERR_NULLPOINTER, "NULL file pointer");
	character = SDL_GetPointerProperty(props, name, NULL);
	CATCH(errctx, write_name_field(name, AKGL_CHARACTER_MAX_NAME_LENGTH, fp));
	CATCH(errctx, write_exact(&character, 1, sizeof(akgl_Character *), fp));
    } CLEANUP {
    } PROCESS(errctx) {
    } FINISH_NORETURN(errctx);
}

akerr_ErrorContext *akgl_game_save_actors(FILE *fp)
{
    PREPARE_ERROR(errctx);
    // Each name table ends with a zeroed name field and a zeroed pointer, which
    // is what load_objectnamemap() looks for to stop reading. The
    // terminator has to come from a buffer at least as long as the longest name
    // field: writing N bytes from the address of a single char would emit N-1
    // bytes of whatever happened to follow it on the stack, which both leaks
    // stack contents into the save file and produces a sentinel the loader
    // cannot recognize.
    char nullbuf[AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH];

    memset((void *)&nullbuf, 0x00, sizeof(nullbuf));

    ATTEMPT {
	FAIL_ZERO_BREAK(errctx, fp, AKERR_NULLPOINTER, "NULL file pointer");
	// write the actor name pointer table
	SDL_EnumerateProperties(
	    AKGL_REGISTRY_ACTOR,
	    &save_actorname_iterator,
	    (void *)fp);
	CATCH(errctx, write_exact((void *)&nullbuf, 1, AKGL_ACTOR_MAX_NAME_LENGTH, fp));
	CATCH(errctx, write_exact((void *)&nullbuf, 1, sizeof(akgl_Actor *), fp));
	// write the sprite name pointer table
	SDL_EnumerateProperties(
	    AKGL_REGISTRY_SPRITE,
	    &save_spritename_iterator,
	    (void *)fp);
	CATCH(errctx, write_exact((void *)&nullbuf, 1, AKGL_SPRITE_MAX_NAME_LENGTH, fp));
	CATCH(errctx, write_exact((void *)&nullbuf, 1, sizeof(akgl_Sprite *), fp));
	// write the spritesheet name pointer table
	SDL_EnumerateProperties(
	    AKGL_REGISTRY_SPRITESHEET,
	    &save_spritesheetname_iterator,
	    (void *)fp);
	CATCH(errctx, write_exact((void *)&nullbuf, 1, AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH, fp));
	CATCH(errctx, write_exact((void *)&nullbuf, 1, sizeof(akgl_SpriteSheet *), fp));
	// write the character name pointer table
	SDL_EnumerateProperties(
	    AKGL_REGISTRY_CHARACTER,
	    &save_charactername_iterator,
	    (void *)fp);
	CATCH(errctx, write_exact((void *)&nullbuf, 1, AKGL_CHARACTER_MAX_NAME_LENGTH, fp));
	CATCH(errctx, write_exact((void *)&nullbuf, 1, sizeof(akgl_Character *), fp));
    } CLEANUP {
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_game_save(char *fpath)
{
    FILE *fp = NULL;
    FILE *tmpfp = NULL;
    PREPARE_ERROR(errctx);

    ATTEMPT {
	FAIL_ZERO_BREAK(errctx, fpath, AKERR_NULLPOINTER, "NULL file path");
	CATCH(errctx, aksl_fopen(fpath, "wb", &fp));
	CATCH(errctx, write_exact(&akgl_game, 1, sizeof(akgl_Game), fp));
	CATCH(errctx, akgl_game_save_actors(fp));
	// Closed here rather than in CLEANUP, because on this path the close is
	// part of the save. Every write above goes through aksl_fwrite and is
	// checked; the flush that those writes were buffered for happens in the
	// close, so a full disk, an exceeded quota or an NFS server going away
	// is reported *only* here. Ignoring it means akgl_game_save returns
	// success over a truncated savegame.
	//
	// The order matters: fclose(3) disassociates the stream whether or not
	// it succeeds, so `fp` is dead either way and has to be dropped
	// *before* the status is examined. Closing it again in CLEANUP is
	// undefined behaviour that no wrapper can detect -- written the other
	// way round, this aborted in glibc with "double free detected in
	// tcache" the first time a close actually failed.
	tmpfp = fp;
	fp = NULL;
	CATCH(errctx, aksl_fclose(tmpfp));
    } CLEANUP {
	// CLEANUP must precede PROCESS: with the two transposed, the fclose
	// lands inside the PROCESS switch and only runs when an error context
	// exists and reports success, so an ordinary save never flushed or
	// closed its stream.
	//
	// Only the failure path reaches this with a stream still open, and it
	// is already carrying an error the caller needs more than a close
	// status, so the descriptor is released without asking how it went.
	if ( fp != NULL ) {
	    IGNORE(aksl_fclose(fp));
	}
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx); // SUCCEED_NORETURN if in main().
}

/**
 * @brief Read one name table back, building an old-address to current-object map.
 *
 * This is the mechanism that makes a save file portable across runs. Objects are
 * serialized carrying the addresses they had at save time; those addresses mean
 * nothing now, but the table says which *name* lived at each one, and the name
 * still resolves through the registry. So each entry becomes
 * `"0x7f..." -> current object`, and a deserialized pointer can be translated by
 * looking up its printed form.
 *
 * The keys are the old pointers rendered with `%p`, because SDL property sets
 * take string keys only.
 *
 * Reading stops at the terminator -- a zeroed name and a zeroed pointer.
 *
 * @param fp         The open save-game stream, positioned at the start of a
 *                   table. Required.
 * @param map        The property set to fill in. Required, and created by the
 *                   caller.
 * @param namelength Width of the table's name field, which must match what the
 *                   corresponding save iterator wrote. It also sizes a
 *                   variable-length array on the stack.
 * @param ptrlength  Width of the table's pointer field, i.errctx. `sizeof` the
 *                   pointer type that table holds.
 * @param registry   The registry to resolve names against -- #AKGL_REGISTRY_ACTOR
 *                   for the actor table, and so on.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKERR_EOF If the stream ends before a terminator is found, which is
 *         what a truncated save file looks like.
 * @throws AKERR_IO On a stream error.
 * @throws AKERR_NULLPOINTER If @p fp is `NULL`, by way of the read wrapper.
 *
 * @note A name in the table that is no longer in @p registry maps to `NULL`
 *       rather than being reported: the entry is written with whatever
 *       `SDL_GetPointerProperty` returned.
 */
static akerr_ErrorContext *load_objectnamemap(FILE *fp, SDL_PropertiesID map, int namelength, int ptrlength, SDL_PropertiesID registry)
{
    void *ptr = NULL;
    char ptrstring[32];
    int ptrstringlen = 0;
    char objname[namelength];
    bool done = false;

    PREPARE_ERROR(errctx);
    // The ATTEMPT block sits inside the loop on purpose. CATCH reports a
    // failure by breaking, and a break binds to the innermost enclosing switch
    // or loop, so a CATCH written directly inside `while` would leave the loop
    // and then fall through to SUCCEED_RETURN -- reporting a truncated or
    // corrupt name table as a successful load.
    while ( done == false ) {
	ATTEMPT {
	    CATCH(errctx, read_exact((void *)&objname, 1, namelength, fp));
	    CATCH(errctx, read_exact((void *)&ptr, 1, ptrlength, fp));
	    // End of the map
	    if ( ptr == 0x00 && objname[0] == 0x00 ) {
		done = true;
		break;
	    }
	    // The map allows us to say "Object X has a reference to object Y at
	    // address Z. The object they had at address Z was named A. Our current
	    // instance of object named A is at address B. So we map address Z to
	    // address B, so that we can reconnect function pointers on objects loaded
	    // from the save game state."

	    // SDL_Properties objects can only use string keys, so we can't use the
	    // old pointer as a key without first converting it to a string.
	    CATCH(errctx, aksl_memset((void *)&ptrstring, 0x00, sizeof(ptrstring)));
	    CATCH(errctx, aksl_snprintf(
		      &ptrstringlen,
		      (char *)&ptrstring,
		      sizeof(ptrstring),
		      "%p",
		      ptr));
	    SDL_SetPointerProperty(
		map,
		ptrstring,
		SDL_GetPointerProperty(registry, objname, NULL));
	} CLEANUP {
	} PROCESS(errctx) {
	} FINISH(errctx, true);
    };
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_game_load_versioncmp(char *versiontype, char *newversion, char *curversion)
{
    semver_t current_version = {};
    semver_t compare_version = {};
    PREPARE_ERROR(errctx);
    FAIL_ZERO_RETURN(errctx, versiontype, AKERR_NULLPOINTER, "NULL argument");
    FAIL_ZERO_RETURN(errctx, curversion, AKERR_NULLPOINTER, "NULL argument");
    FAIL_ZERO_RETURN(errctx, newversion, AKERR_NULLPOINTER, "NULL argument");

    ATTEMPT {
	// Check save game library version
	FAIL_NONZERO_BREAK(
	    errctx,
	    semver_parse((const char *)curversion, &current_version),
	    AKERR_VALUE,
	    "Invalid semantic %s version in current game: %s",
	    versiontype,
	    (char *)curversion);
	FAIL_NONZERO_BREAK(
	    errctx,
	    semver_parse((const char *)newversion, &compare_version),
	    AKERR_VALUE,
	    "Invalid semantic %s version in save game: %s",
	    versiontype,
	    (char *)&newversion);
	FAIL_ZERO_BREAK(
	    errctx,
	    semver_satisfies(compare_version, current_version, "="),
	    AKERR_API,
	    "Incompatible save game %s version (%s != %s)",
	    versiontype,
	    curversion,
	    (char *)&newversion);
    } CLEANUP {
	semver_free(&current_version);
	semver_free(&compare_version);
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_game_load(char *fpath)
{
    akgl_Game savegame;
    SDL_PropertiesID actormap;
    SDL_PropertiesID spritemap;
    SDL_PropertiesID spritesheetmap;
    SDL_PropertiesID charactermap;
    FILE *fp = NULL;

    PREPARE_ERROR(errctx);
    FAIL_ZERO_RETURN(errctx, fpath, AKERR_NULLPOINTER, "NULL file path");

    ATTEMPT {
	CATCH(errctx, aksl_fopen(fpath, "rb", &fp));
	CATCH(errctx, read_exact((void *)&savegame, 1, sizeof(akgl_Game), fp));
	CATCH(errctx, akgl_game_load_versioncmp("library", (char *)&savegame.libversion, (char *)AKGL_VERSION));
	CATCH(errctx, akgl_game_load_versioncmp("game", (char *)&savegame.version, (char *)&akgl_game.version));
	FAIL_NONZERO_BREAK(
	    errctx,
	    strncmp((char *)&savegame.name, (char *)&akgl_game.name, 256),
	    AKERR_API,
	    "Savegame is not compatible with this game");
	FAIL_NONZERO_BREAK(
	    errctx,
	    strncmp((char *)&savegame.uri, (char *)&akgl_game.uri, 256),
	    AKERR_API,
	    "Savegame is not compatible with this game");

	memcpy((void *)&akgl_game, (void *)&savegame, sizeof(akgl_Game));
	// Load actor name map
	actormap = SDL_CreateProperties();
	CATCH(errctx, load_objectnamemap(fp, actormap, AKGL_ACTOR_MAX_NAME_LENGTH, sizeof(akgl_Actor *), AKGL_REGISTRY_ACTOR));
	// Load sprite name map
	spritemap = SDL_CreateProperties();
	CATCH(errctx, load_objectnamemap(fp, spritemap, AKGL_SPRITE_MAX_NAME_LENGTH, sizeof(akgl_Sprite *), AKGL_REGISTRY_SPRITE));
	// Load spritesheet name map
	spritesheetmap = SDL_CreateProperties();
	CATCH(errctx, load_objectnamemap(fp, spritesheetmap, AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH, sizeof(akgl_SpriteSheet *), AKGL_REGISTRY_SPRITESHEET));
	// Load character name map
	charactermap = SDL_CreateProperties();
	CATCH(errctx, load_objectnamemap(fp, charactermap, AKGL_CHARACTER_MAX_NAME_LENGTH, sizeof(akgl_Character *), AKGL_REGISTRY_CHARACTER));

	// The four tables are the whole file, so the stream has to be at its end
	// now. Nothing else checks: the tables carry no length prefix and end at
	// a zeroed sentinel, so a reader whose field width disagrees with the
	// writer's does not run off anything -- it finds a run of zeros
	// somewhere inside an entry, stops early, and reports success with
	// silently wrong maps. This is what makes that disagreement an error
	// rather than a corruption, and it is the check that would have caught
	// the spritesheet width being wrong for however long it was.
	//
	// It also has to move once the objects themselves are written; the
	// comment below is the marker for that.
	CATCH(errctx, require_at_eof(fp));

	// Now that we have all of our pointer maps built, we can load the actual binary objects and reset their pointers
    } CLEANUP {
	// Nothing was written, so unlike akgl_game_save there is no buffered
	// data a failed close could lose. Releasing the descriptor is all this
	// has to do.
	if ( fp != NULL ) {
	    IGNORE(aksl_fclose(fp));
	}
    } PROCESS(errctx) {
    } FINISH(errctx, true);

    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_game_update(akgl_Iterator *opflags)
{
    PREPARE_ERROR(errctx);
    // No AKGL_ITERATOR_OP_LAYERMASK by default: updating every actor once is
    // the whole job, and restricting the sweep to one layer is the caller
    // deliberately asking for less.
    akgl_Iterator defflags = {
	.flags = AKGL_ITERATOR_OP_UPDATE,
	.layerid = 0
    };
    akgl_Actor *actor = NULL;

    if ( opflags == NULL ) {
	opflags = &defflags;
    }

    PASS(errctx, akgl_game_state_lock());

    akgl_game_update_fps();

    // One sweep, not one per layer. This loop used to sit inside a walk over
    // AKGL_TILEMAP_MAX_LAYERS that never compared actor->layer to the layer it
    // was on, so every live actor's updatefunc ran sixteen times a frame --
    // measured at 68.4 ns per update and 64 actors, 70 us of work to do 4.4 us
    // of it. Updating an actor is not a per-layer operation. Drawing is, and
    // akgl_render_2d_draw_world walks the layers itself.
    for ( int j = 0; j < AKGL_MAX_HEAP_ACTOR; j++ ) {
	actor = &akgl_heap_actors[j];
	if ( actor->refcount == 0 ) {
	    continue;
	}
	// The flag is honoured now rather than ignored. A caller that sets it
	// gets exactly the actors on opflags->layerid, once each.
	if ( AKGL_BITMASK_HAS(opflags->flags, AKGL_ITERATOR_OP_LAYERMASK) &&
	     (actor->layer != opflags->layerid) ) {
	    continue;
	}
	if ( AKGL_BITMASK_HAS(opflags->flags, AKGL_ITERATOR_OP_TILEMAPSCALE) ) {
	    PASS(errctx, akgl_tilemap_scale_actor(akgl_gamemap, actor));
	} else {
	    actor->scale = 1.0;
	}
	PASS(errctx, actor->updatefunc(actor));
    }
    PASS(errctx, akgl_physics->simulate(akgl_physics, NULL));
    PASS(errctx, akgl_renderer->draw_world(akgl_renderer, NULL));
    PASS(errctx, akgl_game_state_unlock());
    SUCCEED_RETURN(errctx);
}
