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

#include <akerror.h>
#include <SDL3/SDL.h>
#include <SDL3_ttf/SDL_ttf.h>
#include <akgl/text.h>
#include <akgl/registry.h>
#include <akgl/game.h>

akerr_ErrorContext *akgl_text_loadfont(char *name, char *filepath, int size)
{
    TTF_Font *font = NULL;

    PREPARE_ERROR(errctx);
    FAIL_ZERO_RETURN(errctx, name, AKERR_NULLPOINTER, "Null font name");
    FAIL_ZERO_RETURN(errctx, filepath, AKERR_NULLPOINTER, "Null filepath");
    font = TTF_OpenFont(filepath, size);
    FAIL_ZERO_RETURN(errctx, font, AKGL_ERR_SDL, "%s", SDL_GetError());
    // Loading over an existing name used to abandon the font it displaced --
    // there was nothing in the library that could close one. The old font goes
    // back first now, and only once the new one has opened, so a failed load
    // leaves the caller with the font they already had.
    if ( SDL_GetPointerProperty(AKGL_REGISTRY_FONT, name, NULL) != NULL ) {
	PASS(errctx, akgl_text_unloadfont(name));
    }
    FAIL_ZERO_RETURN(
	errctx,
	SDL_SetPointerProperty(AKGL_REGISTRY_FONT, name, (void *)font),
	AKERR_KEY,
	"Unable to add font %p to registry as %s : %s",
	(void *)font,
	name,
	SDL_GetError());
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_text_unloadfont(char *name)
{
    TTF_Font *font = NULL;

    PREPARE_ERROR(errctx);
    FAIL_ZERO_RETURN(errctx, name, AKERR_NULLPOINTER, "Null font name");

    font = (TTF_Font *)SDL_GetPointerProperty(AKGL_REGISTRY_FONT, name, NULL);
    FAIL_ZERO_RETURN(errctx, font, AKERR_KEY, "No font named %s in the registry", name);

    // Cleared before the close, so a font is never reachable through the
    // registry after it has been handed back to SDL_ttf.
    SDL_ClearProperty(AKGL_REGISTRY_FONT, name);
    TTF_CloseFont(font);
    SUCCEED_RETURN(errctx);
}

/**
 * @brief SDL_EnumerateProperties callback: close one registered font.
 *
 * The properties set cannot be modified while it is being enumerated, so this
 * only closes; akgl_text_unloadallfonts clears the whole set afterwards.
 */
static void unload_font_iterate(void *userdata, SDL_PropertiesID props, const char *name)
{
    TTF_Font *font = NULL;

    if ( name == NULL ) {
	return;
    }
    font = (TTF_Font *)SDL_GetPointerProperty(props, name, NULL);
    if ( font != NULL ) {
	TTF_CloseFont(font);
    }
}

akerr_ErrorContext *akgl_text_unloadallfonts(void)
{
    PREPARE_ERROR(errctx);

    if ( AKGL_REGISTRY_FONT == 0 ) {
	SUCCEED_RETURN(errctx);
    }
    SDL_EnumerateProperties(AKGL_REGISTRY_FONT, &unload_font_iterate, NULL);
    SDL_DestroyProperties(AKGL_REGISTRY_FONT);
    AKGL_REGISTRY_FONT = 0;
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_text_rendertextat(TTF_Font *font, char *text, SDL_Color color, int wraplength, int x, int y)
{
    SDL_Surface *textsurf = NULL;
    SDL_Texture *texture = NULL;
    SDL_FRect dest;

    PREPARE_ERROR(errctx);
    FAIL_ZERO_RETURN(errctx, font, AKERR_NULLPOINTER, "NULL font");
    FAIL_ZERO_RETURN(errctx, text, AKERR_NULLPOINTER, "NULL text string");
    // Checked before anything is rasterized, and checked at all because a
    // backend that has an SDL_Renderer but was never run through
    // akgl_render_2d_bind has a NULL draw_texture -- which is exactly the state
    // akgl_render_2d_init used to be the only escape from.
    FAIL_ZERO_RETURN(errctx, akgl_renderer, AKERR_NULLPOINTER, "No renderer backend");
    FAIL_ZERO_RETURN(errctx, akgl_renderer->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
    FAIL_ZERO_RETURN(errctx, akgl_renderer->draw_texture, AKERR_NULLPOINTER, "Renderer backend has no draw_texture");
    // Drawing nothing is not a failure. SDL_ttf returns NULL with "Text has
    // zero width" from both rasterizers for "", so this used to report
    // AKERR_NULLPOINTER for what a caller means as "draw an empty line" --
    // while akgl_text_measure("") is documented as legal and returns 0 wide by
    // one line high. Two halves of one header disagreeing about one string.
    if ( text[0] == '\0' ) {
	SUCCEED_RETURN(errctx);
    }

    if ( wraplength > 0 ) {
	textsurf = TTF_RenderText_Blended_Wrapped(
	    font,
	    text,
	    0,
	    color,
	    wraplength);
    } else {
	textsurf = TTF_RenderText_Blended(
	    font,
	    text,
	    0,
	    color);
    }
    FAIL_ZERO_RETURN(errctx, textsurf, AKERR_NULLPOINTER, "%s", SDL_GetError());

    // Both were destroyed only after the draw succeeded, so a failed texture
    // upload leaked the surface and a failed draw leaked both -- on a HUD line
    // redrawn every frame, that is a leak per frame.
    ATTEMPT {
	texture = SDL_CreateTextureFromSurface(akgl_renderer->sdl_renderer, textsurf);
	FAIL_ZERO_BREAK(errctx, texture, AKERR_NULLPOINTER, "%s", SDL_GetError());
	dest.x = x;
	dest.y = y;
	SDL_GetTextureSize(texture, &dest.w, &dest.h);
	CATCH(errctx, akgl_renderer->draw_texture(akgl_renderer, texture, NULL, &dest, 0, NULL, SDL_FLIP_NONE));
    } CLEANUP {
	if ( texture != NULL ) {
	    SDL_DestroyTexture(texture);
	    texture = NULL;
	}
	SDL_DestroySurface(textsurf);
	textsurf = NULL;
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_text_measure(TTF_Font *font, char *text, int *w, int *h)
{
    PREPARE_ERROR(errctx);
    FAIL_ZERO_RETURN(errctx, font, AKERR_NULLPOINTER, "NULL font");
    FAIL_ZERO_RETURN(errctx, text, AKERR_NULLPOINTER, "NULL text string");
    FAIL_ZERO_RETURN(errctx, w, AKERR_NULLPOINTER, "NULL width destination");
    FAIL_ZERO_RETURN(errctx, h, AKERR_NULLPOINTER, "NULL height destination");
    // A zero length means "the string is null terminated", not "the empty
    // string" -- an empty text measures 0 wide and one line high.
    FAIL_ZERO_RETURN(
	errctx,
	TTF_GetStringSize(font, text, 0, w, h),
	AKGL_ERR_SDL,
	"%s",
	SDL_GetError());
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_text_measure_wrapped(TTF_Font *font, char *text, int wraplength, int *w, int *h)
{
    PREPARE_ERROR(errctx);
    FAIL_ZERO_RETURN(errctx, font, AKERR_NULLPOINTER, "NULL font");
    FAIL_ZERO_RETURN(errctx, text, AKERR_NULLPOINTER, "NULL text string");
    FAIL_ZERO_RETURN(errctx, w, AKERR_NULLPOINTER, "NULL width destination");
    FAIL_ZERO_RETURN(errctx, h, AKERR_NULLPOINTER, "NULL height destination");
    // SDL_ttf takes the wrap width as an int and reads a negative one as a
    // very large unsigned width, which silently disables wrapping instead of
    // reporting anything. Refuse it here rather than return a wrong measurement.
    FAIL_NONZERO_RETURN(
	errctx,
	(wraplength < 0),
	AKERR_OUTOFBOUNDS,
	"Wrap length %d is negative",
	wraplength);
    FAIL_ZERO_RETURN(
	errctx,
	TTF_GetStringSizeWrapped(font, text, 0, wraplength, w, h),
	AKGL_ERR_SDL,
	"%s",
	SDL_GetError());
    SUCCEED_RETURN(errctx);
}
