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

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

#include <libgen.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_spritesheet_coords_for_frame(akgl_Sprite *self, SDL_FRect *srccoords, uint8_t frameid)
{
    PREPARE_ERROR(errctx);

    FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "NULL sprite");
    FAIL_ZERO_RETURN(errctx, srccoords, AKERR_NULLPOINTER, "NULL SDL_Rect");
    FAIL_ZERO_RETURN(errctx, self->sheet, AKERR_NULLPOINTER, "NULL spritesheet");

    srccoords->x = self->width * self->frameids[frameid];
    if ( srccoords->x >= self->sheet->texture->w ) {
	srccoords->y = ((int)srccoords->x / self->sheet->texture->w) * self->height;
	srccoords->x = ((int)srccoords->x % self->sheet->texture->w);
    } else {
	srccoords->y = 0;
    }
    srccoords->w = self->width;
    srccoords->h = self->height;

    SUCCEED_RETURN(errctx);
}

/**
 * @brief Find or load the spritesheet a sprite definition names.
 *
 * Resolves the `spritesheet.filename` path relative to the sprite definition's
 * own directory, then looks that resolved path up in #AKGL_REGISTRY_SPRITESHEET.
 * A hit is reused as is -- which is what keeps ten sprites cut from one image
 * down to one texture -- and a miss claims a sheet from the pool and loads it,
 * reading `frame_width` and `frame_height` only in that case.
 *
 * @param json          The parsed sprite document, whose `spritesheet` object
 *                      this reads. Required in practice; not checked.
 * @param sheet         Receives the sheet, whether found or freshly loaded.
 *                      Required; not checked. On the reuse path the sheet's
 *                      reference count is *not* incremented, so the sprite
 *                      borrows it.
 * @param relative_path Directory to resolve the image path against -- the
 *                      directory holding the sprite JSON. Required.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKERR_KEY If the document has no `spritesheet` object, or that object
 *         has no `filename` -- or, on the load path, no `frame_width` or
 *         `frame_height`.
 * @throws AKERR_TYPE If one of those has the wrong JSON type.
 * @throws AKERR_OUTOFBOUNDS If the joined path is too long for a pooled string.
 * @throws ENOENT If the image path does not exist.
 * @throws AKGL_ERR_SDL If the image cannot be decoded.
 * @throws AKGL_ERR_HEAP If the spritesheet or string pool is exhausted.
 */
static akerr_ErrorContext *sprite_load_json_spritesheet(json_t *json, akgl_SpriteSheet **sheet, char *relative_path)
{
    PREPARE_ERROR(errctx);
    json_t *spritesheet_json = NULL;
    int ss_frame_width = 0;
    int ss_frame_height = 0;
    akgl_String *ss_filename = NULL;
    akgl_String *tmpstr = NULL;

    ATTEMPT {
	CATCH(errctx, akgl_heap_next_string(&tmpstr));
	CATCH(errctx, akgl_string_initialize(tmpstr, NULL));
	CATCH(errctx, akgl_get_json_object_value((json_t *)json, "spritesheet", &spritesheet_json));
	CATCH(errctx, akgl_get_json_string_value((json_t *)spritesheet_json, "filename", &ss_filename));
	CATCH(errctx, akgl_path_relative(relative_path, ss_filename->data, tmpstr));
	*sheet = SDL_GetPointerProperty(
	    AKGL_REGISTRY_SPRITESHEET,
	    (char *)&tmpstr->data,
	    NULL
	    );
	if ( *sheet == NULL ) {
	    CATCH(errctx, akgl_heap_next_spritesheet(sheet));
	    CATCH(errctx, akgl_get_json_integer_value((json_t *)spritesheet_json, "frame_width", &ss_frame_width));
	    CATCH(errctx, akgl_get_json_integer_value((json_t *)spritesheet_json, "frame_height", &ss_frame_height));
	    CATCH(errctx,
		  akgl_spritesheet_initialize(
		      (akgl_SpriteSheet *)*sheet,
		      ss_frame_width,
		      ss_frame_height,
		      (char *)&tmpstr->data)
		);
	}
    } CLEANUP {
	IGNORE(akgl_heap_release_string(ss_filename));
	IGNORE(akgl_heap_release_string(tmpstr));
    } PROCESS(errctx) {
    } FINISH(errctx, true);

    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_sprite_load_json(char *filename)
{
    PREPARE_ERROR(errctx);
    json_t *json = NULL;
    json_t *frames = NULL;
    json_error_t error;
    akgl_Sprite *obj = NULL;
    akgl_SpriteSheet *sheet = NULL;
    akgl_String *spritename = NULL;
    akgl_String *filename_copy = NULL;
    int i = 0;
    int framecount = 0;
    int frameid = 0;
    int jsonint = 0;

    FAIL_ZERO_RETURN(errctx, filename, AKERR_NULLPOINTER, "Received null filename");
    ATTEMPT {
	CATCH(errctx, akgl_heap_next_sprite(&obj));
	CATCH(errctx, akgl_heap_next_string(&filename_copy));
	FAIL_NONZERO_BREAK(
	    errctx,
	    (strlen(filename) >= AKGL_MAX_STRING_LENGTH),
	    AKERR_OUTOFBOUNDS,
	    "Sprite filename exceeds temporary string capacity"
	    );
	SDL_strlcpy(filename_copy->data, filename, AKGL_MAX_STRING_LENGTH);
	//CATCH(errctx, akgl_heap_next_string(&tmpstr));
	//CATCH(errctx, akgl_string_initialize(tmpstr, NULL));
	CATCH(errctx, akgl_heap_next_string(&spritename));
	CATCH(errctx, akgl_string_initialize(spritename, NULL));

	json = (json_t *)json_load_file(filename, 0, &error);
	FAIL_ZERO_BREAK(
	    errctx,
	    json,
	    AKERR_NULLPOINTER,
	    "Error while loading sprite from %s on line %d: %s", filename, error.line, error.text
	    );

	CATCH(errctx, sprite_load_json_spritesheet((json_t *)json, &sheet, dirname(filename_copy->data)));
	CATCH(errctx, akgl_get_json_string_value((json_t *)json, "name", &spritename));
	CATCH(errctx,
	      akgl_sprite_initialize(
		  (akgl_Sprite *)obj,
		  spritename->data,
		  (akgl_SpriteSheet *)sheet)
	    );

	// Read into an int and narrow deliberately. These three fields are
	// uint32_t and the accessor takes an int *, so passing them directly was
	// a signedness mismatch -- the kind -Wpointer-sign exists to catch, and
	// the kind a cast would have hidden.
	CATCH(errctx, akgl_get_json_integer_value((json_t *)json, "width", &jsonint));
	FAIL_NONZERO_BREAK(
	    errctx,
	    (jsonint <= 0),
	    AKERR_VALUE,
	    "Sprite %s has width %d; a sprite must be at least one pixel wide",
	    (char *)&obj->name,
	    jsonint);
	obj->width = (uint32_t)jsonint;

	CATCH(errctx, akgl_get_json_integer_value((json_t *)json, "height", &jsonint));
	FAIL_NONZERO_BREAK(
	    errctx,
	    (jsonint <= 0),
	    AKERR_VALUE,
	    "Sprite %s has height %d; a sprite must be at least one pixel high",
	    (char *)&obj->name,
	    jsonint);
	obj->height = (uint32_t)jsonint;

	// speed is milliseconds in the file and nanoseconds in the struct, and
	// the struct field is 32 bits, so anything past this overflows on the
	// multiply below rather than being held.
	CATCH(errctx, akgl_get_json_integer_value((json_t *)json, "speed", &jsonint));
	FAIL_NONZERO_BREAK(
	    errctx,
	    ((jsonint < 0) || ((uint32_t)jsonint > (UINT32_MAX / AKGL_TIME_ONEMS_NS))),
	    AKERR_OUTOFBOUNDS,
	    "Sprite %s has a frame speed of %d ms; the range is 0 to %u",
	    (char *)&obj->name,
	    jsonint,
	    (UINT32_MAX / AKGL_TIME_ONEMS_NS));
	obj->speed = ((uint32_t)jsonint) * AKGL_TIME_ONEMS_NS;
	CATCH(errctx, akgl_get_json_boolean_value((json_t *)json, "loop", &obj->loop));
	CATCH(errctx, akgl_get_json_boolean_value((json_t *)json, "loopReverse", &obj->loopReverse));

	CATCH(errctx, akgl_get_json_array_value((json_t *)json, "frames", &frames));
	// Bounded before anything is written. frameids is
	// AKGL_SPRITE_MAX_FRAMES bytes, and this loop used to take its count
	// straight from the document -- so a definition with seventeen frames
	// wrote past the array, past the rest of akgl_Sprite, and into the
	// neighbouring pool slot.
	framecount = (int)json_array_size((json_t *)frames);
	FAIL_NONZERO_BREAK(
	    errctx,
	    (framecount > AKGL_SPRITE_MAX_FRAMES),
	    AKERR_OUTOFBOUNDS,
	    "Sprite %s declares %d frames; the maximum is %d",
	    (char *)&obj->name,
	    framecount,
	    AKGL_SPRITE_MAX_FRAMES
	    );
	obj->frames = framecount;
	for ( i = 0 ; i < framecount; i++ ) {
	    // Read into an int and narrow deliberately. The old form wrote
	    // through a uint32_t * cast of a uint8_t *, so every element write
	    // touched four bytes; it only appeared to work because the next
	    // iteration overwrote the spill and the last one landed in the
	    // struct's alignment padding.
	    CATCH(errctx, akgl_get_json_array_index_integer((json_t *)frames, i, &frameid));
	    FAIL_NONZERO_BREAK(
		errctx,
		((frameid < 0) || (frameid > UINT8_MAX)),
		AKERR_OUTOFBOUNDS,
		"Sprite %s frame %d is %d; frame numbers are 0..%d",
		(char *)&obj->name,
		i,
		frameid,
		UINT8_MAX
		);
	    obj->frameids[i] = (uint8_t)frameid;
	}
    } CLEANUP {
	// The sprite copies every field it wants out of the document and the
	// sheet is found by resolved path, so nothing here points into the
	// parsed tree once this block runs. Released on both paths.
	if ( json != NULL ) {
	    json_decref(json);
	    json = NULL;
	}
	if ( errctx != NULL && errctx->status != 0 ) {
	    IGNORE(akgl_heap_release_sprite(obj));
	    IGNORE(akgl_heap_release_spritesheet(sheet));
	}
	IGNORE(akgl_heap_release_string(spritename));
	IGNORE(akgl_heap_release_string(filename_copy));
	//IGNORE(akgl_heap_release_string(tmpstr));
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_sprite_initialize(akgl_Sprite *spr, char *name, akgl_SpriteSheet *sheet)
{
    PREPARE_ERROR(errctx);
    FAIL_ZERO_RETURN(errctx, spr, AKERR_NULLPOINTER, "Null sprite reference");
    FAIL_ZERO_RETURN(errctx, name, AKERR_NULLPOINTER, "Empty sprite name");
    FAIL_ZERO_RETURN(errctx, sheet, AKERR_NULLPOINTER, "Null spritesheet reference");

    memset(spr, 0x00, sizeof(akgl_Sprite));
    // Was a memcpy of the full field width from a NUL-terminated string, which
    // reads past the end of any name shorter than the field. Copies only what
    // is there now, and terminates.
    PASS(errctx, aksl_strncpy(spr->name, sizeof(spr->name), name, sizeof(spr->name) - 1));
    spr->sheet = sheet;
    FAIL_ZERO_RETURN(
	errctx,
	SDL_SetPointerProperty(AKGL_REGISTRY_SPRITE, (char *)&spr->name, (void *)spr),
	AKERR_KEY,
	"Unable to add sprite to registry");
    spr->refcount += 1;
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_spritesheet_initialize(akgl_SpriteSheet *sheet, int sprite_w, int sprite_h, char *filename)
{
    PREPARE_ERROR(errctx);
    //akgl_String *tmpstr = NULL;

    ATTEMPT {
	FAIL_ZERO_BREAK(errctx, sheet, AKERR_NULLPOINTER, "Null spritesheet pointer");
	FAIL_ZERO_BREAK(errctx, filename, AKERR_NULLPOINTER, "Null filename pointer");

	memset(sheet, 0x00, sizeof(akgl_SpriteSheet));

	//CATCH(errctx, akgl_heap_next_string(&tmpstr));

	//CATCH(errctx, akgl_string_initialize(tmpstr, NULL));
	CATCH(errctx, aksl_strncpy((char *)&sheet->name, sizeof(sheet->name), filename, sizeof(sheet->name) - 1));

	sheet->texture = IMG_LoadTexture(akgl_renderer->sdl_renderer, filename);
	FAIL_ZERO_BREAK(errctx, sheet->texture, AKGL_ERR_SDL, "Failed loading asset %s : %s", filename, SDL_GetError());

	FAIL_ZERO_BREAK(
	    errctx,
	    SDL_SetPointerProperty(AKGL_REGISTRY_SPRITESHEET, (char *)sheet->name, (void *)sheet),
	    AKERR_KEY,
	    "Unable to add spritesheet to registry: %s",
	    SDL_GetError());
	sheet->refcount += 1;
    } CLEANUP {
	//IGNORE(akgl_heap_release_string(tmpstr));
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}
