/**
 * @file docs_checkjson.c
 * @brief Load one documented asset file through the loader that really reads it.
 *
 * libakgl's four asset formats -- sprite, character, tilemap and the property
 * configuration -- are described in prose in the manual and parsed by
 * `src/sprite.c`, `src/character.c`, `src/tilemap.c` and `src/registry.c`, with
 * nothing tying the two together. That gap is not hypothetical:
 * `util/assets/littleguy.json` ships beside the loader it is invalid against,
 * using pre-prefix state names and a `velocity_x` the current loader does not
 * accept.
 *
 * So a `json kind=sprite` block in a chapter is written to a file and handed
 * here, and the documented format is by construction the format the loader
 * accepts. `tests/docs_examples.sh` is the caller.
 *
 * **It takes a list of kind/file pairs and loads them in order**, because three
 * of the four formats are not self-contained: a character names sprites that
 * have to already be in the registry, and a tilemap names characters. One
 * process, several files, in the order the library requires them -- which is
 * itself a fact about the format that the chapter has to state, and now states
 * by writing it down as `preload=`.
 *
 * **It is a host**, like `tools/docs_screenshot.c` and the test suites: it
 * brings SDL and libakgl up headless itself rather than calling
 * `akgl_game_init()`, because the loaders need a renderer and the registries and
 * nothing else.
 *
 * The exit status is 0 when every file loaded and 1 when one did not; 2 is a
 * usage error. Whatever the loader said is on stderr, which is what the harness
 * shows.
 */

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

#include <SDL3/SDL.h>

#include <akerror.h>

#include <akgl/character.h>
#include <akgl/error.h>
#include <akgl/game.h>
#include <akgl/heap.h>
#include <akgl/registry.h>
#include <akgl/renderer.h>
#include <akgl/sprite.h>
#include <akgl/tilemap.h>

/**
 * @brief The map a `kind=tilemap` block is loaded into.
 *
 * File scope because an akgl_Tilemap carries every layer and tileset it owns and
 * does not belong on a stack -- the same reason src/game.c keeps
 * akgl_default_gamemap where it does.
 */
static akgl_Tilemap MAP;

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

/**
 * @brief akgl_tilemap_load() with the signature the dispatch table wants.
 *
 * The other three loaders take a filename and nothing else. Rather than give the
 * table two shapes and a branch to pick between them, the odd one out gets a
 * wrapper.
 */
static akerr_ErrorContext AKERR_NOIGNORE *load_tilemap(char *fname)
{
    PREPARE_ERROR(errctx);
    PASS(errctx, akgl_tilemap_load(fname, &MAP));
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Load every `*.json` in @p dir through @p load, in whatever order the
 *        filesystem hands them back.
 *
 * An absent directory is not a failure. A `kind=character` block with no
 * `setup=` should fail on the sprite it actually names -- which is the sentence
 * a chapter author needs to read -- rather than on a fixture directory they
 * never asked for.
 */
static akerr_ErrorContext AKERR_NOIGNORE *load_directory(
    const char *dir,
    akerr_ErrorContext *(*load)(char *fname))
{
    PREPARE_ERROR(errctx);
    char **found = NULL;
    char path[PATH_MAX];
    int count = 0;
    int i = 0;

    FAIL_ZERO_RETURN(errctx, dir, AKERR_NULLPOINTER, "Received null directory");
    FAIL_ZERO_RETURN(errctx, load, AKERR_NULLPOINTER, "Received null loader");

    found = SDL_GlobDirectory(dir, "*.json", 0, &count);
    if ( found == NULL ) {
	SUCCEED_RETURN(errctx);
    }
    for ( i = 0; i < count; i++ ) {
	snprintf(path, sizeof(path), "%s/%s", dir, found[i]);
	ATTEMPT {
	    CATCH(errctx, load(path));
	} CLEANUP {
	} PROCESS(errctx) {
	} FINISH(errctx, true);
    }
    SDL_free(found);
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Put the sprites a `kind=character` block names into the registry.
 *
 * `akgl_character_load_json` resolves every `sprite` field against
 * #AKGL_REGISTRY_SPRITE and fails if one is missing, so a chapter showing a
 * character file on its own still needs sprites behind it.
 * `tests/docs_setups/character.sh` is what stages them.
 */
static akerr_ErrorContext AKERR_NOIGNORE *prepare_character(void)
{
    static bool done = false;
    PREPARE_ERROR(errctx);

    /*
     * Once per process, whether it is reached through the `character` row or
     * through prepare_tilemap(). Loading the same sprite file twice registers
     * the name twice and leaks the first sprite's pool slot.
     */
    if ( done == true ) {
	SUCCEED_RETURN(errctx);
    }
    done = true;
    PASS(errctx, load_directory("./sprites", akgl_sprite_load_json));
    SUCCEED_RETURN(errctx);
}

/**
 * @brief The same, one level further out: a tilemap's actor objects name characters.
 *
 * `akgl_tilemap_load_layer_object_actor` calls `akgl_actor_set_character`, which
 * is a registry lookup and nothing more, so the character has to be loaded --
 * and a character needs its sprites, which is why this runs both.
 */
static akerr_ErrorContext AKERR_NOIGNORE *prepare_tilemap(void)
{
    static bool done = false;
    PREPARE_ERROR(errctx);

    if ( done == true ) {
	SUCCEED_RETURN(errctx);
    }
    done = true;
    PASS(errctx, prepare_character());
    PASS(errctx, load_directory("./characters", akgl_character_load_json));
    SUCCEED_RETURN(errctx);
}

/** @brief One documented asset format, what has to exist first, and what reads it. */
typedef struct {
    char *kind;					/**< The `kind=` value in the fence. */
    akerr_ErrorContext *(*prepare)(void);	/**< Registry state the format needs, or `NULL`. */
    akerr_ErrorContext *(*load)(char *fname);	/**< What the library does with it. */
} docs_JsonKind;

/*
 * A table rather than an if/else ladder, for the reason AGENTS.md gives about
 * backends: a fifth format is a row, not a branch. The names are the vocabulary
 * docs/MAINTENANCE.md publishes, so they are also the error message when a
 * chapter invents one.
 *
 * The `prepare` column is where "three of these four formats are not
 * self-contained" is written down. Each one guards itself so it runs at most
 * once per process, however it is reached.
 */
static docs_JsonKind KINDS[] = {
    { "sprite",		NULL,			akgl_sprite_load_json		},
    { "character",	prepare_character,	akgl_character_load_json	},
    { "tilemap",	prepare_tilemap,	load_tilemap			},
    { "properties",	NULL,			akgl_registry_load_properties	},
    { NULL,		NULL,			NULL				}
};

static void usage(void)
{
    int i = 0;

    fprintf(stderr, "usage: akgl_docs_checkjson <kind> <file.json> [<kind> <file.json>]...\n"
	    "\n  kind is one of:");
    for ( i = 0; KINDS[i].kind != NULL; i++ ) {
	fprintf(stderr, " %s", KINDS[i].kind);
    }
    fprintf(stderr, "\n\nLoads each file through libakgl's own loader for that format, in the\n"
	    "order given, in one process. A character's sprites and a tilemap's\n"
	    "characters have to be in the registry before the file that names them.\n");
}

/** @brief Load one file through the loader that owns its format. */
static akerr_ErrorContext AKERR_NOIGNORE *load_one(char *kind, char *fname)
{
    PREPARE_ERROR(errctx);
    int i = 0;

    FAIL_ZERO_RETURN(errctx, kind, AKERR_NULLPOINTER, "Received null kind");
    FAIL_ZERO_RETURN(errctx, fname, AKERR_NULLPOINTER, "Received null filename");

    for ( i = 0; KINDS[i].kind != NULL; i++ ) {
	if ( strcmp(KINDS[i].kind, kind) == 0 ) {
	    if ( KINDS[i].prepare != NULL ) {
		PASS(errctx, KINDS[i].prepare());
	    }
	    PASS(errctx, KINDS[i].load(fname));
	    SUCCEED_RETURN(errctx);
	}
    }
    FAIL_RETURN(errctx, AKERR_KEY, "There is no documented asset format called \"%s\"", kind);
}

/** @brief Everything between SDL being up and the loaders having answered. */
static akerr_ErrorContext AKERR_NOIGNORE *check(int pairs, char **argv)
{
    PREPARE_ERROR(errctx);
    int i = 0;

    FAIL_ZERO_RETURN(errctx, argv, AKERR_NULLPOINTER, "Received null argument vector");

    PASS(errctx, akgl_error_init());
    akgl_renderer = &akgl_default_renderer;

    FAIL_ZERO_RETURN(errctx, SDL_Init(SDL_INIT_VIDEO), AKGL_ERR_SDL,
		     "Couldn't initialize SDL: %s", SDL_GetError());
    FAIL_ZERO_RETURN(
	errctx,
	SDL_CreateWindowAndRenderer(
	    "net/aklabs/libakgl/docs_checkjson", 320, 240, 0,
	    &akgl_window, &akgl_renderer->sdl_renderer),
	AKGL_ERR_SDL,
	"Couldn't create window/renderer: %s", SDL_GetError());

    PASS(errctx, akgl_render_2d_bind(akgl_renderer));
    PASS(errctx, akgl_heap_init());
    PASS(errctx, akgl_registry_init());

    for ( i = 0; i < pairs; i++ ) {
	PASS(errctx, load_one(argv[i * 2], argv[(i * 2) + 1]));
    }
    SUCCEED_RETURN(errctx);
}

int main(int argc, char **argv)
{
    PREPARE_ERROR(errctx);

    if ( argc < 3 || ((argc - 1) % 2) != 0 ) {
	usage();
	return 2;
    }

    SDL_SetHint(SDL_HINT_VIDEO_DRIVER, "dummy");
    SDL_SetHint(SDL_HINT_RENDER_DRIVER, "software");
    SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy");

    ATTEMPT {
	CATCH(errctx, check((argc - 1) / 2, &argv[1]));
    } CLEANUP {
	if ( akgl_window != NULL ) {
	    SDL_DestroyWindow(akgl_window);
	    akgl_window = NULL;
	}
	SDL_Quit();
    } PROCESS(errctx) {
    } HANDLE_DEFAULT(errctx) {
	LOG_ERROR_WITH_MESSAGE(errctx, "the documented JSON did not load");
	/*
	 * A flag rather than a `return` here: FINISH ends with RELEASE_ERROR,
	 * and leaving a HANDLE block early means the context never goes back to
	 * AKERR_ARRAY_ERROR. AGENTS.md documents that as a process-killing leak,
	 * and scripts/check_error_protocol.py fails the build over it.
	 */
	failed = 1;
	/*
	 * FINISH_NORETURN rather than FINISH: FINISH expands a
	 * `return __err_context` that an int-returning function cannot compile,
	 * even where the branch is unreachable.
	 */
    } FINISH_NORETURN(errctx);

    return failed;
}
