#include <SDL3/SDL.h>
#include <SDL3_image/SDL_image.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <akerror.h>

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

#include "testutil.h"


akerr_ErrorContext *test_akgl_spritesheet_initialize(void)
{
    PREPARE_ERROR(errctx);
    akgl_SpriteSheet *sheet = NULL;
    SDL_Texture *image = NULL;
    akgl_String *tmpstr = NULL;
    // Does the image file get loaded?
    // Is the image file loaded correctly? (Surface comparison)
    // Is the spritesheet in the registry?
    ATTEMPT {
	CATCH(errctx, akgl_heap_next_spritesheet(&sheet));
	CATCH(errctx, akgl_heap_next_string(&tmpstr));

	snprintf((char *)&tmpstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/spritesheet.png");
	CATCH(errctx, akgl_spritesheet_initialize(sheet, 48, 48, "assets/spritesheet.png"));
	FAIL_ZERO_BREAK(errctx, sheet->texture, AKERR_VALUE, "akgl_spritesheet_initialize failed to load the sprite texture");
	FAIL_NONZERO_BREAK(
	    errctx,
	    ((sheet->texture->w != 576) || (sheet->texture->h != 384)),
	    AKERR_VALUE,
	    "Loaded texture was not the correct size");

	snprintf((char *)&tmpstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/spritesheet.png");
	image = IMG_LoadTexture(akgl_renderer->sdl_renderer, (char *)&tmpstr->data);
	FAIL_ZERO_BREAK(errctx, image, AKGL_ERR_SDL, "Failed to load comparison image");

	CATCH(
	    errctx,
	    akgl_render_and_compare(
		sheet->texture,
		image,
		0, 0, 576, 384,
		"test_spritesheet_loaded_image.png")
	    );

	FAIL_ZERO_BREAK(
	    errctx,
	    SDL_GetPointerProperty(AKGL_REGISTRY_SPRITESHEET, "assets/spritesheet.png", NULL),
	    AKERR_KEY,
	    "Spritesheet was not placed in the registry");

    } CLEANUP {
	IGNORE(akgl_heap_release_string(tmpstr));
	IGNORE(akgl_heap_release_spritesheet(sheet));
	if ( image != NULL )
	    SDL_DestroyTexture(image);
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *test_akgl_sprite_initialize(void)
{
    PREPARE_ERROR(errctx);
    akgl_SpriteSheet *testsheet = NULL;
    akgl_Sprite *testsprite = NULL;
    akgl_String *tmpstr = NULL;

    // Does the sprite get loaded?
    // Do all frames of the sprite get loaded?
    // Are all the frames of the sprite what we expect? (Surface comparison)
    // Is the sprite added to the registry?
    ATTEMPT {
	CATCH(errctx, akgl_heap_next_spritesheet(&testsheet));
	CATCH(errctx, akgl_heap_next_sprite(&testsprite));
	CATCH(errctx, akgl_heap_next_string(&tmpstr));

	snprintf((char *)&tmpstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/spritesheet.png");
	CATCH(errctx, akgl_spritesheet_initialize(testsheet, 48, 48, "assets/spritesheet.png"));
	FAIL_ZERO_BREAK(errctx, testsheet, AKERR_VALUE, "akgl_spritesheet_initialize failed");

	CATCH(errctx, akgl_sprite_initialize(testsprite, "test", testsheet));
	FAIL_NONZERO_BREAK(errctx, (testsprite->sheet != testsheet), AKERR_VALUE, "Initialized sprite uses wrong sheet");
	FAIL_ZERO_BREAK(
	    errctx,
	    SDL_GetPointerProperty(AKGL_REGISTRY_SPRITE, "test", NULL),
	    AKERR_KEY,
	    "Sprite was not placed in the registry");

    } CLEANUP {
	IGNORE(akgl_heap_release_sprite(testsprite));
	IGNORE(akgl_heap_release_string(tmpstr));
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *test_akgl_sprite_load_json(void)
{
    PREPARE_ERROR(errctx);
    akgl_Sprite *testsprite = NULL;
    akgl_Sprite *testsprite2 = NULL;
    akgl_String *tmpstr = NULL;
    SDL_Texture *image = NULL;

    // Does the sprite get loaded?
    // Do all frames of the sprite get loaded?
    // Are all the frames of the sprite what we expect? (Surface comparison)
    // Is the sprite added to the registry?
    ATTEMPT {
	CATCH(errctx, akgl_heap_next_string(&tmpstr));

	CATCH(errctx, akgl_sprite_load_json("assets/testsprite.json"));
	testsprite = SDL_GetPointerProperty(AKGL_REGISTRY_SPRITE, "testsprite", NULL);
	FAIL_ZERO_BREAK(
	    errctx,
	    testsprite,
	    AKERR_KEY,
	    "akgl_sprite_load_json succeeds but sprite is not placed in the registry");

	FAIL_ZERO_BREAK(errctx, (testsprite->width == 48), AKERR_VALUE, "width incorrect (48 : %d)", testsprite->width);
	FAIL_ZERO_BREAK(errctx, (testsprite->height == 48), AKERR_VALUE, "height incorrect (48 : %d)", testsprite->height);
	FAIL_ZERO_BREAK(errctx, (testsprite->speed == 100000000), AKERR_VALUE, "speed incorrect (100 : %d)", testsprite->speed);
	FAIL_ZERO_BREAK(errctx, (testsprite->loop == true), AKERR_VALUE, "loop incorrect (1 : %d)", testsprite->loop);
	FAIL_ZERO_BREAK(errctx, (testsprite->loopReverse == true), AKERR_VALUE, "loopReverse incorrect (1 : %d)", testsprite->loopReverse);
	FAIL_ZERO_BREAK(errctx, (testsprite->frames == 3), AKERR_VALUE, "frame count incorrect (3 : %d)", testsprite->frames);
	FAIL_ZERO_BREAK(errctx, (testsprite->frameids[0] == 12), AKERR_VALUE, "frameids[0] incorrect (12 : %d)", testsprite->frameids[0]);
	FAIL_ZERO_BREAK(errctx, (testsprite->frameids[1] == 13), AKERR_VALUE, "frameids[1] incorrect (13 : %d)", testsprite->frameids[1]);
	FAIL_ZERO_BREAK(errctx, (testsprite->frameids[2] == 14), AKERR_VALUE, "frameids[2] incorrect (14 : %d)", testsprite->frameids[2]);
	FAIL_NONZERO_BREAK(errctx, strcmp(testsprite->name, "testsprite"), AKERR_VALUE, "name incorrect (testsprite : %s)", (char *)testsprite->name);

	// Is it using the right spritesheet?

	snprintf((char *)&tmpstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/spritesheet.png");
	image = IMG_LoadTexture(akgl_renderer->sdl_renderer, (char *)&tmpstr->data);
	FAIL_ZERO_BREAK(errctx, image, AKGL_ERR_SDL, "Failed to load comparison image");

	CATCH(
	    errctx,
	    akgl_render_and_compare(
		testsprite->sheet->texture,
		image,
		0, 0, 576, 384,
		"test_sprite_loaded_from_json_sheet.png"
		)
	    );

	// If we load a second sprite using the same sheet name, do they use the same sheet in memory?
	snprintf((char *)&tmpstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/testsprite2.json");
	CATCH(errctx, akgl_sprite_load_json("assets/testsprite2.json"));
	testsprite2 = SDL_GetPointerProperty(AKGL_REGISTRY_SPRITE, "testsprite2", NULL);
	FAIL_ZERO_BREAK(
	    errctx,
	    testsprite,
	    AKERR_KEY,
	    "akgl_sprite_load_json succeeds but second sprite is not placed in the registry");

	FAIL_ZERO_BREAK(
	    errctx,
	    (testsprite->sheet == testsprite2->sheet),
	    AKERR_VALUE,
	    "Previously loaded spritesheets are not reused");

    } CLEANUP {
	if ( testsprite != NULL ) {
	    IGNORE(akgl_heap_release_sprite(testsprite));
	}
	if ( testsprite2 != NULL ) {
	    IGNORE(akgl_heap_release_sprite(testsprite2));
	}
	IGNORE(akgl_heap_release_string(tmpstr));
	if ( image != NULL )
	    SDL_DestroyTexture(image);
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

/**
 * @brief A sprite definition must not be able to write past `frameids`.
 *
 * `frameids` is AKGL_SPRITE_MAX_FRAMES bytes and the loader took 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 next pool slot. It also
 * wrote through a `uint32_t *` cast of a `uint8_t *`, four bytes at a time,
 * which is why the overrun reached four bytes past the array rather than one.
 *
 * The neighbouring slot is claimed and stamped first, so the test fails on the
 * corruption rather than on whatever the corruption happens to do later.
 */
akerr_ErrorContext *test_akgl_sprite_load_json_bounds_frames(void)
{
    PREPARE_ERROR(errctx);
    akgl_Sprite *loaded = NULL;
    int i = 0;

    ATTEMPT {
	// Exactly the maximum is legal and must still load.
	TEST_EXPECT_OK(errctx, akgl_sprite_load_json("assets/testsprite_maxframes.json"),
		       "loading a sprite with exactly AKGL_SPRITE_MAX_FRAMES frames");
	loaded = SDL_GetPointerProperty(AKGL_REGISTRY_SPRITE, "testsprite_maxframes", NULL);
	FAIL_ZERO_BREAK(errctx, loaded, AKERR_KEY, "the max-frames sprite is not in the registry");
	TEST_ASSERT(errctx, loaded->frames == AKGL_SPRITE_MAX_FRAMES,
		    "max-frames sprite loaded %d frames, expected %d",
		    loaded->frames, AKGL_SPRITE_MAX_FRAMES);
	for ( i = 0; i < AKGL_SPRITE_MAX_FRAMES; i++ ) {
	    TEST_ASSERT(errctx, loaded->frameids[i] == (uint8_t)i,
			"max-frames sprite frame %d is %d, expected %d",
			i, loaded->frameids[i], i);
	}

	// One more than the maximum is refused, and nothing is registered.
	TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
			   akgl_sprite_load_json("assets/testsprite_toomanyframes.json"),
			   "loading a sprite with more frames than the array holds");
	TEST_ASSERT(errctx,
		    SDL_GetPointerProperty(AKGL_REGISTRY_SPRITE, "testsprite_toomanyframes", NULL) == NULL,
		    "a sprite with too many frames was registered anyway");

	// A frame number a uint8_t cannot hold is refused rather than truncated
	// to something that indexes a different tile.
	TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
			   akgl_sprite_load_json("assets/testsprite_widecount.json"),
			   "loading a sprite whose frame number does not fit a uint8_t");
    } CLEANUP {
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

int main(void)
{
    PREPARE_ERROR(errctx);

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

    ATTEMPT {
	CATCH(errctx, akgl_error_init());
	akgl_renderer = &akgl_default_renderer;

	SDL_SetAppMetadata("SDL3-GameTest", "0.1", "net.aklabs.sdl3-gametest");

	if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_JOYSTICK | SDL_INIT_AUDIO )) {
	    FAIL_BREAK(errctx, AKGL_ERR_SDL, "Couldn't initialize SDL: %s", SDL_GetError());
	}

	if (!SDL_CreateWindowAndRenderer("net/aklabs/libakgl/test_sprite", 640, 480, 0, &akgl_window, &akgl_renderer->sdl_renderer)) {
	    FAIL_BREAK(errctx, AKGL_ERR_SDL, "Couldn't create window/renderer: %s", SDL_GetError());
	}
	akgl_renderer->draw_texture = &akgl_render_2d_draw_texture;

	CATCH(errctx, akgl_heap_init());
	CATCH(errctx, akgl_registry_init_sprite());
	CATCH(errctx, akgl_registry_init_spritesheet());

	CATCH(errctx, test_akgl_spritesheet_initialize());
	CATCH(errctx, test_akgl_sprite_initialize());
	CATCH(errctx, test_akgl_sprite_load_json());
	CATCH(errctx, test_akgl_sprite_load_json_bounds_frames());
    } CLEANUP {
    } PROCESS(errctx) {
    } FINISH_NORETURN(errctx);
    return 0;
}
