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

#include <limits.h>
#include <stdlib.h>
#include <errno.h>
#include <libgen.h>

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

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

#include <akstdlib.h>

/**
 * @brief Resolve @p path against @p root and write the absolute result to @p dest.
 *
 * The second half of akgl_path_relative: joins the two with a `/` and resolves
 * the join, so symlinks and `..` are folded out and the result is absolute. It
 * is the fallback, reached only once resolving @p path on its own has failed.
 *
 * `static`: it is reachable only through akgl_path_relative, which is the only
 * caller and the only sensible one.
 *
 * @param root Directory to resolve against, normally `dirname` of the file that
 *             named @p path. Required. A trailing `/` is harmless; the join adds
 *             one unconditionally and `realpath` collapses the double.
 * @param path Relative path to resolve. Required. An absolute @p path still gets
 *             @p root pasted in front of it, which will not exist -- so callers
 *             must not send absolute paths down this branch.
 * @param dest  Receives the resolved absolute path. Required, and must already be
 *             a claimed pool string.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKERR_NULLPOINTER If @p root, @p path, or @p dest is `NULL`.
 * @throws AKERR_OUTOFBOUNDS If `root + path` is at least #AKGL_MAX_STRING_LENGTH
 *         bytes. The message reports both the combined length and the limit.
 * @throws ENOENT If the joined path does not exist. Any other `errno`
 *         `realpath(3)` raises -- EACCES, ELOOP, ENOTDIR -- propagates likewise.
 * @throws AKGL_ERR_HEAP If the string pool cannot supply the two scratch buffers.
 *
 * @note The AKERR_OUTOFBOUNDS check uses `FAIL_RETURN` from inside the `ATTEMPT`
 *       block, which returns past `CLEANUP` -- so on that one path the two
 *       scratch strings are never released. This is exactly the hazard AGENTS.md
 *       warns about; it wants a `FAIL_BREAK`.
 */
static akerr_ErrorContext *path_relative_root(char *root, char *path, akgl_String *dest)
{
    PREPARE_ERROR(errctx);
    akgl_String *pathbuf;
    akgl_String *strbuf;
    int rootlen;
    int pathlen;
    int count;

    FAIL_ZERO_RETURN(errctx, root, AKERR_NULLPOINTER, "NULL argument");
    FAIL_ZERO_RETURN(errctx, path, AKERR_NULLPOINTER, "NULL argument");
    FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL argument");

    PASS(errctx, akgl_heap_next_string(&strbuf));
    PASS(errctx, akgl_heap_next_string(&pathbuf));

    ATTEMPT {
	// Is it relative to the root?
	rootlen = strlen(root);
	pathlen = strlen(path);
	if ( (rootlen + pathlen) >= AKGL_MAX_STRING_LENGTH ) {
	    FAIL_BREAK(errctx, AKERR_OUTOFBOUNDS, "Total path length (%d) is greater than maximum akgl_String length (%d)", (rootlen + pathlen), AKGL_MAX_STRING_LENGTH);
	}
	CATCH(errctx, aksl_snprintf(
		  &count,
		  (char *)&pathbuf->data,
		  sizeof(pathbuf->data),
		  "%s/%s",
		  root,
		  path
		  ));
	CATCH(errctx, aksl_realpath((char *)&pathbuf->data, (char *)&strbuf->data, sizeof(strbuf->data)));
	CATCH(errctx, akgl_string_copy(strbuf, dest, 0));
    } CLEANUP {
	IGNORE(akgl_heap_release_string(strbuf));
	IGNORE(akgl_heap_release_string(pathbuf));
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_path_relative(char *root, char *path, akgl_String *dest)
{
    PREPARE_ERROR(errctx);
    akgl_String *strbuf;
    bool relative_to_root = false;

    FAIL_ZERO_RETURN(errctx, root, AKERR_NULLPOINTER, "NULL argument");
    FAIL_ZERO_RETURN(errctx, path, AKERR_NULLPOINTER, "NULL argument");
    FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL argument");

    PASS(errctx, akgl_heap_next_string(&strbuf));

    ATTEMPT {
	// Is path relative to our current working directory?
	CATCH(errctx, aksl_realpath(path, (char *)&strbuf->data, sizeof(strbuf->data)));
	// Yes it is. strbuf->data contains the absolute path.
	CATCH(errctx, akgl_string_copy(strbuf, dest, 0));
    } CLEANUP {
	IGNORE(akgl_heap_release_string(strbuf));
    } PROCESS(errctx) {
    } HANDLE(errctx, ENOENT) {
	// Path is not relative to our current working directory. Resolve it
	// against root instead -- but after FINISH, not from in here. Returning
	// from inside a HANDLE block skips the RELEASE_ERROR that FINISH ends
	// with, so the handled context is never given back to
	// AKERR_ARRAY_ERROR. That leaks one context per call, and the 129th
	// call takes the whole process down with "Unable to pull an error
	// context from the array!". Every map load resolves several paths this
	// way.
	relative_to_root = true;
    } FINISH(errctx, true);

    if ( relative_to_root == true ) {
	PASS(errctx, path_relative_root(root, path, dest));
    }
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_collide_rectangles(SDL_FRect *r1, SDL_FRect *r2, bool *collide)
{
    PREPARE_ERROR(errctx);
    FAIL_ZERO_RETURN(errctx, r1, AKERR_NULLPOINTER, "NULL rectangle reference");
    FAIL_ZERO_RETURN(errctx, r2, AKERR_NULLPOINTER, "NULL rectangle reference");
    FAIL_ZERO_RETURN(errctx, collide, AKERR_NULLPOINTER, "NULL collision flag reference");

    /*
     * Two rectangles overlap when their spans overlap on both axes. This used to
     * ask a different question -- whether either rectangle enclosed a corner of
     * the other, eight akgl_collide_point_rectangle calls -- and that question
     * has a different answer for a tall thin rectangle crossing a short wide
     * one. They overlap in a plus sign, neither holds a corner of the other, and
     * all eight tests said no. util.h carried a @note describing the arrangement
     * rather than a fix.
     *
     * `<=` and not `<`: two rectangles sharing exactly one edge collide. That is
     * what the corner form did, inclusive on all four edges, and changing it
     * would silently change what every pickup test in a consumer answers.
     *
     * The comparison is in float now. The corner form went through akgl_Point,
     * whose members were int, so a rectangle at x = 10.9 had its corner at 10
     * and a sub-pixel overlap was invisible. That type is gone with the corner
     * functions, in 0.8.0.
     */
    *collide = ( (r1->x <= (r2->x + r2->w)) &&
		 (r2->x <= (r1->x + r1->w)) &&
		 (r1->y <= (r2->y + r2->h)) &&
		 (r2->y <= (r1->y + r1->h)) );

    SUCCEED_RETURN(errctx);
}


akerr_ErrorContext *akgl_compare_sdl_surfaces(SDL_Surface *s1, SDL_Surface *s2)
{
    PREPARE_ERROR(errctx);
    FAIL_ZERO_RETURN(errctx, s1, AKERR_NULLPOINTER, "NULL Surface pointer");
    FAIL_ZERO_RETURN(errctx, s2, AKERR_NULLPOINTER, "NULL Surface pointer");
    // Geometry first. The memcmp reads s1->pitch * s1->h bytes out of *both*,
    // so a smaller s2 was read past its end rather than reported as a
    // mismatch -- and a differing pitch or format made the comparison
    // meaningless even when it did not run off.
    FAIL_NONZERO_RETURN(
	errctx,
	((s1->w != s2->w) || (s1->h != s2->h)),
	AKERR_VALUE,
	"Comparison surfaces differ in size: %dx%d against %dx%d",
	s1->w, s1->h, s2->w, s2->h);
    FAIL_NONZERO_RETURN(
	errctx,
	(s1->pitch != s2->pitch),
	AKERR_VALUE,
	"Comparison surfaces differ in pitch: %d against %d",
	s1->pitch, s2->pitch);
    FAIL_NONZERO_RETURN(
	errctx,
	(s1->format != s2->format),
	AKERR_VALUE,
	"Comparison surfaces differ in pixel format: %s against %s",
	SDL_GetPixelFormatName(s1->format), SDL_GetPixelFormatName(s2->format));
    FAIL_ZERO_RETURN(errctx, s1->pixels, AKERR_NULLPOINTER, "First surface has no pixels");
    FAIL_ZERO_RETURN(errctx, s2->pixels, AKERR_NULLPOINTER, "Second surface has no pixels");
    FAIL_NONZERO_RETURN(errctx, memcmp(s1->pixels, s2->pixels, (s1->pitch * s1->h)), AKERR_VALUE, "Comparison surfaces are not equal");
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_render_and_compare(SDL_Texture *t1, SDL_Texture *t2, int x, int y, int w, int h, char *writeout)
{
    SDL_Surface *s1 = NULL;
    SDL_Surface *s2 = NULL;
    SDL_FRect src = {.x = x, .y = y, .w = w, .h = h};
    SDL_FRect dest = {.x = x, .y = y, .w = w, .h = h};
    SDL_Rect read = {.x = x, .y = y, .w = w, .h = h};
    akgl_String *tmpstring = NULL;
    int count = 0;

    PREPARE_ERROR(errctx);
    ATTEMPT {
	FAIL_ZERO_BREAK(errctx, t1, AKERR_NULLPOINTER, "NULL texture");
	FAIL_ZERO_BREAK(errctx, t2, AKERR_NULLPOINTER, "NULL texture");

	CATCH(errctx, akgl_heap_next_string(&tmpstring));
	SDL_RenderClear(akgl_renderer->sdl_renderer);
	CATCH(errctx, akgl_renderer->draw_texture(akgl_renderer, t1, &src, &dest, 0, NULL, SDL_FLIP_NONE));
	s1 = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, &read);
	FAIL_ZERO_BREAK(errctx, s1, AKGL_ERR_SDL, "Failed to read pixels from renderer");

	if ( writeout != NULL ) {
	    CATCH(errctx, aksl_snprintf(
		      &count,
		      (char *)&tmpstring->data,
		      sizeof(tmpstring->data),
		      "%s%s",
		      SDL_GetBasePath(),
		      writeout));
	    FAIL_ZERO_BREAK(
		errctx,
		IMG_SavePNG(s1, (char *)&tmpstring->data),
		AKERR_IO,
		"Unable to save %s: %s",
		(char *)&tmpstring->data,
		SDL_GetError());
	}

	SDL_RenderClear(akgl_renderer->sdl_renderer);

	CATCH(errctx, akgl_renderer->draw_texture(akgl_renderer, t2, &src, &dest, 0, NULL, SDL_FLIP_NONE));
	s2 = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, &read);
	FAIL_ZERO_BREAK(errctx, s2, AKGL_ERR_SDL, "Failed to read pixels from renderer");

	CATCH(errctx, akgl_compare_sdl_surfaces(s1, s2));
    } CLEANUP {
	if ( s1 != NULL )
	    SDL_DestroySurface(s1);
	if ( s2 != NULL )
	    SDL_DestroySurface(s2);
	IGNORE(akgl_heap_release_string(tmpstring));
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}
