/**
 * @file perf.c
 * @brief Stress and timing benchmarks for every subsystem that does not need a renderer.
 *
 * These are not correctness tests. Each one drives a hot path -- the pool scans,
 * the registry, the physics sweep, the per-actor update, the geometry helpers,
 * the JSON accessors -- hard enough that its per-operation cost is measurable,
 * and reports what that cost is. The companion suite in `tests/perf_render.c`
 * covers the paths that need a renderer.
 *
 * The pools are the reason this matters. Every acquire is a linear scan of a
 * fixed array looking for a zero reference count, so cost grows with how full
 * the pool is, not with how much is being asked for. Several benchmarks below
 * are therefore run twice: once against an empty pool, which is the number a
 * casual reading of the code predicts, and once against a nearly full one,
 * which is the number a game in its fifth minute actually gets.
 *
 * Everything here runs headless under the dummy drivers, and every benchmark
 * rebuilds the state it needs rather than inheriting whatever the previous one
 * left behind.
 *
 * @note SDL's log output is redirected to a sink that discards it. The library
 *       logs on paths this suite hammers -- akgl_actor_initialize logs every
 *       spawn -- and timing a write to a terminal or a CTest capture file
 *       measures the machine's I/O, not libakgl. What the *formatting* costs is
 *       measured deliberately, by the pair of actor-spawn benchmarks.
 */

#include <SDL3/SDL.h>
#include <string.h>
#include <jansson.h>
#include <akerror.h>

#include <akgl/error.h>
#include <akgl/game.h>
#include <akgl/heap.h>
#include <akgl/registry.h>
#include <akgl/actor.h>
#include <akgl/character.h>
#include <akgl/sprite.h>
#include <akgl/physics.h>
#include <akgl/staticstring.h>
#include <akgl/collision.h>
#include <akgl/util.h>
#include <akgl/json_helpers.h>
#include <akgl/tilemap.h>

#include "benchutil.h"
#include "testutil.h"

/** @brief How many actors the frame-shaped benchmarks put on the heap. */
#define BENCH_ACTOR_COUNT       AKGL_MAX_HEAP_ACTOR
/** @brief The JSON document the accessor benchmarks read. */
#define BENCH_JSON_FIXTURE      "assets/snippets/test_json_helpers.json"
/** @brief A path that exists relative to the CTest working directory, for akgl_path_relative. */
#define BENCH_PATH_FIXTURE      "assets/testcharacter.json"

/**
 * @brief Print the library's fixed memory footprint.
 *
 * Not a benchmark, but it belongs in the same report: libakgl does not call
 * `malloc`, so every one of these arrays exists from process start whether the
 * game uses one slot or all of them, and several of the timings above are
 * explained entirely by their size. The string pool is a megabyte of `char`
 * arrays, and an akgl_Tilemap is dominated by 16 layers of 512x512 `int` cells
 * and 16 tilesets of 65536 offset pairs -- which is why zeroing one costs what
 * it costs, and why the header warns against putting one on the stack.
 */
static void bench_report_footprint(void)
{
    size_t total = 0;

    total = sizeof(akgl_heap_actors) + sizeof(akgl_heap_sprites) + sizeof(akgl_heap_spritesheets) +
	sizeof(akgl_heap_characters) + sizeof(akgl_heap_strings);

    printf("\n");
    printf("static footprint, fixed at compile time:\n");
    printf("  %-24s %5d x %7zu = %10zu bytes\n",
	   "akgl_heap_actors", AKGL_MAX_HEAP_ACTOR, sizeof(akgl_Actor), sizeof(akgl_heap_actors));
    printf("  %-24s %5d x %7zu = %10zu bytes\n",
	   "akgl_heap_sprites", AKGL_MAX_HEAP_SPRITE, sizeof(akgl_Sprite), sizeof(akgl_heap_sprites));
    printf("  %-24s %5d x %7zu = %10zu bytes\n",
	   "akgl_heap_spritesheets", AKGL_MAX_HEAP_SPRITESHEET, sizeof(akgl_SpriteSheet), sizeof(akgl_heap_spritesheets));
    printf("  %-24s %5d x %7zu = %10zu bytes\n",
	   "akgl_heap_characters", AKGL_MAX_HEAP_CHARACTER, sizeof(akgl_Character), sizeof(akgl_heap_characters));
    printf("  %-24s %5d x %7zu = %10zu bytes\n",
	   "akgl_heap_strings", AKGL_MAX_HEAP_STRING, sizeof(akgl_String), sizeof(akgl_heap_strings));
    printf("  %-24s %5s   %7s   %10zu bytes\n", "pools, total", "", "", total);
    printf("  %-24s %5d x %7s = %10zu bytes\n",
	   "akgl_Tilemap", 1, "", sizeof(akgl_Tilemap));
    printf("  %-24s %5d x %7zu = %10zu bytes\n",
	   "  of which layers", AKGL_TILEMAP_MAX_LAYERS, sizeof(akgl_TilemapLayer),
	   (sizeof(akgl_TilemapLayer) * AKGL_TILEMAP_MAX_LAYERS));
    printf("  %-24s %5d x %7zu = %10zu bytes\n",
	   "  of which tilesets", AKGL_TILEMAP_MAX_TILESETS, sizeof(akgl_Tileset),
	   (sizeof(akgl_Tileset) * AKGL_TILEMAP_MAX_TILESETS));
    fflush(stdout);
}

/**
 * @brief Swallow SDL's log output.
 *
 * Installed for the whole run. See the note in the file comment: the library
 * logs on paths this suite calls hundreds of thousands of times, and the cost of
 * writing that out is not a property of libakgl.
 */
static void bench_discard_log(void *userdata, int category, SDL_LogPriority priority, const char *message)
{
    return;
}

/**
 * @brief Claim a spritesheet slot without loading an image into it.
 *
 * akgl_spritesheet_initialize uploads a texture and so needs a renderer, which
 * this suite deliberately does not have. Nothing here draws, and the only thing
 * that reads the sheet is akgl_sprite_initialize storing the pointer, so an
 * empty claimed slot is enough.
 */
static akerr_ErrorContext *bench_make_sheet(akgl_SpriteSheet **dest)
{
    PREPARE_ERROR(errctx);

    FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "dest");
    PASS(errctx, akgl_heap_next_spritesheet(dest));
    memset(*dest, 0x00, sizeof(akgl_SpriteSheet));
    (*dest)->refcount += 1;
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Build a sprite on a freshly claimed sheet and publish it in the sprite registry.
 *
 * @param dest  Receives the sprite. Required.
 * @param name  Registry key. Copied at a fixed #AKGL_SPRITE_MAX_NAME_LENGTH
 *              bytes by akgl_sprite_initialize, so it is staged through a buffer
 *              of exactly that size first.
 * @param speed Nanoseconds one frame is held. 0 makes every akgl_actor_update
 *              advance the animation, which is the expensive path.
 */
static akerr_ErrorContext *bench_make_sprite(akgl_Sprite **dest, char *name, uint32_t speed)
{
    PREPARE_ERROR(errctx);
    akgl_SpriteSheet *sheet = NULL;
    char namebuf[AKGL_SPRITE_MAX_NAME_LENGTH];

    FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "dest");
    FAIL_ZERO_RETURN(errctx, name, AKERR_NULLPOINTER, "name");

    memset(&namebuf, 0x00, sizeof(namebuf));
    strncpy((char *)&namebuf, name, AKGL_SPRITE_MAX_NAME_LENGTH - 1);

    PASS(errctx, bench_make_sheet(&sheet));
    PASS(errctx, akgl_heap_next_sprite(dest));
    PASS(errctx, akgl_sprite_initialize(*dest, (char *)&namebuf, sheet));
    (*dest)->frames = 3;
    (*dest)->frameids[0] = 0;
    (*dest)->frameids[1] = 1;
    (*dest)->frameids[2] = 2;
    (*dest)->width = 48;
    (*dest)->height = 48;
    (*dest)->speed = speed;
    (*dest)->loop = true;
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Build a character with one sprite mapped to the two states these benchmarks use.
 *
 * State 0 is what akgl_actor_initialize leaves an actor in and what
 * akgl_actor_automatic_face leaves it in while it is standing still;
 * `MOVING_RIGHT | FACE_RIGHT` is what bench_fill_actor_pool puts it in and what
 * the face logic settles on from there. Mapping both keeps the update path off
 * the missing-sprite branch, which is measured deliberately elsewhere rather
 * than by accident here.
 */
static akerr_ErrorContext *bench_make_character(akgl_Character **dest, char *name, uint32_t spritespeed)
{
    PREPARE_ERROR(errctx);
    akgl_Sprite *sprite = NULL;

    FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "dest");
    FAIL_ZERO_RETURN(errctx, name, AKERR_NULLPOINTER, "name");

    PASS(errctx, akgl_heap_next_character(dest));
    PASS(errctx, akgl_character_initialize(*dest, name));
    (*dest)->sx = 120.0;
    (*dest)->sy = 120.0;
    (*dest)->sz = 0.0;
    (*dest)->ax = 40.0;
    (*dest)->ay = 40.0;
    (*dest)->speedtime = 16;

    PASS(errctx, bench_make_sprite(&sprite, name, spritespeed));
    PASS(errctx, (*dest)->sprite_add(*dest, sprite, 0));
    PASS(errctx,
	 (*dest)->sprite_add(
	     *dest,
	     sprite,
	     (AKGL_ACTOR_STATE_MOVING_RIGHT | AKGL_ACTOR_STATE_FACE_RIGHT))
	);
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Populate the actor pool with @p count live actors bound to @p basechar.
 *
 * The heap and the actor registry are rebuilt first, so this is the state every
 * frame-shaped benchmark starts from rather than something it inherits.
 */
static akerr_ErrorContext *bench_fill_actor_pool(akgl_Character *basechar, int count)
{
    PREPARE_ERROR(errctx);
    akgl_Actor *actor = NULL;
    char name[AKGL_ACTOR_MAX_NAME_LENGTH];
    int i = 0;

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

    PASS(errctx, akgl_heap_init_actor());
    PASS(errctx, akgl_registry_init_actor());

    for ( i = 0; i < count; i++ ) {
	memset(&name, 0x00, sizeof(name));
	snprintf((char *)&name, AKGL_ACTOR_MAX_NAME_LENGTH, "benchactor%d", i);
	PASS(errctx, akgl_heap_next_actor(&actor));
	PASS(errctx, akgl_actor_initialize(actor, (char *)&name));
	PASS(errctx, akgl_actor_set_character(actor, basechar->name));
	actor->visible = true;
	actor->layer = 0;
	actor->x = (float32_t)(i * 13);
	actor->y = (float32_t)(i * 7);
	actor->sx = 120.0;
	actor->sy = 120.0;
	actor->ax = 40.0;
	actor->ay = 40.0;
	AKGL_BITMASK_ADD(actor->state, AKGL_ACTOR_STATE_MOVING_RIGHT);
    }
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Time a claim from an empty actor pool against one from a nearly full pool.
 *
 * The pool scan stops at the first slot with a zero reference count, so an empty
 * pool answers from index 0 and a pool with one slot left answers from index
 * #AKGL_MAX_HEAP_ACTOR - 1. The ratio between these two numbers *is* the cost of
 * the linear-scan allocator.
 */
static akerr_ErrorContext *bench_heap_actor_claim(void)
{
    PREPARE_ERROR(errctx);
    akerr_ErrorContext *inner = NULL;
    akgl_Actor *actor = NULL;
    int count = bench_iterations(500000);
    int i = 0;
    int rep = 0;

    for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
	PASS(errctx, akgl_heap_init_actor());
	bench_start("heap_next_actor, empty pool", "call", 100.0);
	BENCH_LOOP(inner, i, count, akgl_heap_next_actor(&actor));
	bench_stop(count);
	PASS(errctx, inner);

	for ( i = 0; i < (AKGL_MAX_HEAP_ACTOR - 1); i++ ) {
	    akgl_heap_actors[i].refcount = 1;
	}
	bench_start("heap_next_actor, one slot left", "call", 400.0);
	BENCH_LOOP(inner, i, count, akgl_heap_next_actor(&actor));
	bench_stop(count);
	PASS(errctx, inner);
	PASS(errctx, akgl_heap_init_actor());
    }
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Time the string pool, which is the one that hurts.
 *
 * Every entry is #AKGL_MAX_STRING_LENGTH bytes, so 256 of them is a megabyte and
 * the scan for a free slot touches one reference count every 4 KiB -- a cache
 * miss per candidate. Release then wipes the whole 4 KiB whether the string held
 * a path or a single character. Three numbers come out of this: the claim on an
 * empty pool, the claim with one slot left, and the release on its own.
 */
static akerr_ErrorContext *bench_heap_string(void)
{
    PREPARE_ERROR(errctx);
    akerr_ErrorContext *inner = NULL;
    akgl_String *str = NULL;
    int count = bench_iterations(100000);
    int i = 0;
    int rep = 0;

    for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
	PASS(errctx, akgl_heap_init());

	// Claim and release together: the scratch-buffer idiom the library uses
	// everywhere, and the only string benchmark whose cost a caller can
	// actually observe from outside.
	bench_start("heap string claim + release cycle", "cycle", 500.0);
	for ( i = 0; i < count; i++ ) {
	    inner = akgl_heap_next_string(&str);
	    if ( inner != NULL ) {
		break;
	    }
	    inner = akgl_heap_release_string(str);
	    if ( inner != NULL ) {
		break;
	    }
	}
	bench_stop(count);
	PASS(errctx, inner);

	// The claim on its own. The reference the claim takes is dropped by hand
	// rather than through release, so the 4 KiB wipe stays out of this one.
	bench_start("heap_next_string, empty pool", "call", 100.0);
	for ( i = 0; i < count; i++ ) {
	    inner = akgl_heap_next_string(&str);
	    if ( inner != NULL ) {
		break;
	    }
	    str->refcount = 0;
	}
	bench_stop(count);
	PASS(errctx, inner);

	// The same claim with 255 of the 256 slots taken: a megabyte walked, one
	// reference count read per 4 KiB page.
	for ( i = 0; i < (AKGL_MAX_HEAP_STRING - 1); i++ ) {
	    akgl_heap_strings[i].refcount = 1;
	}
	bench_start("heap_next_string, one slot left", "call", 2500.0);
	for ( i = 0; i < count; i++ ) {
	    inner = akgl_heap_next_string(&str);
	    if ( inner != NULL ) {
		break;
	    }
	    str->refcount = 0;
	}
	bench_stop(count);
	PASS(errctx, inner);
	PASS(errctx, akgl_heap_init());

	// Release on its own: one 4 KiB memset per call, whatever the string held.
	PASS(errctx, akgl_heap_next_string(&str));
	bench_start("heap_release_string, 4 KiB wipe", "call", 500.0);
	for ( i = 0; i < count; i++ ) {
	    str->refcount = 1;
	    inner = akgl_heap_release_string(str);
	    if ( inner != NULL ) {
		break;
	    }
	}
	bench_stop(count);
	PASS(errctx, inner);
    }
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Time a full reset of every pool.
 *
 * akgl_heap_init zeroes all five arrays -- better than a megabyte, dominated by
 * the string pool. A game pays this once at startup and again on every call to
 * akgl_game_init; a level transition that calls it per level pays it per level.
 */
static akerr_ErrorContext *bench_heap_init(void)
{
    PREPARE_ERROR(errctx);
    akerr_ErrorContext *inner = NULL;
    int count = bench_iterations(2000);
    int i = 0;
    int rep = 0;

    for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
	bench_start("heap_init, all five pools", "call", 450000.0);
	BENCH_LOOP(inner, i, count, akgl_heap_init());
	bench_stop(count);
	PASS(errctx, inner);
    }
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Time an actor spawn, with and without the log line the library writes.
 *
 * A spawn is a pool claim, a `memset` of the whole actor, a registry insert, and
 * an `SDL_Log`. The second run raises the log priority so SDL returns before it
 * formats anything; the gap between the two numbers is what the log line costs
 * every caller, including the ones that never read it.
 */
static akerr_ErrorContext *bench_actor_spawn(void)
{
    PREPARE_ERROR(errctx);
    akerr_ErrorContext *inner = NULL;
    akgl_Actor *actor = NULL;
    char name[AKGL_ACTOR_MAX_NAME_LENGTH];
    int count = bench_iterations(20000);
    int i = 0;
    int rep = 0;

    memset(&name, 0x00, sizeof(name));
    strncpy((char *)&name, "benchspawn", AKGL_ACTOR_MAX_NAME_LENGTH - 1);

    for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
	PASS(errctx, akgl_heap_init_actor());
	PASS(errctx, akgl_registry_init_actor());

	bench_start("actor spawn + release, library logging on", "actor", 2100.0);
	for ( i = 0; i < count; i++ ) {
	    inner = akgl_heap_next_actor(&actor);
	    if ( inner != NULL ) {
		break;
	    }
	    inner = akgl_actor_initialize(actor, (char *)&name);
	    if ( inner != NULL ) {
		break;
	    }
	    inner = akgl_heap_release_actor(actor);
	    if ( inner != NULL ) {
		break;
	    }
	}
	bench_stop(count);
	PASS(errctx, inner);

	SDL_SetLogPriorities(SDL_LOG_PRIORITY_CRITICAL);
	bench_start("actor spawn + release, logging suppressed", "actor", 1700.0);
	for ( i = 0; i < count; i++ ) {
	    inner = akgl_heap_next_actor(&actor);
	    if ( inner != NULL ) {
		break;
	    }
	    inner = akgl_actor_initialize(actor, (char *)&name);
	    if ( inner != NULL ) {
		break;
	    }
	    inner = akgl_heap_release_actor(actor);
	    if ( inner != NULL ) {
		break;
	    }
	}
	bench_stop(count);
	SDL_SetLogPriorities(SDL_LOG_PRIORITY_INFO);
	PASS(errctx, inner);
    }
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Time a registry lookup with the actor registry full.
 *
 * akgl_actor_set_character is the library's own name-to-pointer lookup: an SDL
 * property fetch against a registry holding #AKGL_MAX_HEAP_ACTOR entries, plus
 * the four field copies that follow it. Every actor built from a tilemap object
 * layer pays it once.
 */
static akerr_ErrorContext *bench_registry_lookup(void)
{
    PREPARE_ERROR(errctx);
    akerr_ErrorContext *inner = NULL;
    akgl_Character *basechar = NULL;
    akgl_Actor *actor = NULL;
    int count = bench_iterations(200000);
    int i = 0;
    int rep = 0;

    PASS(errctx, akgl_heap_init());
    PASS(errctx, akgl_registry_init());
    PASS(errctx, bench_make_character(&basechar, "benchlookupchar", 0));
    PASS(errctx, bench_fill_actor_pool(basechar, BENCH_ACTOR_COUNT));
    actor = &akgl_heap_actors[0];

    for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
	bench_start("actor_set_character, registry lookup", "call", 400.0);
	BENCH_LOOP(inner, i, count, akgl_actor_set_character(actor, basechar->name));
	bench_stop(count);
	PASS(errctx, inner);
    }
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Time the property store a game reads its configuration out of.
 *
 * akgl_get_property copies a fixed #AKGL_MAX_STRING_LENGTH bytes out of the
 * property store regardless of how long the value is, so reading `"0.0"` moves
 * 4 KiB. akgl_physics_init_arcade makes six of these calls.
 */
static akerr_ErrorContext *bench_properties(void)
{
    PREPARE_ERROR(errctx);
    akerr_ErrorContext *inner = NULL;
    akgl_String *value = NULL;
    int count = bench_iterations(200000);
    int i = 0;
    int rep = 0;

    PASS(errctx, akgl_registry_init_properties());
    PASS(errctx, akgl_set_property("bench.property", "a short value"));
    PASS(errctx, akgl_heap_next_string(&value));

    for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
	bench_start("set_property", "call", 1000.0);
	BENCH_LOOP(inner, i, count, akgl_set_property("bench.property", "a short value"));
	bench_stop(count);
	PASS(errctx, inner);

	bench_start("get_property, 4 KiB copy", "call", 900.0);
	BENCH_LOOP(inner, i, count, akgl_get_property("bench.property", &value, "unset"));
	bench_stop(count);
	PASS(errctx, inner);
    }
    PASS(errctx, akgl_heap_release_string(value));
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Time the state-to-sprite lookup that runs twice per actor per frame.
 *
 * akgl_character_sprite_get renders the state bitmask to decimal with `SDL_itoa`
 * and looks the result up as a property string. akgl_actor_update calls it, and
 * so does akgl_actor_render, so a 64-actor frame makes 128 of these calls before
 * anything is drawn.
 */
static akerr_ErrorContext *bench_character_sprite_get(void)
{
    PREPARE_ERROR(errctx);
    akerr_ErrorContext *inner = NULL;
    akgl_Character *basechar = NULL;
    akgl_Sprite *sprite = NULL;
    int count = bench_iterations(200000);
    int i = 0;
    int rep = 0;

    PASS(errctx, akgl_heap_init());
    PASS(errctx, akgl_registry_init());
    PASS(errctx, bench_make_character(&basechar, "benchspritechar", 0));

    for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
	bench_start("character_sprite_get, state to sprite", "call", 400.0);
	BENCH_LOOP(inner, i, count, basechar->sprite_get(basechar, 0, &sprite));
	bench_stop(count);
	PASS(errctx, inner);
    }
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Time one actor's logic update, with the animation advancing every call.
 *
 * The sprite's frame time is 0, so every update takes the branch that changes
 * frame -- the expensive path, and the one a fast animation actually takes. An
 * update is a face recalculation, a sprite lookup, a clock read, and the frame
 * arithmetic.
 */
static akerr_ErrorContext *bench_actor_update(void)
{
    PREPARE_ERROR(errctx);
    akerr_ErrorContext *inner = NULL;
    akgl_Character *basechar = NULL;
    akgl_Actor *actor = NULL;
    int count = bench_iterations(200000);
    int i = 0;
    int rep = 0;

    PASS(errctx, akgl_heap_init());
    PASS(errctx, akgl_registry_init());
    PASS(errctx, bench_make_character(&basechar, "benchupdatechar", 0));
    PASS(errctx, bench_fill_actor_pool(basechar, 1));
    actor = &akgl_heap_actors[0];
    AKGL_BITMASK_CLEAR(actor->state);

    for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
	bench_start("actor_update, animation advancing", "actor", 700.0);
	BENCH_LOOP(inner, i, count, actor->updatefunc(actor));
	bench_stop(count);
	PASS(errctx, inner);

	// An actor in a state its character has no sprite for is not an error
	// the library refuses -- akgl_actor_update handles AKERR_KEY and carries
	// on, and akgl_actor_render logs it and draws nothing. It is a normal
	// condition on a partly authored character, and it is worth knowing what
	// one costs, because raising, formatting, and handling a context is far
	// more work than the update it replaces.
	AKGL_BITMASK_ADD(actor->state, AKGL_ACTOR_STATE_MOVING_UP);
	bench_start("actor_update, no sprite for state", "actor", 6200.0);
	BENCH_LOOP(inner, i, count, actor->updatefunc(actor));
	bench_stop(count);
	PASS(errctx, inner);
	AKGL_BITMASK_CLEAR(actor->state);
    }
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Time the physics sweep over a full actor pool, and over an empty one.
 *
 * akgl_physics_simulate walks all #AKGL_MAX_HEAP_ACTOR slots whether or not
 * anything is in them, so the empty-pool number is the floor every frame pays.
 * The full-pool number is one frame of movement for 64 actors: movement logic,
 * gravity, drag, clamp, and integrate, per actor.
 */
static akerr_ErrorContext *bench_physics_simulate(void)
{
    PREPARE_ERROR(errctx);
    akerr_ErrorContext *inner = NULL;
    akgl_Character *basechar = NULL;
    akgl_PhysicsBackend backend;
    int count = bench_iterations(20000);
    int i = 0;
    int rep = 0;

    PASS(errctx, akgl_heap_init());
    PASS(errctx, akgl_registry_init());
    PASS(errctx, akgl_registry_init_properties());
    PASS(errctx, bench_make_character(&basechar, "benchphysicschar", 0));

    memset(&backend, 0x00, sizeof(akgl_PhysicsBackend));
    PASS(errctx, akgl_physics_init_arcade(&backend));
    backend.gravity_y = 9.8;
    backend.drag_x = 0.1;
    backend.drag_y = 0.1;

    PASS(errctx, bench_fill_actor_pool(basechar, BENCH_ACTOR_COUNT));
    for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
	bench_start("physics_simulate, 64 live actors", "frame", 16000.0);
	BENCH_LOOP(inner, i, count, backend.simulate(&backend, NULL));
	bench_stop(count);
	PASS(errctx, inner);
    }

    PASS(errctx, akgl_heap_init_actor());
    PASS(errctx, akgl_registry_init_actor());
    for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
	bench_start("physics_simulate, empty pool", "frame", 650.0);
	BENCH_LOOP(inner, i, count, backend.simulate(&backend, NULL));
	bench_stop(count);
	PASS(errctx, inner);
    }
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Time a whole physics step with collision attached, over a full pool.
 *
 * **This is the number Target 12 is about**: collision for a full actor pool
 * without the caller writing a broad phase. It is the whole step -- the proxy
 * sync pass, the movement logic, gravity, drag, the sub-stepped move, and
 * akgl_collision_resolve per actor per sub-step -- against the same pool the
 * row above measures without a world, so the two subtract.
 *
 * The actors are spread on a 24-pixel lattice with 16-pixel boxes, so neighbours
 * are close enough to share grid cells and be tested against each other and far
 * enough apart that most of those tests answer "no". A population that all
 * overlaps measures the resolver; this measures a frame.
 */
static akerr_ErrorContext *bench_physics_collision(void)
{
    PREPARE_ERROR(errctx);
    akerr_ErrorContext *inner = NULL;
    akgl_Character *basechar = NULL;
    akgl_CollisionWorld world;
    akgl_PhysicsBackend backend;
    SDL_FRect body = { .x = -8.0, .y = -8.0, .w = 16.0, .h = 16.0 };
    char label[64];
    int count = bench_iterations(2000);
    int i = 0;
    int rep = 0;

    PASS(errctx, akgl_heap_init());
    PASS(errctx, akgl_registry_init());
    PASS(errctx, akgl_registry_init_properties());
    PASS(errctx, bench_make_character(&basechar, "benchcollisionchar", 0));

    memset(&backend, 0x00, sizeof(akgl_PhysicsBackend));
    PASS(errctx, akgl_physics_init_arcade(&backend));
    backend.gravity_y = 9.8;
    backend.drag_x = 0.1;
    backend.drag_y = 0.1;

    PASS(errctx, bench_fill_actor_pool(basechar, BENCH_ACTOR_COUNT));
    for ( i = 0; i < BENCH_ACTOR_COUNT; i++ ) {
	PASS(errctx, akgl_collision_shape_box(&akgl_heap_actors[i].shape, &body, 0.0));
	akgl_heap_actors[i].shape_override = true;
	akgl_heap_actors[i].shape.collidemask |= AKGL_COLLISION_LAYER_ACTOR;
	akgl_heap_actors[i].x = (float32_t)((i % 20) * 24);
	akgl_heap_actors[i].y = (float32_t)((i / 20) * 24);
    }
    PASS(errctx, akgl_collision_world_init(&world, "grid", 16.0, 16.0));
    backend.collision = &world;

    snprintf((char *)&label, sizeof(label),
	     "physics_simulate + collision, %d live actors", BENCH_ACTOR_COUNT);
    for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
	bench_start((char *)&label, "frame", 2000000.0);
	BENCH_LOOP(inner, i, count, backend.simulate(&backend, NULL));
	bench_stop(count);
	PASS(errctx, inner);
    }
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Time the logic half of a frame: update every actor, then simulate.
 *
 * This is what a host's main loop does before it draws anything, and it is the
 * number to subtract from a 16.6 ms frame to find out what is left for drawing.
 * Note that it is *not* what akgl_game_update does: that one runs the update
 * loop once per tilemap layer, which is measured in the render suite.
 */
static akerr_ErrorContext *bench_logic_frame(void)
{
    PREPARE_ERROR(errctx);
    akerr_ErrorContext *inner = NULL;
    akgl_Character *basechar = NULL;
    akgl_Actor *actor = NULL;
    akgl_PhysicsBackend backend;
    int count = bench_iterations(5000);
    int i = 0;
    int j = 0;
    int rep = 0;

    PASS(errctx, akgl_heap_init());
    PASS(errctx, akgl_registry_init());
    PASS(errctx, akgl_registry_init_properties());
    PASS(errctx, bench_make_character(&basechar, "benchframechar", 0));
    memset(&backend, 0x00, sizeof(akgl_PhysicsBackend));
    PASS(errctx, akgl_physics_init_arcade(&backend));
    PASS(errctx, bench_fill_actor_pool(basechar, BENCH_ACTOR_COUNT));

    for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
	bench_start("logic frame, 64 actors updated + simulated", "frame", 60000.0);
	for ( i = 0; i < count; i++ ) {
	    for ( j = 0; j < AKGL_MAX_HEAP_ACTOR; j++ ) {
		actor = &akgl_heap_actors[j];
		if ( actor->refcount == 0 ) {
		    continue;
		}
		inner = actor->updatefunc(actor);
		if ( inner != NULL ) {
		    break;
		}
	    }
	    if ( inner != NULL ) {
		break;
	    }
	    inner = backend.simulate(&backend, NULL);
	    if ( inner != NULL ) {
		break;
	    }
	}
	bench_stop(count);
	PASS(errctx, inner);
    }
    SUCCEED_RETURN(errctx);
}

/** @brief Counts nothing; the query cost is what is being measured. */
static akerr_ErrorContext *bench_collision_visit(akgl_CollisionProxy *proxy, void *data)
{
    (void)proxy;
    (void)data;
    return NULL;
}

/** @brief A map with a floor, for the tile-collision row. */
static akgl_Tilemap bench_collision_map;

/**
 * @brief Collision: the narrowphase, the broad phase, and a whole sweep.
 *
 * The all-pairs row in bench_geometry stays where it is and is the control for
 * these: it is the cost a caller paid before the library had a broad phase, run
 * on the same machine in the same run, which is the only way to read a
 * reduction honestly.
 */
static akerr_ErrorContext *bench_collision(void)
{
    PREPARE_ERROR(errctx);
    akerr_ErrorContext *inner = NULL;
    akgl_CollisionWorld world;
    akgl_CollisionShape boxshape;
    akgl_CollisionShape circleshape;
    akgl_CollisionProxy a;
    akgl_CollisionProxy b;
    akgl_CollisionProxy *proxies[BENCH_ACTOR_COUNT];
    akgl_Contact contact;
    SDL_FRect body = { .x = -8.0, .y = -8.0, .w = 16.0, .h = 16.0 };
    SDL_FRect area = { .x = 0.0, .y = 0.0, .w = 48.0, .h = 48.0 };
    bool hit = false;
    int count = bench_iterations(500000);
    int sweeps = bench_iterations(20000);
    int i = 0;
    int x = 0;
    int rep = 0;

    PASS(errctx, akgl_collision_shape_box(&boxshape, &body, 0.0));
    PASS(errctx, akgl_collision_shape_circle(&circleshape, 0.0, 0.0, 8.0, 0.0));

    for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
	PASS(errctx, akgl_heap_init());
	PASS(errctx, akgl_collision_world_init(&world, "grid", 16.0, 16.0));

	PASS(errctx, akgl_collision_proxy_initialize(&a, NULL, &boxshape, 0.0, 0.0, 0.0));
	PASS(errctx, akgl_collision_proxy_initialize(&b, NULL, &boxshape, 4.0, 0.0, 0.0));
	bench_start("narrowphase box/box, overlapping", "call", 210.0);
	BENCH_LOOP(inner, i, count, akgl_collision_test(&a, &b, world.flags, &contact, &hit));
	bench_stop(count);
	PASS(errctx, inner);

	PASS(errctx, akgl_collision_proxy_initialize(&b, NULL, &boxshape, 900.0, 900.0, 0.0));
	bench_start("narrowphase box/box, disjoint", "call", 90.0);
	BENCH_LOOP(inner, i, count, akgl_collision_test(&a, &b, world.flags, &contact, &hit));
	bench_stop(count);
	PASS(errctx, inner);

	PASS(errctx, akgl_collision_proxy_initialize(&a, NULL, &circleshape, 0.0, 0.0, 0.0));
	PASS(errctx, akgl_collision_proxy_initialize(&b, NULL, &circleshape, 4.0, 0.0, 0.0));
	bench_start("narrowphase circle/circle, overlapping", "call", 680.0);
	BENCH_LOOP(inner, i, bench_iterations(100000),
		   akgl_collision_test(&a, &b, world.flags, &contact, &hit));
	bench_stop(bench_iterations(100000));
	PASS(errctx, inner);

	// The broad phase, with a realistic population spread over the map.
	for ( i = 0; i < BENCH_ACTOR_COUNT; i++ ) {
	    PASS(errctx, akgl_heap_next_collision_proxy(&proxies[i]));
	    PASS(errctx, akgl_collision_proxy_initialize(proxies[i], NULL, &boxshape,
							 (float32_t)((i % 20) * 24),
							 (float32_t)((i / 20) * 24), 0.0));
	    PASS(errctx, world.partitioner.insert(&world.partitioner, proxies[i]));
	}

	bench_start("grid move, unchanged cells", "actor", 120.0);
	BENCH_LOOP(inner, i, count, world.partitioner.move(&world.partitioner, proxies[0]));
	bench_stop(count);
	PASS(errctx, inner);

	bench_start("grid query, 64 actors", "query", 440.0);
	BENCH_LOOP(inner, i, sweeps,
		   world.partitioner.query(&world.partitioner, &area,
					   AKGL_COLLISION_LAYER_ALL, &bench_collision_visit, NULL));
	bench_stop(sweeps);
	PASS(errctx, inner);

	// The same population through the tree, so the two are comparable.
	PASS(errctx, akgl_collision_world_init(&world, "bsp", 16.0, 16.0));
	for ( i = 0; i < BENCH_ACTOR_COUNT; i++ ) {
	    PASS(errctx, world.partitioner.insert(&world.partitioner, proxies[i]));
	}
	bench_start("bsp query, 64 actors", "query", 1140.0);
	BENCH_LOOP(inner, i, sweeps,
		   world.partitioner.query(&world.partitioner, &area,
					   AKGL_COLLISION_LAYER_ALL, &bench_collision_visit, NULL));
	bench_stop(sweeps);
	PASS(errctx, inner);

	// One actor against a floor of tiles, which is the case a platformer
	// runs every frame for every actor.
	memset(&bench_collision_map, 0x00, sizeof(akgl_Tilemap));
	bench_collision_map.tilewidth = 16;
	bench_collision_map.tileheight = 16;
	bench_collision_map.width = 128;
	bench_collision_map.height = 32;
	bench_collision_map.numlayers = 1;
	bench_collision_map.layers[0].type = AKGL_TILEMAP_LAYER_TYPE_TILES;
	for ( x = 0; x < bench_collision_map.width; x++ ) {
	    bench_collision_map.layers[0].data[(20 * bench_collision_map.width) + x] = 1;
	}
	bench_collision_map.collidablelayers = 1u;
	PASS(errctx, akgl_collision_world_init(&world, "grid", 16.0, 16.0));
	PASS(errctx, akgl_collision_bind_tilemap(&world, &bench_collision_map));

	area.x = 100.0;
	area.y = 310.0;
	area.w = 16.0;
	area.h = 16.0;
	bench_start("tile query, actor on a floor", "query", 160.0);
	BENCH_LOOP(inner, i, sweeps, akgl_collision_box_blocked(&world, &area,
								AKGL_COLLISION_LAYER_ALL, &hit));
	bench_stop(sweeps);
	PASS(errctx, inner);
    }
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Time akgl_collide_rectangles, and the all-pairs sweep it used to imply.
 *
 * The function is four span comparisons in `float32_t`, so an overlap and a miss
 * cost the same -- it has no early exit and does not want one.
 *
 * The last benchmark is the **control**, not a recommendation. It is what a game
 * had to write before 0.8.0: 64 actors is 2016 pairs, every frame. The
 * partitioner rows in bench_collision are what to compare it against, and the
 * comparison is generous to the control -- it tests pairs and does not sync,
 * sub-step or resolve anything.
 */
static akerr_ErrorContext *bench_geometry(void)
{
    PREPARE_ERROR(errctx);
    akerr_ErrorContext *inner = NULL;
    SDL_FRect rects[BENCH_ACTOR_COUNT];
    SDL_FRect overlapping = { .x = 8.0, .y = 8.0, .w = 32.0, .h = 32.0 };
    SDL_FRect disjoint = { .x = 900.0, .y = 900.0, .w = 32.0, .h = 32.0 };
    SDL_FRect subject = { .x = 0.0, .y = 0.0, .w = 32.0, .h = 32.0 };
    bool collide = false;
    int count = bench_iterations(500000);
    int sweeps = bench_iterations(2000);
    int pairs = 0;
    int i = 0;
    int j = 0;
    int k = 0;
    int rep = 0;

    for ( i = 0; i < BENCH_ACTOR_COUNT; i++ ) {
	rects[i].x = (float32_t)(i * 24);
	rects[i].y = (float32_t)(i * 18);
	rects[i].w = 32.0;
	rects[i].h = 32.0;
    }
    pairs = (BENCH_ACTOR_COUNT * (BENCH_ACTOR_COUNT - 1)) / 2;

    for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
	bench_start("collide_rectangles, overlapping", "call", 300.0);
	BENCH_LOOP(inner, i, count, akgl_collide_rectangles(&subject, &overlapping, &collide));
	bench_stop(count);
	PASS(errctx, inner);

	bench_start("collide_rectangles, disjoint", "call", 600.0);
	BENCH_LOOP(inner, i, count, akgl_collide_rectangles(&subject, &disjoint, &collide));
	bench_stop(count);
	PASS(errctx, inner);

	bench_start("control: all-pairs collision sweep, 64 actors", "sweep", 1200000.0);
	for ( k = 0; k < sweeps; k++ ) {
	    for ( i = 0; i < BENCH_ACTOR_COUNT; i++ ) {
		for ( j = i + 1; j < BENCH_ACTOR_COUNT; j++ ) {
		    inner = akgl_collide_rectangles(&rects[i], &rects[j], &collide);
		    if ( inner != NULL ) {
			break;
		    }
		}
	    }
	    if ( inner != NULL ) {
		break;
	    }
	}
	bench_stop(sweeps);
	PASS(errctx, inner);
    }
    printf("all-pairs collision sweep covers %d pairs per sweep\n", pairs);
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Time the pooled-string operations.
 *
 * Both of these move #AKGL_MAX_STRING_LENGTH bytes by default -- a copy with a
 * count of 0 means "all of it" -- so the cost is the same for a filename and for
 * a single character.
 */
static akerr_ErrorContext *bench_strings(void)
{
    PREPARE_ERROR(errctx);
    akerr_ErrorContext *inner = NULL;
    akgl_String *src = NULL;
    akgl_String *dst = NULL;
    int count = bench_iterations(200000);
    int i = 0;
    int rep = 0;

    PASS(errctx, akgl_heap_init());
    PASS(errctx, akgl_heap_next_string(&src));
    PASS(errctx, akgl_heap_next_string(&dst));
    PASS(errctx, akgl_string_initialize(src, "assets/testcharacter.json"));

    for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
	bench_start("string_initialize", "call", 350.0);
	BENCH_LOOP(inner, i, count, akgl_string_initialize(dst, "assets/testcharacter.json"));
	bench_stop(count);
	PASS(errctx, inner);

	bench_start("string_copy, full length", "call", 350.0);
	BENCH_LOOP(inner, i, count, akgl_string_copy(src, dst, 0));
	bench_stop(count);
	PASS(errctx, inner);
    }
    PASS(errctx, akgl_heap_release_string(src));
    PASS(errctx, akgl_heap_release_string(dst));
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Time parsing a document and reading values back out of it.
 *
 * The parse is what an asset load pays per file. The accessors are what the
 * tilemap and character loaders pay per field, and every one of them that
 * returns a string claims and fills a 4 KiB pooled string.
 */
static akerr_ErrorContext *bench_json(void)
{
    PREPARE_ERROR(errctx);
    akerr_ErrorContext *inner = NULL;
    akgl_String *path = NULL;
    akgl_String *value = NULL;
    json_t *doc = NULL;
    json_error_t jsonerr;
    int number = 0;
    int loads = bench_iterations(2000);
    int count = bench_iterations(200000);
    int i = 0;
    int rep = 0;

    PASS(errctx, akgl_heap_init());
    PASS(errctx, akgl_heap_next_string(&path));
    PASS(errctx, akgl_heap_next_string(&value));
    snprintf((char *)&path->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), BENCH_JSON_FIXTURE);

    doc = json_load_file((char *)&path->data, 0, &jsonerr);
    FAIL_ZERO_RETURN(errctx, doc, AKERR_IO, "Unable to load %s: %s", (char *)&path->data, jsonerr.text);

    for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
	bench_start("json_load_file, small document", "load", 120000.0);
	for ( i = 0; i < loads; i++ ) {
	    json_t *scratch = json_load_file((char *)&path->data, 0, &jsonerr);
	    if ( scratch == NULL ) {
		break;
	    }
	    json_decref(scratch);
	}
	bench_stop(loads);

	bench_start("get_json_string_value", "call", 450.0);
	BENCH_LOOP(inner, i, count, akgl_get_json_string_value(doc, "name", &value));
	bench_stop(count);
	PASS(errctx, inner);

	bench_start("get_json_integer_value", "call", 150.0);
	BENCH_LOOP(inner, i, count, akgl_get_json_integer_value(doc, "count", &number));
	bench_stop(count);
	PASS(errctx, inner);
    }
    json_decref(doc);
    PASS(errctx, akgl_heap_release_string(path));
    PASS(errctx, akgl_heap_release_string(value));
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Time path resolution, which every asset reference goes through.
 *
 * akgl_path_relative calls `realpath(3)`, so this is a syscall and a walk of the
 * filesystem, per reference, at load time. A tilemap naming twenty tilesets and
 * sprites pays it twenty times.
 */
static akerr_ErrorContext *bench_path_relative(void)
{
    PREPARE_ERROR(errctx);
    akerr_ErrorContext *inner = NULL;
    akgl_String *dst = NULL;
    int count = bench_iterations(20000);
    int i = 0;
    int rep = 0;

    PASS(errctx, akgl_heap_init());
    PASS(errctx, akgl_heap_next_string(&dst));

    for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
	bench_start("path_relative, realpath on an existing file", "call", 36000.0);
	BENCH_LOOP(inner, i, count, akgl_path_relative(".", BENCH_PATH_FIXTURE, dst));
	bench_stop(count);
	PASS(errctx, inner);
    }
    PASS(errctx, akgl_heap_release_string(dst));
    SUCCEED_RETURN(errctx);
}

int main(void)
{
    PREPARE_ERROR(errctx);

    SDL_SetHint(SDL_HINT_VIDEO_DRIVER, "dummy");
    SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy");
    SDL_SetLogOutputFunction(bench_discard_log, NULL);

    ATTEMPT {
	CATCH(errctx, akgl_error_init());
	CATCH(errctx, akgl_heap_init());
	CATCH(errctx, akgl_registry_init());
	CATCH(errctx, akgl_registry_init_properties());

	CATCH(errctx, bench_heap_actor_claim());
	CATCH(errctx, bench_heap_string());
	CATCH(errctx, bench_heap_init());
	CATCH(errctx, bench_actor_spawn());
	CATCH(errctx, bench_registry_lookup());
	CATCH(errctx, bench_properties());
	CATCH(errctx, bench_character_sprite_get());
	CATCH(errctx, bench_actor_update());
	CATCH(errctx, bench_physics_simulate());
	CATCH(errctx, bench_physics_collision());
	CATCH(errctx, bench_logic_frame());
	CATCH(errctx, bench_geometry());
	CATCH(errctx, bench_collision());
	CATCH(errctx, bench_strings());
	CATCH(errctx, bench_json());
	CATCH(errctx, bench_path_relative());

	bench_report_footprint();
	BENCH_REPORT_BREAK(errctx);
    } CLEANUP {
    } PROCESS(errctx) {
    } FINISH_NORETURN(errctx);
}
