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

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

#include <akstdlib.h>
#include <akgl/heap.h>
#include <akgl/sprite.h>
#include <akgl/registry.h>
#include <akgl/iterator.h>
#include <akgl/actor.h>
#include <akgl/json_helpers.h>

SDL_PropertiesID AKGL_REGISTRY_ACTOR = 0;
SDL_PropertiesID AKGL_REGISTRY_ACTOR_STATE_STRINGS = 0;
SDL_PropertiesID AKGL_REGISTRY_SPRITE = 0;
SDL_PropertiesID AKGL_REGISTRY_SPRITESHEET = 0;
SDL_PropertiesID AKGL_REGISTRY_CHARACTER = 0;
SDL_PropertiesID AKGL_REGISTRY_MUSIC = 0;
SDL_PropertiesID AKGL_REGISTRY_FONT = 0;
SDL_PropertiesID AKGL_REGISTRY_PROPERTIES = 0;

/**
 * @brief Create one registry, replacing whatever set was held under it.
 *
 * All eight initializers go through this so they behave the same way on a
 * second call. Only akgl_registry_init_actor used to destroy the old set --
 * akgl_heap_init_actor's counterpart, so resetting actors between levels was
 * the one path that did not leak -- and the other seven abandoned their
 * SDL_PropertiesID every time they were called again.
 *
 * @param dest The registry handle to (re)create. Required.
 * @param what What it holds, for the failure message. Required.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKERR_NULLPOINTER If @p dest is `NULL`, or if SDL cannot create the
 *         property set.
 */
static akerr_ErrorContext *registry_create(SDL_PropertiesID *dest, const char *what)
{
    PREPARE_ERROR(errctx);
    FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "dest");
    if ( *dest != 0 ) {
	SDL_DestroyProperties(*dest);
	*dest = 0;
    }
    *dest = SDL_CreateProperties();
    FAIL_ZERO_RETURN(errctx, *dest, AKERR_NULLPOINTER, "Error initializing %s registry", what);
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_registry_init(void)
{
    PREPARE_ERROR(errctx);
    ATTEMPT {
	CATCH(errctx, akgl_registry_init_spritesheet());
	CATCH(errctx, akgl_registry_init_sprite());
	CATCH(errctx, akgl_registry_init_character());
	CATCH(errctx, akgl_registry_init_actor());
	CATCH(errctx, akgl_registry_init_actor_state_strings());
	CATCH(errctx, akgl_registry_init_font());
	CATCH(errctx, akgl_registry_init_music());
	// Missing until 0.5.0. Without it AKGL_REGISTRY_PROPERTIES stayed 0,
	// akgl_set_property was a silent no-op, akgl_get_property always
	// returned the caller's default, and akgl_physics_init_arcade and
	// akgl_render_2d_init quietly ignored their configuration. Only callers
	// going through akgl_game_init, which calls it separately, escaped that.
	CATCH(errctx, akgl_registry_init_properties());
    } CLEANUP {
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_registry_init_actor(void)
{
    PREPARE_ERROR(errctx);
    PASS(errctx, registry_create(&AKGL_REGISTRY_ACTOR, "actor"));
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_registry_init_font(void)
{
    PREPARE_ERROR(errctx);
    PASS(errctx, registry_create(&AKGL_REGISTRY_FONT, "font"));
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_registry_init_music(void)
{
    PREPARE_ERROR(errctx);
    PASS(errctx, registry_create(&AKGL_REGISTRY_MUSIC, "music"));
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_registry_init_properties(void)
{
    PREPARE_ERROR(errctx);
    PASS(errctx, registry_create(&AKGL_REGISTRY_PROPERTIES, "properties"));
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_registry_init_actor_state_strings(void)
{
    int i = 0;
    int flag = 0;
    PREPARE_ERROR(errctx);
    PASS(errctx, registry_create(&AKGL_REGISTRY_ACTOR_STATE_STRINGS, "actor state strings"));
    for ( i = 0 ; i < AKGL_ACTOR_MAX_STATES; i++ ) {
	flag = (1 << i);
	SDL_SetNumberProperty(
	    AKGL_REGISTRY_ACTOR_STATE_STRINGS,
	    AKGL_ACTOR_STATE_STRING_NAMES[i],
	    flag);
    }
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_registry_init_sprite(void)
{
    PREPARE_ERROR(errctx);
    PASS(errctx, registry_create(&AKGL_REGISTRY_SPRITE, "sprite"));
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_registry_init_spritesheet(void)
{
    PREPARE_ERROR(errctx);
    PASS(errctx, registry_create(&AKGL_REGISTRY_SPRITESHEET, "spritesheet"));
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_registry_init_character(void)
{
    PREPARE_ERROR(errctx);
    PASS(errctx, registry_create(&AKGL_REGISTRY_CHARACTER, "character"));
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_registry_load_properties(char *fname)
{
    json_t *json = NULL;
    json_t *props = NULL;
    const char *pkey = NULL;
    json_t *pvalue = NULL;

    json_error_t error;
    akgl_String *tmpstr;

    PREPARE_ERROR(errctx);
    FAIL_ZERO_RETURN(errctx, fname, AKERR_NULLPOINTER, "null filename");
    // One ATTEMPT around the whole function, rather than one for the parse and
    // a bare loop after it. `props` is a borrowed reference into `json` and is
    // read by the loop, so the document cannot be released until the loop is
    // done -- and with the loop outside the block, every path out of it leaked
    // the document. The per-property ATTEMPT stays nested inside the loop,
    // which is what AGENTS.md requires: a CATCH written directly in a loop
    // breaks the loop rather than leaving the function.
    ATTEMPT {
	SDL_Log("Loading from %s", fname);
	json = json_load_file(fname, 0, &error);
	FAIL_ZERO_BREAK(
	    errctx,
	    json,
	    AKERR_NULLPOINTER,
	    "Error while loading properties from %s on line %d: %s-",
	    fname,
	    error.line,
	    error.text);
	CATCH(errctx, akgl_get_json_object_value(json, "properties", &props));

	json_object_foreach(props, pkey, pvalue) {
	    ATTEMPT {
		CATCH(errctx, akgl_heap_next_string(&tmpstr));
		CATCH(errctx, akgl_get_json_string_value(props, (char *)pkey, &tmpstr));
		SDL_SetStringProperty(AKGL_REGISTRY_PROPERTIES, pkey, tmpstr->data);
		SDL_Log("Set property %s = %s", pkey, tmpstr->data);
		CATCH(errctx, akgl_heap_release_string(tmpstr));
	    } CLEANUP {
	    } PROCESS(errctx) {
	    } FINISH(errctx, true);
	}
    } CLEANUP {
	// Every value was copied into the property store on the way past, so
	// nothing outlives the document.
	if ( json != NULL ) {
	    json_decref(json);
	    json = NULL;
	}
    } PROCESS(errctx) {
    } FINISH(errctx, true);

    SDL_Log("Properties loaded");
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_set_property(char *name, char *value)
{
    PREPARE_ERROR(errctx);
    FAIL_ZERO_RETURN(errctx, name, AKERR_NULLPOINTER, "NULL char *");
    FAIL_ZERO_RETURN(errctx, value, AKERR_NULLPOINTER, "NULL char *");
    SDL_SetStringProperty(AKGL_REGISTRY_PROPERTIES, name, value);
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_get_property(char *name, akgl_String **dest, char *def)
{
    const char *value = NULL;
    size_t valuelen = 0;

    PREPARE_ERROR(errctx);
    FAIL_ZERO_RETURN(errctx, name, AKERR_NULLPOINTER, "NULL char *");
    FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL akgl_String *");
    ATTEMPT {
	if ( *dest == NULL ) {
	    CATCH(errctx, akgl_heap_next_string(dest));
	}
	// Copy the value's own length, not the destination's capacity. What
	// SDL hands back is its strdup of the value -- four bytes for "0.0" --
	// so copying a fixed AKGL_MAX_STRING_LENGTH read up to 4 KiB past the
	// end of somebody else's allocation on every call. It returned garbage
	// past the terminator, and it would have faulted outright on a value
	// that happened to sit at the end of a page.
	value = SDL_GetStringProperty(AKGL_REGISTRY_PROPERTIES, (const char *)name, (const char *)def);
	// An unset property with a NULL default still reports AKERR_NULLPOINTER,
	// which is what the header has always promised. It used to arrive from
	// inside aksl_memcpy; now it is raised here, before anything is measured.
	FAIL_ZERO_BREAK(
	    errctx,
	    value,
	    AKERR_NULLPOINTER,
	    "Property %s is not set and no default was given",
	    name);
	valuelen = strlen(value);
	FAIL_NONZERO_BREAK(
	    errctx,
	    (valuelen >= AKGL_MAX_STRING_LENGTH),
	    AKERR_OUTOFBOUNDS,
	    "Property %s is %zu bytes, which does not fit an akgl_String (%d)",
	    name,
	    valuelen,
	    AKGL_MAX_STRING_LENGTH);
	CATCH(errctx, aksl_memcpy((*dest)->data, (void *)value, (valuelen + 1)));
    } CLEANUP {
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}
