/**
 * @file main.c
 * @brief Startup, the frame loop, and teardown for the sidescroller tutorial.
 *
 * The order everything happens in is the point of this file. libakgl has one
 * startup sequence that works, documented at the top of `include/akgl/game.h`,
 * and three of its steps are ones a reader gets wrong the first time:
 *
 * - the configuration properties have to be set *before* the renderer and the
 *   physics backend are initialized, because both read them;
 * - `akgl_game_init` does **not** choose a physics backend, so the application
 *   has to;
 * - characters name sprites and maps name characters, so the assets load
 *   sprites, then characters, then the map -- and the map creates the actors.
 */

#include <string.h>

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

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

#include <akgl/character.h>
#include <akgl/controller.h>
#include <akgl/heap.h>
#include <akgl/physics.h>
#include <akgl/registry.h>
#include <akgl/renderer.h>
#include <akgl/sprite.h>
#include <akgl/text.h>
#include <akgl/tilemap.h>

#include "sidescroller.h"

/** @brief Where the tutorial assets live. CMake defines it; `--assets` overrides it. */
#ifndef SS_ASSET_DIR
#define SS_ASSET_DIR "."
#endif

/** @brief Longest asset path this program will build. */
#define SS_PATH_MAX 1024

ss_Game ss_game;
akgl_CollisionWorld ss_collision;

/** @brief Where `--screenshot` writes, and on which frame. NULL means never. */
static char *ss_shotpath = NULL;
static int ss_shotframe = 0;

/**
 * @brief Read the render target back and write it out as a PNG.
 *
 * Called between the world being drawn and the frame being presented, because
 * SDL_RenderPresent is where the target stops being readable. This is how the
 * figure in chapter 20 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);
}

akerr_ErrorContext *ss_grounded(akgl_CollisionShape *shape, float32_t x, float32_t y, bool *dest)
{
    SDL_FRect feet;
    PREPARE_ERROR(errctx);

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

    /* One pixel down, and only one: a taller probe reports a floor the actor is
     * still falling towards, and a jump that fires off it looks like the player
     * jumped out of thin air. */
    PASS(errctx, akgl_collision_shape_bounds(shape, x, y + 1.0f, &feet));
    PASS(errctx, akgl_collision_box_blocked(&ss_collision, &feet, AKGL_COLLISION_LAYER_STATIC, dest));
    SUCCEED_RETURN(errctx);
}

/**
 * @brief The sprites, loaded in this order.
 *
 * Sprites first and characters second is not a preference. A character's JSON
 * names its sprites by registry name and `akgl_character_load_json` looks each
 * one up as it reads the mapping, so a character loaded first fails with
 * `AKERR_NULLPOINTER` on the first sprite it cannot find.
 */
static char *ss_sprite_files[] = {
    "sprite_ss_player_idle_left.json",   /* one frame, held */
    "sprite_ss_player_idle_right.json",
    "sprite_ss_player_run_left.json",    /* four frames at 90 ms */
    "sprite_ss_player_run_right.json",
    "sprite_ss_player_jump_left.json",   /* one frame, held for the whole arc */
    "sprite_ss_player_jump_right.json",
    "sprite_ss_coin.json",
    "sprite_ss_hazard_blob.json",
    "sprite_ss_hazard_moth.json",
    NULL
};

/** @brief The characters. Each one binds state bitmasks to the sprites above. */
static char *ss_character_files[] = {
    "character_ss_player.json",
    "character_ss_coin.json",
    "character_ss_hazard_blob.json",
    "character_ss_hazard_moth.json",
    NULL
};

/** @brief Set in HANDLE_DEFAULT and read after FINISH; see the note in main. */
static int ss_failed = 0;

/**
 * @brief Replacement for `akgl_game.lowfpsfunc`, which logs a line per frame.
 *
 * The default is `akgl_game_lowfps`, and it fires on **every frame** the frame
 * rate is under 30 -- including every frame of the first second of the process,
 * because `akgl_game.fps` is a completed-second average and reads 0 until the
 * first second is up. The hook exists to be replaced; the point of it is that a
 * game can shed work rather than log about it. This one has nothing to shed.
 */
static void ss_lowfps(void)
{
}

/**
 * @brief Join the asset directory and a file name into @p dest.
 *
 * `aksl_snprintf` rather than `snprintf`, because a path that does not fit has
 * to arrive as `AKERR_OUTOFBOUNDS` naming both lengths. Truncated silently, it
 * reports itself later as a missing file with a name nobody wrote.
 */
static akerr_ErrorContext *asset_path(char *dir, char *name, char *dest, size_t size)
{
    int count = 0;
    PREPARE_ERROR(errctx);

    FAIL_ZERO_RETURN(errctx, dir, AKERR_NULLPOINTER, "dir");
    FAIL_ZERO_RETURN(errctx, name, AKERR_NULLPOINTER, "name");
    FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "dest");
    PASS(errctx, aksl_snprintf(&count, dest, size, "%s/%s", dir, name));
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Bring the library up and open the window.
 *
 * `akgl_game.name`, `.version` and `.uri` are filled in first because
 * `akgl_game_init` refuses to run without all three: the window title, SDL's
 * application metadata and the savegame compatibility check are built from them.
 */
static akerr_ErrorContext *startup(void)
{
    PREPARE_ERROR(errctx);

    PASS(errctx, aksl_strncpy(
	     (char *)&akgl_game.name,
	     sizeof(akgl_game.name),
	     "libakgl sidescroller tutorial",
	     sizeof(akgl_game.name) - 1));
    PASS(errctx, aksl_strncpy(
	     (char *)&akgl_game.version,
	     sizeof(akgl_game.version),
	     "1.0.0",
	     sizeof(akgl_game.version) - 1));
    PASS(errctx, aksl_strncpy(
	     (char *)&akgl_game.uri,
	     sizeof(akgl_game.uri),
	     "net.aklabs.libakgl.sidescroller",
	     sizeof(akgl_game.uri) - 1));

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

    /* Properties before the renderer: akgl_render_2d_init reads both of these
     * out of the registry, and an unset one defaults to the string "0", which
     * asks SDL for a zero-sized window. */
    PASS(errctx, akgl_set_property("game.screenwidth", "960"));
    PASS(errctx, akgl_set_property("game.screenheight", "480"));
    PASS(errctx, akgl_render_2d_init(akgl_renderer));

    /*
     * libakgl draws in map pixels and has no scale factor of its own -- the only
     * scaling it applies is the tilemap's perspective band, which this map does
     * not use. So a 16-pixel tile is 16 screen pixels, and pixel art wants more
     * than that. SDL's logical presentation is the answer: the game renders a
     * 480x240 view and SDL scales it up by whole multiples to fill the window.
     */
    FAIL_ZERO_RETURN(
	errctx,
	SDL_SetRenderLogicalPresentation(
	    akgl_renderer->sdl_renderer,
	    SS_VIEW_WIDTH,
	    SS_VIEW_HEIGHT,
	    SDL_LOGICAL_PRESENTATION_INTEGER_SCALE),
	AKGL_ERR_SDL,
	"%s",
	SDL_GetError()
	);

    /* akgl_render_2d_init sized the camera from the window. The view is what the
     * camera looks through, so it has to say the same thing. */
    akgl_camera->x = 0.0f;
    akgl_camera->y = 0.0f;
    akgl_camera->w = (float32_t)SS_VIEW_WIDTH;
    akgl_camera->h = (float32_t)SS_VIEW_HEIGHT;

    /*
     * akgl_game_init does NOT choose a physics backend. It points akgl_physics
     * at akgl_default_physics, which is zeroed storage -- all four of its method
     * pointers are NULL -- and never initializes it. There is no
     * `physics.engine` property either, whatever physics.h says. Skip this call
     * and the first akgl_game_update calls through a NULL `simulate`.
     *
     * The map replaces this backend below with one of its own. It is still done
     * here, because a game that loads a map without physics properties gets a
     * working backend rather than a crash.
     */
    PASS(errctx, akgl_physics_init_arcade(akgl_physics));
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Load the sprites, the characters and the level, in that order.
 */
static akerr_ErrorContext *load_level(char *assetdir)
{
    char path[SS_PATH_MAX];
    akgl_Actor *player = NULL;
    int i = 0;
    PREPARE_ERROR(errctx);

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

    for ( i = 0; ss_sprite_files[i] != NULL; i++ ) {
	PASS(errctx, asset_path(assetdir, ss_sprite_files[i], (char *)&path, sizeof(path)));
	PASS(errctx, akgl_sprite_load_json((char *)&path));
    }
    for ( i = 0; ss_character_files[i] != NULL; i++ ) {
	PASS(errctx, asset_path(assetdir, ss_character_files[i], (char *)&path, sizeof(path)));
	PASS(errctx, akgl_character_load_json((char *)&path));
    }

    /*
     * `akgl_gamemap` already points at `akgl_default_gamemap`, 25 MiB of static
     * storage in the library. That is not an accident and it is not a
     * micro-optimisation: sizeof(akgl_Tilemap) is three times a default 8 MiB
     * thread stack, so a local one is a segfault before the loader writes a
     * byte.
     *
     * Loading the map creates every `actor` object in its object layers, binds
     * each to the character its properties name, and publishes it in
     * AKGL_REGISTRY_ACTOR -- which is why the characters had to be loaded first.
     */
    PASS(errctx, asset_path(assetdir, "level1.tmj", (char *)&path, sizeof(path)));
    PASS(errctx, akgl_tilemap_load((char *)&path, akgl_gamemap));

    /*
     * The map carried `physics.model`, `physics.gravity.y` and `physics.drag.y`,
     * so the loader built a backend of its own and set `use_own_physics`.
     * Nothing in the library acts on that flag: akgl_game_update steps the
     * global akgl_physics and never looks at the map's. Honouring it is this
     * line, and it is what lets a swimming level and a walking level differ by
     * data rather than by code.
     */
    if ( akgl_gamemap->use_own_physics == true ) {
	akgl_physics = &akgl_gamemap->physics;
	SDL_Log(
	    "Using the map's own physics: gravity %.1f, drag %.1f, terminal fall %.1f px/s",
	    akgl_physics->gravity_y,
	    akgl_physics->drag_y,
	    (akgl_physics->gravity_y / akgl_physics->drag_y));
    }

    /*
     * Collision is opt-in: a backend with no world attached moves an actor and
     * consults nothing, which is what this game used to rely on and then spend
     * 383 lines of its own working around. The cell arguments are placeholders
     * -- akgl_collision_bind_tilemap overwrites them from the map's own tile
     * size, and reads which layers are solid off each layer's `collidable`
     * property rather than a layer id compiled into the game.
     */
    PASS(errctx, akgl_collision_world_init(&ss_collision, NULL, (float32_t)SS_TILE_SIZE, (float32_t)SS_TILE_SIZE));
    PASS(errctx, akgl_collision_bind_tilemap(&ss_collision, akgl_gamemap));
    akgl_physics->collision = &ss_collision;

    player = SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, "player", NULL);
    FAIL_ZERO_RETURN(errctx, player, AKERR_KEY, "The map placed no actor called player");
    PASS(errctx, ss_player_bind(player));
    PASS(errctx, ss_actors_bind());
    PASS(errctx, ss_player_controls(0, "player"));

    /*
     * Re-stamp the clock the simulation measures against. Everything above --
     * nine sprite files, four characters, a map and its tileset image -- happened
     * between the backend being created and the first step, and dt is measured
     * from `gravity_time`. The bound on `physics.max_timestep` would catch it,
     * at the cost of one visibly slow-motion frame.
     */
    akgl_physics->gravity_time = SDL_GetTicksNS();
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Centre the camera on the player, clamped to the level.
 *
 * The camera is a plain `SDL_FRect` in map pixels that the library reads; moving
 * it is the whole of scrolling. It is floored to a whole pixel because
 * `akgl_tilemap_draw` truncates it when it works out how much of the edge tiles
 * to show, and a camera that is fractionally different every frame makes the
 * tile grid shimmer.
 */
static akerr_ErrorContext *update_camera(void)
{
    float32_t limit = 0.0f;
    PREPARE_ERROR(errctx);

    FAIL_ZERO_RETURN(errctx, ss_game.player, AKERR_NULLPOINTER, "ss_game.player");
    FAIL_ZERO_RETURN(errctx, akgl_camera, AKERR_NULLPOINTER, "akgl_camera");

    limit = (float32_t)(akgl_gamemap->width * akgl_gamemap->tilewidth) - akgl_camera->w;
    akgl_camera->x = (ss_game.player->x + 16.0f) - (akgl_camera->w / 2.0f);
    if ( akgl_camera->x > limit ) {
	akgl_camera->x = limit;
    }
    if ( akgl_camera->x < 0.0f ) {
	akgl_camera->x = 0.0f;
    }
    akgl_camera->x = (float32_t)((int)akgl_camera->x);
    akgl_camera->y = 0.0f;
    SUCCEED_RETURN(errctx);
}

/**
 * @brief One frame: events, then the camera, then the library's own tick.
 *
 * `akgl_game_update` is update-every-actor, step-the-physics, draw-the-world. It
 * does not clear or present, so the frame is bracketed by the backend's
 * `frame_start` and `frame_end` here.
 */
static akerr_ErrorContext *frame(bool *running)
{
    SDL_Event event;
    PREPARE_ERROR(errctx);

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

    while ( SDL_PollEvent(&event) == true ) {
	if ( event.type == SDL_EVENT_QUIT ) {
	    *running = false;
	}
	/* Every event, unconditionally: one that no control map binds is not an
	 * error, it is a call that did nothing. */
	PASS(errctx, akgl_controller_handle_event((void *)&akgl_game.state, &event));
    }

    ss_game.frame += 1;
    if ( ss_game.autoplay == true ) {
	PASS(errctx, ss_player_autoplay(ss_game.frame));
    }
    PASS(errctx, update_camera());

    PASS(errctx, akgl_renderer->frame_start(akgl_renderer));
    /*
     * Note the failure contract: akgl_game_update takes the game state lock and
     * every one of its failure paths returns with the lock still held. SDL
     * mutexes are recursive so a single-threaded loop does not deadlock on the
     * next frame, but a frame that failed has left the world half-stepped.
     * Treat it as terminal, which is what PASS does here.
     */
    PASS(errctx, akgl_game_update(NULL));
    if ( (ss_shotpath != NULL) && (ss_game.frame == ss_shotframe) ) {
	PASS(errctx, save_screenshot(ss_shotpath));
    }
    PASS(errctx, akgl_renderer->frame_end(akgl_renderer));
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext *run(int frames)
{
    bool running = true;
    PREPARE_ERROR(errctx);

    while ( running == true ) {
	PASS(errctx, frame(&running));
	if ( (frames > 0) && (ss_game.frame >= frames) ) {
	    running = false;
	}
	/* A crude frame limiter. A game with a window on a real display should
	 * ask SDL for vsync instead; this one has to work under the dummy video
	 * driver, where there is nothing to sync to. */
	SDL_Delay(16);
    }

    /* Recorded here and not in main(): shutdown_game() runs in main()'s CLEANUP
     * block and releases the actor pool, so ss_game.player is stale by the time
     * the summary is printed. */
    if ( ss_game.player != NULL ) {
	ss_game.final_x = ss_game.player->x;
	ss_game.final_y = ss_game.player->y;
    }
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Give back what the process is holding.
 *
 * **There is no `akgl_game_shutdown`.** Teardown is the application's, and the
 * order matters in one place: `akgl_text_unloadallfonts` has to run before
 * `TTF_Quit` or `SDL_Quit`, because those destroy the fonts underneath the
 * registry that still points at them.
 *
 * The pools are not walked beyond the actors. They are static storage in a
 * process that is exiting, and the sprites and characters are still referenced
 * by each other; a game that loads a second level has to unwind that properly,
 * and this one does not pretend to.
 */
static void shutdown_game(void)
{
    int i = 0;

    IGNORE(akgl_text_unloadallfonts());
    for ( i = 0; i < AKGL_MAX_HEAP_ACTOR; i++ ) {
	if ( akgl_heap_actors[i].refcount > 0 ) {
	    IGNORE(akgl_heap_release_actor(&akgl_heap_actors[i]));
	}
    }
    /* Guarded: this runs from CLEANUP, which is reached even when startup failed
     * before akgl_game_init pointed akgl_gamemap at anything. */
    if ( akgl_gamemap != NULL ) {
	IGNORE(akgl_tilemap_release(akgl_gamemap));
    }
    TTF_Quit();
    MIX_Quit();
    SDL_Quit();
}

static akerr_ErrorContext *parse_args(int argc, char *argv[], char **assetdir, int *frames)
{
    const char *env = NULL;
    int i = 0;
    PREPARE_ERROR(errctx);

    FAIL_ZERO_RETURN(errctx, assetdir, AKERR_NULLPOINTER, "assetdir");
    FAIL_ZERO_RETURN(errctx, frames, AKERR_NULLPOINTER, "frames");

    env = SDL_getenv("AKGL_SIDESCROLLER_FRAMES");
    if ( env != NULL ) {
	PASS(errctx, aksl_atoi((char *)env, frames));
    }
    for ( i = 1; i < argc; i++ ) {
	if ( strcmp(argv[i], "--autoplay") == 0 ) {
	    ss_game.autoplay = true;
	} else if ( strcmp(argv[i], "--frames") == 0 ) {
	    i += 1;
	    FAIL_NONZERO_RETURN(errctx, (i >= argc), AKERR_VALUE, "--frames needs a count");
	    PASS(errctx, aksl_atoi(argv[i], frames));
	} else if ( strcmp(argv[i], "--assets") == 0 ) {
	    i += 1;
	    FAIL_NONZERO_RETURN(errctx, (i >= argc), AKERR_VALUE, "--assets needs a directory");
	    *assetdir = argv[i];
	} else if ( strcmp(argv[i], "--screenshot") == 0 ) {
	    i += 1;
	    FAIL_NONZERO_RETURN(errctx, (i >= argc), AKERR_VALUE, "--screenshot needs a path");
	    ss_shotpath = argv[i];
	} else if ( strcmp(argv[i], "--screenshot-frame") == 0 ) {
	    i += 1;
	    FAIL_NONZERO_RETURN(errctx, (i >= argc), AKERR_VALUE, "--screenshot-frame needs a number");
	    PASS(errctx, aksl_atoi(argv[i], &ss_shotframe));
	} else {
	    FAIL_RETURN(
		errctx,
		AKERR_VALUE,
		"usage: sidescroller [--assets DIR] [--frames N] [--autoplay]"
		" [--screenshot PATH] [--screenshot-frame N]"
		);
	}
    }
    SUCCEED_RETURN(errctx);
}

int main(int argc, char *argv[])
{
    char *assetdir = SS_ASSET_DIR;
    int frames = 0;
    PREPARE_ERROR(errctx);

    ATTEMPT {
	CATCH(errctx, parse_args(argc, argv, &assetdir, &frames));
	CATCH(errctx, startup());
	CATCH(errctx, load_level(assetdir));
	CATCH(errctx, run(frames));
    } CLEANUP {
	shutdown_game();
    } PROCESS(errctx) {
    } HANDLE_DEFAULT(errctx) {
	LOG_ERROR_WITH_MESSAGE(errctx, "the sidescroller could not run");
	/*
	 * Set a flag rather than returning: leaving a HANDLE block early skips
	 * FINISH's RELEASE_ERROR and leaks the context's pool slot, and the
	 * 129th such call aborts the process. AGENTS.md spells it out.
	 */
	ss_failed = 1;
	/*
	 * FINISH_NORETURN rather than FINISH: FINISH expands a
	 * `return __err_context` that an int-returning function cannot compile,
	 * even on a branch that cannot be reached.
	 */
    } FINISH_NORETURN(errctx);

    /*
     * The position is in the summary because exiting 0 is not evidence that
     * collision ran. The map is 240 pixels tall and gravity is 900 px/s^2: an
     * unresolved player is hundreds of pixels below the floor within a second,
     * and this line is what makes that visible in a CI log instead of passing.
     */
    SDL_Log(
	"sidescroller: %d frames, %d of %d coins, %d deaths, player at %.1f,%.1f%s",
	ss_game.frame,
	ss_game.coins_taken,
	SS_COIN_COUNT,
	ss_game.deaths,
	ss_game.final_x,
	ss_game.final_y,
	((ss_game.grounded == true) ? " grounded" : " airborne"));
    return ss_failed;
}
