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

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

#include <akgl/game.h>
#include <akgl/sprite.h>
#include <akgl/json_helpers.h>
#include <akgl/heap.h>
#include <akgl/registry.h>
#include <akgl/staticstring.h>
#include <akgl/iterator.h>
#include <akgl/util.h>

akerr_ErrorContext *akgl_character_initialize(akgl_Character *obj, char *name)
{
    PREPARE_ERROR(errctx);
    FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL akgl_Character reference");
    FAIL_ZERO_RETURN(errctx, name, AKERR_NULLPOINTER, "NULL name string pointer");
    memset(obj, 0x00, sizeof(akgl_Character));
    // Always terminated: this is a registry key, and strncpy at exactly the
    // field width leaves an over-long name unterminated.
    PASS(errctx, aksl_strncpy(obj->name, sizeof(obj->name), name, sizeof(obj->name) - 1));
    obj->state_sprites = SDL_CreateProperties();
    FAIL_ZERO_RETURN(errctx, obj->state_sprites, AKERR_NULLPOINTER, "Unable to initialize SDL_PropertiesID for character state map");

    obj->sprite_add = &akgl_character_sprite_add;
    obj->sprite_get = &akgl_character_sprite_get;

    FAIL_ZERO_RETURN(
	errctx,
	SDL_SetPointerProperty(AKGL_REGISTRY_CHARACTER, name, (void *)obj),
	AKERR_KEY,
	"Unable to add character to registry");
    obj->refcount += 1;
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_character_sprite_add(akgl_Character *basechar, akgl_Sprite *ref, int state)
{
    PREPARE_ERROR(errctx);
    char stateval[32];
    akgl_Sprite *displaced = NULL;
    FAIL_ZERO_RETURN(errctx, basechar, AKERR_NULLPOINTER, "NULL character reference");
    FAIL_ZERO_RETURN(errctx, ref, AKERR_NULLPOINTER, "NULL sprite reference");
    memset(&stateval, 0x00, 32);
    SDL_itoa(state, (char *)&stateval, 10);

    // Whatever this state was bound to is losing its binding, and the reference
    // taken for that binding with it. Without this a character that rebinds a
    // state while alive leaks one sprite slot per rebind -- releasing at
    // teardown only gives back whatever the map happens to hold at the end.
    displaced = (akgl_Sprite *)SDL_GetPointerProperty(basechar->state_sprites, (char *)&stateval, NULL);

    // Take the new reference before anything can fail, so a failed write cannot
    // leave the sprite bound with no reference behind it.
    ref->refcount += 1;
    if ( SDL_SetPointerProperty(basechar->state_sprites, (char *)&stateval, ref) == false ) {
	ref->refcount -= 1;
	FAIL_RETURN(
	    errctx,
	    AKERR_KEY,
	    "Unable to bind sprite %s to character %s for state %d: %s",
	    (char *)&ref->name,
	    (char *)&basechar->name,
	    state,
	    SDL_GetError());
    }

    if ( (displaced != NULL) && (displaced != ref) ) {
	PASS(errctx, akgl_heap_release_sprite(displaced));
    }
    SDL_Log("Added sprite %s to character %s for state %b", (char *)&ref->name, (char *)&basechar->name, state);
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_character_sprite_get(akgl_Character *basechar, int state, akgl_Sprite **dest)
{
    PREPARE_ERROR(errctx);
    char stateval[32];
    FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL pointer to sprite pointer (**dest)");
    FAIL_ZERO_RETURN(errctx, basechar, AKERR_NULLPOINTER, "NULL character reference");
    memset(&stateval, 0x00, 32);
    SDL_itoa(state, (char *)&stateval, 10);
    *dest = (akgl_Sprite *)SDL_GetPointerProperty(basechar->state_sprites, (char *)&stateval, NULL);
    FAIL_ZERO_RETURN(errctx, *dest, AKERR_KEY, "Sprite for state %d (%b) not found in the character's registry", state, state);
    SUCCEED_RETURN(errctx);
}

// SDL iterator so we can't return error information here, void only
// this means we don't have anywhere to send exceptions up to, so if we hit an error, we log and exit(1) here
void akgl_character_state_sprites_iterate(void *userdata, SDL_PropertiesID props, const char *name)
{
    PREPARE_ERROR(errctx);
    akgl_Sprite *spriteptr;
    akgl_Iterator *opflags = (akgl_Iterator *)userdata;
    ATTEMPT {
	FAIL_ZERO_BREAK(errctx, opflags, AKERR_NULLPOINTER, "Character state sprite iterator received null iterator op pointer");
	FAIL_ZERO_BREAK(errctx, name, AKERR_NULLPOINTER, "Character state sprite iterator received null sprite name");
	spriteptr = (akgl_Sprite *)SDL_GetPointerProperty(props, name, NULL);
	FAIL_ZERO_BREAK(errctx, spriteptr, AKERR_NULLPOINTER, "Character state sprite for %s not found", name);
	if ( AKGL_BITMASK_HAS(opflags->flags, AKGL_ITERATOR_OP_RELEASE) ) {
	    CATCH(errctx, akgl_heap_release_sprite(spriteptr));
	}
    } CLEANUP {
    } PROCESS(errctx) {
    } FINISH_NORETURN(errctx);
}

/**
 * @brief OR a JSON array of actor-state names together into one bitmask.
 *
 * This is what lets a character JSON say `["FACE_LEFT", "MOVING_LEFT"]` rather
 * than a number: each name is looked up in #AKGL_REGISTRY_ACTOR_STATE_STRINGS
 * and OR-ed in. A name that is not a known state is an error rather than being
 * skipped -- a typo in a state name would otherwise bind a sprite to the wrong
 * combination and show up as art that never appears.
 *
 * @param states JSON array of state-name strings. Required. An empty array is
 *               legal and leaves @p dest as it was.
 * @param dest   Receives the OR of every named bit. Required in practice, and
 *               **not** checked -- the guard that should test it tests @p states
 *               a second time instead, so a `NULL` here is a crash. It is OR-ed
 *               into rather than assigned, so the caller must zero it first.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKERR_NULLPOINTER If @p states is `NULL`.
 * @throws AKERR_KEY If a name is not in the actor-state registry. The message
 *         quotes it. Note that bits 11 and 12 are unreachable by name; see
 *         akgl_registry_init_actor_state_strings.
 * @throws AKERR_TYPE If an array element is not a string.
 * @throws AKERR_OUTOFBOUNDS If the array is indexed past its end.
 * @throws AKGL_ERR_HEAP If the string pool is exhausted.
 */
static akerr_ErrorContext *character_load_json_state_int_from_strings(json_t *states, int *dest)
{
    int i = 0;
    long newstate = 0;
    akgl_String *tmpstring = NULL;
    PREPARE_ERROR(errctx);
    FAIL_ZERO_RETURN(errctx, states, AKERR_NULLPOINTER, "NULL states array");
    FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL destination integer");

    ATTEMPT {
	CATCH(errctx, akgl_heap_next_string(&tmpstring));
	for ( i = 0; i < json_array_size((json_t *)states) ; i++ ) {
	    CATCH(errctx, akgl_get_json_array_index_string(states, i, &tmpstring));
	    newstate = (long)SDL_GetNumberProperty(AKGL_REGISTRY_ACTOR_STATE_STRINGS, (char *)&tmpstring->data, 0);
	    FAIL_ZERO_BREAK(errctx, newstate, AKERR_KEY, "Unknown actor state %s", (char *)&tmpstring->data);
	    *dest = (*dest | (int)(newstate));
	}
    } CLEANUP {
	IGNORE(akgl_heap_release_string(tmpstring));
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Name a character and build its state-to-sprite map from a JSON document.
 *
 * The half of akgl_character_load_json that deals with identity and sprites; the
 * caller handles the numeric fields afterwards. Reads `name`, initializes the
 * character with it, then walks `sprite_mappings` binding each named sprite to
 * the bitmask its `state` array adds up to.
 *
 * Every sprite named must already be in #AKGL_REGISTRY_SPRITE.
 *
 * @param json The parsed character document. Required in practice; not checked,
 *             and passed straight to accessors that report it.
 * @param obj  The pooled character to fill in. Required, unchecked -- it is
 *             handed to akgl_character_initialize, which does check it.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKERR_NULLPOINTER If a mapping names a sprite that is not registered.
 *         The message names the character, the state, and the sprite.
 * @throws AKERR_KEY If `name`, `sprite_mappings`, or a mapping's `sprite` or
 *         `state` is absent, or if a state name is unknown.
 * @throws AKERR_TYPE If one of those keys has the wrong JSON type.
 * @throws AKERR_OUTOFBOUNDS If an array is indexed past its end.
 * @throws AKGL_ERR_HEAP If the string pool is exhausted.
 */
static akerr_ErrorContext *character_load_json_inner(json_t *json, akgl_Character *obj)
{
    PREPARE_ERROR(errctx);
    json_t *mappings = NULL;
    json_t *curmapping = NULL;
    json_t *statearray = NULL;
    akgl_Sprite *spriteptr = NULL;
    int i = 0;
    akgl_String *tmpstr = NULL;
    akgl_String *tmpstr2 = NULL;
    int stateval = 0;

    ATTEMPT {
	CATCH(errctx, akgl_get_json_string_value((json_t *)json, "name", &tmpstr));
	CATCH(errctx, akgl_character_initialize((akgl_Character *)obj, tmpstr->data));
	CATCH(errctx, akgl_get_json_array_value((json_t *)json, "sprite_mappings", &mappings));
	for ( i = 0; i < json_array_size((json_t *)mappings) ; i++ ) {
	    stateval = 0;
	    CATCH(errctx, akgl_get_json_array_index_object((json_t *)mappings, i, &curmapping));
	    CATCH(errctx, akgl_get_json_string_value((json_t *)curmapping, "sprite", &tmpstr));
	    spriteptr = (akgl_Sprite *)SDL_GetPointerProperty(
		AKGL_REGISTRY_SPRITE,
		tmpstr->data,
		NULL
		);
	    CATCH(errctx, akgl_get_json_string_value((json_t *)json, "name", &tmpstr2));

	    CATCH(errctx, akgl_get_json_array_value((json_t *)curmapping, "state", &statearray));
	    CATCH(errctx, character_load_json_state_int_from_strings(statearray, &stateval));

	    CATCH(errctx, akgl_get_json_string_value((json_t *)curmapping, "sprite", &tmpstr));
	    FAIL_ZERO_BREAK(
		errctx,
		spriteptr,
		AKERR_NULLPOINTER,
		"Character %s for state %b references sprite %s but not found in the registry",
		tmpstr2->data,
		stateval,
		tmpstr->data
		);
	    CATCH(errctx, akgl_character_sprite_add((akgl_Character *)obj, (akgl_Sprite *)spriteptr, stateval));
	}
    } CLEANUP {
	if ( tmpstr != NULL ) {
	    IGNORE(akgl_heap_release_string(tmpstr));
	}
	if ( tmpstr2 != NULL ) {
	    IGNORE(akgl_heap_release_string(tmpstr2));
	}
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_character_load_json(char *filename)
{
    PREPARE_ERROR(errctx);
    json_t *json = NULL;
    json_error_t error;
    akgl_Character *obj = NULL;
    //akgl_String *tmpstr = NULL;

    FAIL_ZERO_RETURN(errctx, filename, AKERR_NULLPOINTER, "Received null filename");
    ATTEMPT {
	CATCH(errctx, akgl_heap_next_character(&obj));
	//CATCH(errctx, akgl_heap_next_string(&tmpstr));
	//CATCH(errctx, akgl_string_initialize(tmpstr, NULL));
	json = (json_t *)json_load_file(filename, 0, &error);
	FAIL_ZERO_BREAK(
	    errctx,
	    json,
	    AKERR_NULLPOINTER,
	    "Error while loading character from %s on line %d: %s", filename, error.line, error.text
	    );
	CATCH(errctx, character_load_json_inner(json, obj));
	CATCH(errctx, akgl_get_json_integer_value(json, "speedtime", (int *)&obj->speedtime));
	obj->speedtime = obj->speedtime * AKGL_TIME_ONEMS_NS;
	CATCH(errctx, akgl_get_json_number_value(json, "speed_x", &obj->sx));
	CATCH(errctx, akgl_get_json_number_value(json, "speed_y", &obj->sy));
	CATCH(errctx, akgl_get_json_number_value(json, "acceleration_x", &obj->ax));
	CATCH(errctx, akgl_get_json_number_value(json, "acceleration_y", &obj->ay));
    } CLEANUP {
	//IGNORE(akgl_heap_release_string(tmpstr));
	// The character keeps nothing that points into the document -- every
	// field above is copied out of it -- so the whole parsed tree goes back
	// here, on the success path as well as the failure one.
	if ( json != NULL ) {
	    json_decref(json);
	    json = NULL;
	}
	if ( errctx != NULL ) {
	    if ( errctx->status != 0 ) {
		IGNORE(akgl_heap_release_character(obj));
	    }
	}
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SDL_Log("Character %s loaded from %s", obj->name, filename);
    SUCCEED_RETURN(errctx);
}
