/**
 * @file sprite_akgl.c
 * @brief Wires the sprite backend record to libakgl's actors.
 *
 * Each of BASIC's eight sprites becomes a full libakgl object graph -- a
 * spritesheet holding one texture, a sprite cutting one frame from it, a
 * character mapping state 0 to that sprite, and an actor instantiating the
 * character -- so a host game sees BASIC's sprites in libakgl's actor registry
 * alongside its own and can do anything to them it can do to an actor.
 *
 * **None of that comes from JSON.** akgl_sprite_load_json() is a thin wrapper
 * over the same four initializers plus writes to public struct fields, and every
 * field it fills from a document -- frame list, animation speed, loop flags,
 * state-to-sprite map -- is something a Commodore sprite does not have. The one
 * exception is the image, and that is exactly where this file *does* call
 * libakgl: `SPRSAV "ship.png", 1` resolves through akgl_path_relative() and
 * loads through akgl_spritesheet_initialize(), the same two calls
 * akgl_sprite_load_json_spritesheet() makes.
 *
 * Every actor gets a renderfunc of its own rather than akgl_actor_render(). It
 * was two reasons, both filed upstream, and **libakgl 0.5.0 fixed one of them**:
 * the default used to compute its destination height from the sprite's *width*,
 * drawing a 24x21 Commodore sprite as a 24x24 square, and it now uses the
 * height. That was libakgl defect 26.
 *
 * The remaining reason is the one that still requires this file: an akgl_Actor
 * carries a single scalar `scale` applied to both axes, which cannot express
 * SPRITE's separate x- and y-expand bits. libakgl records that as an open item
 * and names this interpreter as the caller that needs it; the shape it takes --
 * scale_x/scale_y beside scale, or replacing scale outright -- is a design
 * decision over there rather than a patch. Until it lands, installing a function
 * pointer is libakgl's own extension point for exactly this, so nothing is
 * forked.
 */

#include <errno.h>

#include <SDL3_image/SDL_image.h>

#include <akerror.h>
#include <akstdlib.h>

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

#include <akbasic/akgl.h>
#include <akbasic/error.h>

/** @brief The colour conversion, same as src/graphics_akgl.c's. */
static SDL_Color to_sdl(akbasic_Color color)
{
    SDL_Color out;

    out.r = color.r;
    out.g = color.g;
    out.b = color.b;
    out.a = color.a;
    return out;
}

/** @brief Recover the backend's own state, or say that it has none. */
static akerr_ErrorContext *state_of(akbasic_SpriteBackend *self, akbasic_AkglSprites **dest)
{
    PREPARE_ERROR(errctx);

    FAIL_ZERO_RETURN(errctx, (self != NULL && dest != NULL), AKERR_NULLPOINTER,
		     "NULL argument in akgl sprite backend");
    *dest = (akbasic_AkglSprites *)self->self;
    FAIL_ZERO_RETURN(errctx, (*dest != NULL), AKERR_NULLPOINTER,
		     "akgl sprite backend has no state");
    SUCCEED_RETURN(errctx);
}

/** @brief Recover the state and turn a BASIC sprite number into a slot index. */
static akerr_ErrorContext *slot_of(akbasic_SpriteBackend *self, int n, akbasic_AkglSprites **state, int *index)
{
    PREPARE_ERROR(errctx);

    PASS(errctx, state_of(self, state));
    FAIL_ZERO_RETURN(errctx, (n >= 1 && n <= AKBASIC_MAX_SPRITES), AKBASIC_ERR_BOUNDS,
		     "Sprite %d is outside 1..%d", n, AKBASIC_MAX_SPRITES);
    *index = n - 1;
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Draw one BASIC sprite, in place of akgl_actor_render().
 *
 * Differs from the default in exactly one way now, noted at the top of this file:
 * the two expansion bits scale the axes independently, where an actor's single
 * `scale` cannot. Taking the destination height from the sprite's height used to
 * be the second difference and is the default's behaviour as of libakgl 0.5.0 --
 * the line below is no longer a correction, only agreement.
 *
 * Like the default, it skips rather than reports: an actor with no image, one
 * that is hidden, one whose slot has been reused. A frame is not the place to
 * fail.
 */
static akerr_ErrorContext *spr_render_actor(akgl_Actor *obj)
{
    PREPARE_ERROR(errctx);
    akbasic_AkglSprites *state = NULL;
    akgl_Sprite *sprite = NULL;
    SDL_FRect src;
    SDL_FRect dest;
    int i = 0;
    int found = -1;

    FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL actor in sprite render");
    if ( !obj->visible || obj->basechar == NULL ) {
	SUCCEED_RETURN(errctx);
    }
    state = (akbasic_AkglSprites *)obj->actorData;
    FAIL_ZERO_RETURN(errctx, (state != NULL), AKERR_NULLPOINTER,
		     "Sprite actor \"%s\" carries no backend state", obj->name);
    for ( i = 0; i < AKBASIC_MAX_SPRITES; i++ ) {
	if ( state->actors[i] == obj ) {
	    found = i;
	    break;
	}
    }
    if ( found < 0 ) {
	SUCCEED_RETURN(errctx);
    }
    sprite = state->sprites[found];
    if ( sprite == NULL || sprite->sheet == NULL || sprite->sheet->texture == NULL ) {
	SUCCEED_RETURN(errctx);
    }

    PASS(errctx, akgl_spritesheet_coords_for_frame(sprite, &src, 0));
    dest.x = obj->x - (akgl_camera != NULL ? akgl_camera->x : 0.0f);
    dest.y = obj->y - (akgl_camera != NULL ? akgl_camera->y : 0.0f);
    dest.w = (float32_t)sprite->width * (state->xexpand[found] ? 2.0f : 1.0f);
    dest.h = (float32_t)sprite->height * (state->yexpand[found] ? 2.0f : 1.0f);

    PASS(errctx, state->renderer->draw_texture(state->renderer, sprite->sheet->texture,
					       &src, &dest, 0, NULL, SDL_FLIP_NONE));
    SUCCEED_RETURN(errctx);
}

/** @brief Tear down slot @p i, releasing its texture with it. */
static akerr_ErrorContext *release_slot(akbasic_AkglSprites *state, int i)
{
    PREPARE_ERROR(errctx);

    /*
     * Actor first, then character, sprite and sheet. Each borrows the one below
     * it without taking a reference, so releasing in the other order leaves a
     * live object pointing at a zeroed pool slot.
     */
    if ( state->actors[i] != NULL ) {
	PASS(errctx, akgl_heap_release_actor(state->actors[i]));
	state->actors[i] = NULL;
    }
    if ( state->characters[i] != NULL ) {
	PASS(errctx, akgl_heap_release_character(state->characters[i]));
	state->characters[i] = NULL;
    }
    if ( state->sprites[i] != NULL ) {
	PASS(errctx, akgl_heap_release_sprite(state->sprites[i]));
	state->sprites[i] = NULL;
    }
    if ( state->sheets[i] != NULL ) {
	/* This is what destroys the texture: see akgl_heap_release_spritesheet. */
	PASS(errctx, akgl_heap_release_spritesheet(state->sheets[i]));
	state->sheets[i] = NULL;
    }
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Build slot @p i's object graph around a sheet that is already loaded.
 *
 * The names are fixed 128-byte buffers, not string literals, because
 * akgl_sprite_initialize() and akgl_actor_initialize() memcpy a whole
 * AKGL_*_MAX_NAME_LENGTH from whatever they are handed.
 */
static akerr_ErrorContext *build_slot(akbasic_AkglSprites *state, int i, int width, int height)
{
    PREPARE_ERROR(errctx);
    char spritename[AKGL_SPRITE_MAX_NAME_LENGTH];
    char charname[AKGL_SPRITE_MAX_NAME_LENGTH];
    char actorname[AKGL_ACTOR_MAX_NAME_LENGTH];
    float32_t oldx = 0.0f;
    float32_t oldy = 0.0f;
    bool oldvisible = false;

    if ( state->actors[i] != NULL ) {
	oldx = state->actors[i]->x;
	oldy = state->actors[i]->y;
	oldvisible = state->actors[i]->visible;
    }

    PASS(errctx, aksl_memset(spritename, 0, sizeof(spritename)));
    PASS(errctx, aksl_memset(charname, 0, sizeof(charname)));
    PASS(errctx, aksl_memset(actorname, 0, sizeof(actorname)));
    SDL_snprintf(spritename, sizeof(spritename), "akbasic:sprite:%d", i + 1);
    SDL_snprintf(charname, sizeof(charname), "akbasic:character:%d", i + 1);
    SDL_snprintf(actorname, sizeof(actorname), "akbasic:actor:%d", i + 1);

    PASS(errctx, akgl_heap_next_sprite(&state->sprites[i]));
    PASS(errctx, akgl_sprite_initialize(state->sprites[i], spritename, state->sheets[i]));
    state->sprites[i]->frames = 1;
    state->sprites[i]->frameids[0] = 0;
    state->sprites[i]->width = (uint32_t)width;
    state->sprites[i]->height = (uint32_t)height;

    PASS(errctx, akgl_heap_next_character(&state->characters[i]));
    PASS(errctx, akgl_character_initialize(state->characters[i], charname));
    PASS(errctx, akgl_character_sprite_add(state->characters[i], state->sprites[i], 0));

    PASS(errctx, akgl_heap_next_actor(&state->actors[i]));
    PASS(errctx, akgl_actor_initialize(state->actors[i], actorname));
    PASS(errctx, akgl_actor_set_character(state->actors[i], charname));
    state->actors[i]->state = 0;
    state->actors[i]->actorData = state;
    state->actors[i]->renderfunc = spr_render_actor;
    /*
     * Position and visibility survive a redefinition. SPRSAV changes what a
     * sprite looks like, not where it is or whether it is on -- a program that
     * animates by swapping patterns in a loop would otherwise have to re-issue
     * SPRITE and MOVSPR after every frame.
     */
    state->actors[i]->x = oldx;
    state->actors[i]->y = oldy;
    state->actors[i]->visible = oldvisible;
    SUCCEED_RETURN(errctx);
}

/** @brief Take ownership of @p surface as slot @p i's image, replacing whatever was there. */
static akerr_ErrorContext *install_surface(akbasic_AkglSprites *state, int i, SDL_Surface *surface)
{
    PREPARE_ERROR(errctx);
    SDL_Texture *texture = NULL;
    char sheetname[AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH];
    int width = 0;
    int height = 0;

    FAIL_ZERO_RETURN(errctx, (surface != NULL), AKERR_NULLPOINTER, "NULL surface for a sprite");
    width = surface->w;
    height = surface->h;
    texture = SDL_CreateTextureFromSurface(state->renderer->sdl_renderer, surface);
    FAIL_ZERO_RETURN(errctx, (texture != NULL), AKGL_ERR_SDL,
		     "Could not upload a sprite pattern: %s", SDL_GetError());
    /*
     * Nearest-neighbour. A sprite is a 24x21 bitmap and an expanded one is drawn
     * at twice the size; smoothing it would turn a pixel into a smudge, which is
     * not what anybody typing SPRSAV into a BASIC has in mind.
     */
    SDL_SetTextureScaleMode(texture, SDL_SCALEMODE_NEAREST);

    PASS(errctx, release_slot(state, i));

    /*
     * The sheet is assembled rather than initialized: akgl_spritesheet_initialize
     * loads an image from a path, and this pattern came from bytes or from a
     * saved region. Everything about it still matches what that function
     * produces, so akgl_heap_release_spritesheet() tears it down the same way --
     * which is what makes the texture's ownership one rule rather than two.
     */
    PASS(errctx, akgl_heap_next_spritesheet(&state->sheets[i]));
    PASS(errctx, aksl_memset(state->sheets[i], 0, sizeof(*state->sheets[i])));
    PASS(errctx, aksl_memset(sheetname, 0, sizeof(sheetname)));
    SDL_snprintf(sheetname, sizeof(sheetname), "akbasic:sheet:%d", i + 1);
    SDL_strlcpy(state->sheets[i]->name, sheetname, sizeof(state->sheets[i]->name));
    state->sheets[i]->texture = texture;
    state->sheets[i]->refcount = 1;
    SDL_SetPointerProperty(AKGL_REGISTRY_SPRITESHEET, state->sheets[i]->name, state->sheets[i]);

    PASS(errctx, build_slot(state, i, width, height));
    SUCCEED_RETURN(errctx);
}

/** @brief Write one unpacked pattern into @p surface, a set bit in @p fg and a clear one in @p bg. */
static akerr_ErrorContext *paint_pattern(SDL_Surface *surface, const uint8_t *pixels, akbasic_Color fg, akbasic_Color bg)
{
    PREPARE_ERROR(errctx);
    int x = 0;
    int y = 0;

    for ( y = 0; y < AKBASIC_SPRITE_HEIGHT; y++ ) {
	for ( x = 0; x < AKBASIC_SPRITE_WIDTH; x++ ) {
	    akbasic_Color c = (pixels[(y * AKBASIC_SPRITE_WIDTH) + x] != 0 ? fg : bg);

	    FAIL_ZERO_RETURN(errctx,
			     SDL_WriteSurfacePixel(surface, x, y, c.r, c.g, c.b, c.a),
			     AKGL_ERR_SDL,
			     "Could not write sprite pixel %d,%d: %s", x, y, SDL_GetError());
	}
    }
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext *spr_define(akbasic_SpriteBackend *self, int n, const uint8_t *pattern, int bytes, akbasic_Color fg, akbasic_Color bg)
{
    PREPARE_ERROR(errctx);
    akbasic_AkglSprites *state = NULL;
    SDL_Surface *surface = NULL;
    uint8_t pixels[AKBASIC_SPRITE_WIDTH * AKBASIC_SPRITE_HEIGHT];
    int index = 0;

    PASS(errctx, slot_of(self, n, &state, &index));
    PASS(errctx, akbasic_sprite_unpack(pattern, bytes, pixels));

    surface = SDL_CreateSurface(AKBASIC_SPRITE_WIDTH, AKBASIC_SPRITE_HEIGHT,
				SDL_PIXELFORMAT_RGBA32);
    FAIL_ZERO_RETURN(errctx, (surface != NULL), AKGL_ERR_SDL,
		     "Could not create a sprite surface: %s", SDL_GetError());
    ATTEMPT {
	/*
	 * The pixel loop is its own function so that it can be reached with a
	 * single CATCH. Written inline it would need a FAIL inside two nested
	 * loops inside this ATTEMPT, and the _BREAK forms expand to a C break
	 * that escapes only the inner loop.
	 */
	CATCH(errctx, paint_pattern(surface, pixels, fg, bg));
	CATCH(errctx, install_surface(state, index, surface));
    } CLEANUP {
	SDL_DestroySurface(surface);
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext *spr_define_shape(akbasic_SpriteBackend *self, int n, int handle)
{
    PREPARE_ERROR(errctx);
    akbasic_AkglSprites *state = NULL;
    int index = 0;

    PASS(errctx, slot_of(self, n, &state, &index));
    FAIL_ZERO_RETURN(errctx, (state->graphics != NULL), AKBASIC_ERR_DEVICE,
		     "SPRSAV from a saved shape needs a graphics backend, and this host supplied none");
    FAIL_ZERO_RETURN(errctx, (handle >= 0 && handle < state->graphics->shapecount),
		     AKBASIC_ERR_VALUE,
		     "Shape handle %d names no saved region", handle);
    FAIL_ZERO_RETURN(errctx, (state->graphics->shapes[handle] != NULL), AKBASIC_ERR_VALUE,
		     "Shape handle %d has been released", handle);
    PASS(errctx, install_surface(state, index, state->graphics->shapes[handle]));
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext *spr_define_file(akbasic_SpriteBackend *self, int n, const char *path, const char *root)
{
    PREPARE_ERROR(errctx);
    akbasic_AkglSprites *state = NULL;
    akgl_String *resolved = NULL;
    int index = 0;
    bool missing = false;

    PASS(errctx, slot_of(self, n, &state, &index));
    FAIL_ZERO_RETURN(errctx, (path != NULL && path[0] != '\0'), AKBASIC_ERR_VALUE,
		     "SPRSAV was given an empty file name");

    PASS(errctx, akgl_heap_next_string(&resolved));
    ATTEMPT {
	CATCH(errctx, akgl_string_initialize(resolved, NULL));
	/*
	 * The same resolution a sprite document gets for the spritesheet it
	 * names: the working directory first, then the directory the file doing
	 * the naming lives in -- here, the directory the BASIC program was
	 * loaded from. A program stored beside its art therefore works whether
	 * it was launched from its own directory or from anywhere else.
	 */
	CATCH(errctx, akgl_path_relative((char *)(root != NULL ? root : "."),
					 (char *)path, resolved));
	CATCH(errctx, release_slot(state, index));
	CATCH(errctx, akgl_heap_next_spritesheet(&state->sheets[index]));
	/*
	 * And the same load. The two frame-size arguments are dead upstream --
	 * akgl_spritesheet_initialize does not write them -- and a Commodore
	 * sprite has no frame grid anyway, so the image is one whole frame.
	 */
	CATCH(errctx, akgl_spritesheet_initialize(state->sheets[index], 0, 0,
						  (char *)&resolved->data));
	FAIL_ZERO_BREAK(errctx, (state->sheets[index]->texture != NULL), AKGL_ERR_SDL,
			"Loaded %s but it produced no texture", path);
	SDL_SetTextureScaleMode(state->sheets[index]->texture, SDL_SCALEMODE_NEAREST);
	/*
	 * The image's own size, not 24x21. A Commodore sprite is 24x21 because
	 * that is what the hardware could address; there is no reason to throw
	 * away art that is not sprite-shaped on a machine with no such limit.
	 */
	CATCH(errctx, build_slot(state, index,
				 state->sheets[index]->texture->w,
				 state->sheets[index]->texture->h));
    } CLEANUP {
	IGNORE(akgl_heap_release_string(resolved));
    } PROCESS(errctx) {
    } HANDLE(errctx, ENOENT) {
	/*
	 * Recorded rather than raised here. Returning from inside a HANDLE block
	 * leaves FINISH's bookkeeping undone, so the flag is set and the new
	 * error is raised below where an ordinary return is safe.
	 */
	missing = true;
    } FINISH(errctx, true);
    FAIL_NONZERO_RETURN(errctx, missing, AKBASIC_ERR_VALUE, "No such sprite image: %s", path);
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext *spr_show(akbasic_SpriteBackend *self, int n, bool visible)
{
    PREPARE_ERROR(errctx);
    akbasic_AkglSprites *state = NULL;
    int index = 0;

    PASS(errctx, slot_of(self, n, &state, &index));
    if ( state->actors[index] != NULL ) {
	state->actors[index]->visible = visible;
    }
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext *spr_move(akbasic_SpriteBackend *self, int n, double x, double y)
{
    PREPARE_ERROR(errctx);
    akbasic_AkglSprites *state = NULL;
    int index = 0;

    PASS(errctx, slot_of(self, n, &state, &index));
    if ( state->actors[index] != NULL ) {
	state->actors[index]->x = (float32_t)x;
	state->actors[index]->y = (float32_t)y;
    }
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext *spr_configure(akbasic_SpriteBackend *self, int n, akbasic_Color color, bool xexpand, bool yexpand, bool behind)
{
    PREPARE_ERROR(errctx);
    akbasic_AkglSprites *state = NULL;
    SDL_Color sdl = to_sdl(color);
    int index = 0;

    PASS(errctx, slot_of(self, n, &state, &index));
    state->xexpand[index] = xexpand;
    state->yexpand[index] = yexpand;
    /*
     * Colour is a texture modulation rather than a repaint: a sprite defined
     * from a pattern was drawn in the colour SPRSAV was given, and SPRITE's
     * colour argument tints it. For a sprite loaded from an image that means a
     * white image takes the colour and a coloured one is shaded by it, which is
     * what texture modulation does everywhere else.
     */
    if ( state->sheets[index] != NULL && state->sheets[index]->texture != NULL ) {
	SDL_SetTextureColorMod(state->sheets[index]->texture, sdl.r, sdl.g, sdl.b);
    }
    /*
     * Priority is not honoured. A C128 draws a low-priority sprite behind the
     * bitmap screen; here the text layer and the drawing surface are the same
     * render target and sprites are composited on top of it every frame, so
     * there is nothing to go behind. Recorded in TODO.md section 5.
     */
    (void)behind;
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext *spr_shared_colors(akbasic_SpriteBackend *self, akbasic_Color c1, akbasic_Color c2)
{
    PREPARE_ERROR(errctx);
    akbasic_AkglSprites *state = NULL;

    PASS(errctx, state_of(self, &state));
    /*
     * Nothing to do yet, and saying so is better than pretending. Multicolour
     * mode packs two bitmap bits per pixel and selects between the sprite's own
     * colour and these two shared registers; SPRSAV here takes one bit per pixel,
     * so there is no second bit to select with. The state is kept by the
     * interpreter, RSPCOLOR reads it back, and the day a multicolour pattern
     * format exists this is where it lands.
     */
    (void)c1;
    (void)c2;
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Is slot @p i a thing that can be collided with at all?
 *
 * The three guards the pair loop used to repeat inline. A slot with no actor was
 * never defined; a slot with no sprite has no frame and therefore no size; and an
 * invisible sprite is out of the world entirely, which is the cheapest way a
 * program has of switching one off without forgetting where it was.
 */
static bool slot_collidable(akbasic_AkglSprites *state, int i)
{
    return (state->actors[i] != NULL
	    && state->sprites[i] != NULL
	    && state->actors[i]->visible
	    && state->shapekind[i] != AKBASIC_SHAPE_NONE);
}

/**
 * @brief Point slot @p i's proxy at where the sprite is and what size it is now.
 *
 * The shape is rebuilt every scan rather than cached against a dirty flag. It is
 * derived from the sprite's frame and the two expansion bits, both of which a
 * program may change at any statement, and building a box is a handful of
 * arithmetic -- cheaper than being wrong about what invalidates it.
 *
 * **The mask override is the load-bearing line.** akgl_collision_shape_box()
 * leaves a shape on the ACTOR layer responding to STATIC only, so that giving a
 * town full of NPCs hitboxes does not make them shove each other around. That is
 * the right default for a tile game and exactly wrong here: BASIC's collision is
 * sprite-against-sprite first, and taking the library's default would make
 * BUMP(1) report nothing at all, silently.
 */
/**
 * @brief Record the shape SPRHIT asked for. The next scan builds it.
 *
 * Nothing is built here, and the reason is worth stating: a frame-fitting shape
 * depends on the picture and the expansion bits, both of which may change after
 * this call and before the next scan, so deriving it now would be deriving it
 * from the wrong numbers. Zeroing the synced rectangle is what makes the next
 * scan rebuild rather than recognise the position as unchanged and skip.
 */
static akerr_ErrorContext *spr_shape(akbasic_SpriteBackend *self, int n, int kind,
				     double x1, double y1, double x2, double y2)
{
    PREPARE_ERROR(errctx);
    akbasic_AkglSprites *state = NULL;
    int i = 0;

    PASS(errctx, slot_of(self, n, &state, &i));
    state->shapekind[i] = kind;
    state->shapex1[i] = (float32_t)x1;
    state->shapey1[i] = (float32_t)y1;
    state->shapex2[i] = (float32_t)x2;
    state->shapey2[i] = (float32_t)y2;
    state->shapeexplicit[i] = (x2 > x1 && y2 > y1);
    PASS(errctx, aksl_memset(&state->syncedbox[i], 0, sizeof(state->syncedbox[i])));
    SUCCEED_RETURN(errctx);
}

/**
 * @brief The rectangle slot @p i collides with, in device pixels.
 *
 * Either what SPRHIT was given, offset to where the sprite is, or the whole
 * drawn frame -- which is what a sprite nobody has shaped has always collided
 * with, and is the default this must keep reproducing exactly.
 */
static void collision_box(akbasic_AkglSprites *state, int i, SDL_FRect *dest)
{
    dest->x = state->actors[i]->x;
    dest->y = state->actors[i]->y;
    if ( state->shapeexplicit[i] ) {
	dest->x += state->shapex1[i];
	dest->y += state->shapey1[i];
	dest->w = state->shapex2[i] - state->shapex1[i];
	dest->h = state->shapey2[i] - state->shapey1[i];
	return;
    }
    dest->w = (float32_t)state->sprites[i]->width * (state->xexpand[i] ? 2.0f : 1.0f);
    dest->h = (float32_t)state->sprites[i]->height * (state->yexpand[i] ? 2.0f : 1.0f);
}

static akerr_ErrorContext AKERR_NOIGNORE *sync_proxy(akbasic_AkglSprites *state, int i,
						     SDL_FRect *box)
{
    PREPARE_ERROR(errctx);
    SDL_FRect body;

    /*
     * **Nothing to do if nothing moved.** The scan runs at the top of every
     * interpreter step -- up to 256 times a rendered frame -- and a sprite moves
     * at most once in that time, so the overwhelming majority of syncs would
     * rewrite a proxy with what it already holds. Four float comparisons to find
     * that out are far cheaper than the shape build and the sync they skip.
     *
     * Compared against the last synced rectangle rather than tracked with a dirty
     * flag a verb sets. A flag would be smaller and would be wrong: this file's
     * own header says a host game sees BASIC's sprites in libakgl's actor
     * registry "and can do anything to them it can do to an actor", which
     * includes moving one behind the interpreter's back. Comparing the answer
     * cannot miss that; flagging the question can.
     */
    if ( state->syncedbox[i].x == box->x && state->syncedbox[i].y == box->y
	 && state->syncedbox[i].w == box->w && state->syncedbox[i].h == box->h ) {
	SUCCEED_RETURN(errctx);
    }

    body.x = 0.0f;
    body.y = 0.0f;
    body.w = box->w;
    body.h = box->h;
    /*
     * A zero-sized frame would be refused by the shape builder, and a sprite can
     * legitimately have one before its sheet is loaded. Treat it as not
     * collidable rather than raising: the program has not done anything wrong.
     */
    if ( body.w <= 0.0f || body.h <= 0.0f ) {
	SUCCEED_RETURN(errctx);
    }
    switch ( state->shapekind[i] ) {
    case AKBASIC_SHAPE_CIRCLE:
	/*
	 * Inscribed, and in the *smaller* of the two half-extents. A circle that
	 * fitted the larger one would poke out of the rectangle the program
	 * asked for, which is the opposite of what "give this sprite a circle"
	 * means to somebody looking at a disc drawn inside a square frame.
	 */
	PASS(errctx, akgl_collision_shape_circle(&state->shapes[i],
						 body.w / 2.0f, body.h / 2.0f,
						 (body.w < body.h ? body.w : body.h) / 2.0f,
						 0.0f));
	break;
    case AKBASIC_SHAPE_CAPSULE_X:
    case AKBASIC_SHAPE_CAPSULE_Y:
	PASS(errctx, akgl_collision_shape_capsule(&state->shapes[i], &body,
						  (uint8_t)state->shapekind[i], 0.0f));
	break;
    default:
	PASS(errctx, akgl_collision_shape_box(&state->shapes[i], &body, 0.0f));
	break;
    }
    state->shapes[i].collidemask = AKGL_COLLISION_LAYER_ACTOR | AKGL_COLLISION_LAYER_STATIC;
    PASS(errctx, akgl_collision_proxy_sync(state->proxies[i], &state->shapes[i],
					   box->x, box->y, 0.0f));
    state->syncedbox[i] = *box;
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Keep @p contact for slot @p i if it is deeper than what is there.
 *
 * A sprite may be in several overlaps at once -- a ball touching two bricks at a
 * corner -- and only one can be reported. The deepest is the one that has to be
 * undone, so it is the one worth keeping.
 *
 * The normal comes out of libakgl pointing out of the *other* shape and toward
 * this one, which is the direction a program wants: move along it by the depth
 * and you are clear.
 */
static void record_contact(akbasic_AkglSprites *state, int i, int what, int other,
			   akgl_Contact *contact)
{
    akbasic_Contact *dest = &state->contacts[i];

    if ( state->hascontact[i] && dest->depth >= (double)contact->depth ) {
	return;
    }
    dest->what = what;
    dest->other = other;
    dest->nx = (double)contact->nx;
    dest->ny = (double)contact->ny;
    dest->depth = (double)contact->depth;
    dest->px = (double)contact->px;
    dest->py = (double)contact->py;
    /*
     * The minimum translation axis, from the normal rather than from an overlap
     * rectangle: whichever component is larger is the axis the shapes are least
     * far through, and therefore the one to reverse. Computed here because doing
     * it in BASIC means comparing two floats, which is exactly where this
     * dialect's left-operand rule bites.
     */
    dest->axis = ((contact->nx < 0.0f ? -contact->nx : contact->nx)
		  >= (contact->ny < 0.0f ? -contact->ny : contact->ny)) ? 1 : 2;
    state->hascontact[i] = true;
}

static akerr_ErrorContext *spr_contact(akbasic_SpriteBackend *self, int n,
				       akbasic_Contact *dest, bool *found)
{
    PREPARE_ERROR(errctx);
    akbasic_AkglSprites *state = NULL;
    int i = 0;

    PASS(errctx, slot_of(self, n, &state, &i));
    FAIL_ZERO_RETURN(errctx, (dest != NULL && found != NULL), AKERR_NULLPOINTER,
		     "NULL argument in contact");
    *found = state->hascontact[i];
    if ( *found ) {
	*dest = state->contacts[i];
    }
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Run the scan if anything has changed, and leave both masks on the state.
 *
 * One worker behind two entry points, because both masks come out of the same
 * pass and computing them separately would be computing the sprite boxes twice
 * for one answer. `spr_collisions()` reports the sprite-against-sprite mask and
 * `spr_solids()` the sprite-against-static one; either may be called first, and
 * the cache below is what makes that true rather than merely usually true.
 */
static akerr_ErrorContext AKERR_NOIGNORE *run_scan(akbasic_AkglSprites *state)
{
    PREPARE_ERROR(errctx);
    akgl_Contact contact;
    SDL_FRect box[AKBASIC_MAX_SPRITES];
    bool live[AKBASIC_MAX_SPRITES];
    bool synced[AKBASIC_MAX_SPRITES];
    bool hit = false;
    int i = 0;
    int j = 0;

    for ( i = 0; i < AKBASIC_MAX_SPRITES; i++ ) {
	live[i] = slot_collidable(state, i);
	synced[i] = false;
	/* PASS rather than CATCH, as everywhere else in this loop-heavy function. */
	PASS(errctx, aksl_memset(&box[i], 0, sizeof(box[i])));
	if ( !live[i] ) {
	    continue;
	}
	collision_box(state, i, &box[i]);
    }

    /*
     * **Nothing moved, so nothing can have changed.**
     *
     * The scan's inputs are exactly these boxes and which slots are collidable;
     * static geometry invalidates the cache where it is registered. So if all
     * eight match what the last scan saw, the answer is the answer it gave, and
     * eight rectangle comparisons are far cheaper than recomputing it.
     *
     * This is what makes static geometry affordable. The scan runs at the top of
     * every interpreter step -- up to 256 times a rendered frame -- and eight
     * sprites against sixty-four rectangles is five hundred and twelve tests.
     * That is fine once a frame and ruinous 256 times, and a sprite moves at
     * most once in that window.
     *
     * It changes nothing a program can observe: the same mask comes back, so
     * BUMP accumulates the same bits and the interrupt is raised on the same
     * steps.
     */
    if ( state->lastvalid ) {
	bool same = true;

	for ( i = 0; i < AKBASIC_MAX_SPRITES; i++ ) {
	    if ( live[i] != state->lastlive[i]
		 || box[i].x != state->lastbox[i].x || box[i].y != state->lastbox[i].y
		 || box[i].w != state->lastbox[i].w || box[i].h != state->lastbox[i].h ) {
		same = false;
		break;
	    }
	}
	if ( same ) {
	    SUCCEED_RETURN(errctx);
	}
    }
    state->lastmask = 0;
    state->lastsolidmask = 0;
    PASS(errctx, aksl_memset(state->hascontact, 0, sizeof(state->hascontact)));

    /*
     * **Two phases, and the split is the whole performance story.**
     *
     * The obvious implementation -- sync all eight proxies, then run
     * akgl_collision_test() on all twenty-eight pairs -- was measured at 984 ns a
     * scan against 96 ns for the four-comparison loop it replaced. At 256 scans a
     * frame that is 21% of a frame, and it bought nothing: the mask came back
     * bit-identical and the contact was thrown away.
     *
     * So: reject on the bounding boxes first, in the four comparisons that were
     * always here, and pay for the narrowphase only on a pair that survives.
     * Nearly every scan of a real game rejects all twenty-eight, and a pair that
     * does overlap is worth an exact answer. This is not a workaround for a slow
     * library -- it is what a broad phase *is*, and akgl_collision_test() does the
     * same thing internally with the proxies' own bounds before it commits to a
     * solver.
     *
     * **The narrowphase still decides.** The box test only says "maybe"; the bit
     * is set by what the library answers. That matters for a shape that is not a
     * box -- a circle inscribed in a frame overlaps a smaller region than the
     * frame does -- so the prefilter can over-report and must never be the final
     * word. Today every shape is the whole frame and the two always agree, which
     * is exactly why this is the moment to get the ordering right rather than
     * the moment it starts to matter.
     *
     * AKGL_COLLISION_TEST_PLANAR because a Commodore sprite has no z. Without it
     * the extrusion the shape builder chose takes part in the test and the
     * minimum axis can come back as a depth along z, which no BASIC program can
     * act on.
     *
     * Still box against box, not pixel against pixel: a C128's VIC-II collides on
     * set pixels, so two sprites whose boxes overlap but whose art does not are
     * reported as colliding here and would not be there. TODO.md section 5.
     */
    for ( i = 0; i < AKBASIC_MAX_SPRITES; i++ ) {
	if ( !live[i] ) {
	    continue;
	}
	for ( j = i + 1; j < AKBASIC_MAX_SPRITES; j++ ) {
	    if ( !live[j] ) {
		continue;
	    }
	    if ( !(box[i].x < box[j].x + box[j].w && box[j].x < box[i].x + box[i].w
		   && box[i].y < box[j].y + box[j].h && box[j].y < box[i].y + box[i].h) ) {
		continue;
	    }
	    /*
	     * PASS rather than CATCH throughout: this is a loop, and CATCH
	     * expands to a break, which would leave the remaining pairs untested
	     * and the mask half built.
	     */
	    if ( !synced[i] ) {
		PASS(errctx, sync_proxy(state, i, &box[i]));
		synced[i] = true;
	    }
	    if ( !synced[j] ) {
		PASS(errctx, sync_proxy(state, j, &box[j]));
		synced[j] = true;
	    }
	    hit = false;
	    PASS(errctx, akgl_collision_test(state->proxies[i], state->proxies[j],
					     AKGL_COLLISION_TEST_PLANAR, &contact, &hit));
	    if ( hit ) {
		state->lastmask |= (uint16_t)(1u << i);
		state->lastmask |= (uint16_t)(1u << j);
		record_contact(state, i, 1, j + 1, &contact);
		/*
		 * And the mirror for the other sprite. The normal points out of
		 * b toward a, so b's own copy has to be negated or both sprites
		 * would be told to move the same way.
		 */
		contact.nx = -contact.nx;
		contact.ny = -contact.ny;
		record_contact(state, j, 1, i + 1, &contact);
	    }
	}
    }

    /*
     * Every live sprite against every registered rectangle, same shape: reject
     * on the boxes, and only then ask the library. A wall is motionless, so its
     * proxy was synced when SOLID registered it and there is nothing to refresh
     * here.
     */
    for ( i = 0; i < AKBASIC_MAX_SPRITES; i++ ) {
	if ( !live[i] ) {
	    continue;
	}
	for ( j = 0; j < AKBASIC_MAX_SOLIDS; j++ ) {
	    if ( !state->solidactive[j] ) {
		continue;
	    }
	    if ( !(box[i].x < state->solidbox[j].x + state->solidbox[j].w
		   && state->solidbox[j].x < box[i].x + box[i].w
		   && box[i].y < state->solidbox[j].y + state->solidbox[j].h
		   && state->solidbox[j].y < box[i].y + box[i].h) ) {
		continue;
	    }
	    if ( !synced[i] ) {
		PASS(errctx, sync_proxy(state, i, &box[i]));
		synced[i] = true;
	    }
	    hit = false;
	    PASS(errctx, akgl_collision_test(state->proxies[i], state->solidproxies[j],
					     AKGL_COLLISION_TEST_PLANAR, &contact, &hit));
	    if ( hit ) {
		state->lastsolidmask |= (uint16_t)(1u << i);
		record_contact(state, i, 2, j + 1, &contact);
	    }
	}
    }

    for ( i = 0; i < AKBASIC_MAX_SPRITES; i++ ) {
	state->lastbox[i] = box[i];
	state->lastlive[i] = live[i];
    }
    state->lastvalid = true;
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Register, replace or retire static rectangle @p id.
 *
 * A proxy with a **NULL owner**: there is no actor behind a wall, and libakgl's
 * proxy carries the owner only so a resolver can push something. Nothing here
 * resolves anything, so the field stays empty and the shape is what matters.
 *
 * Layers are the other half. A wall sits on `AKGL_COLLISION_LAYER_STATIC` and
 * responds to nothing, which is the asymmetry libakgl's masks exist for: the
 * sprite's own `collidemask` includes STATIC, so a sprite finds a wall and two
 * walls never test against each other. Sixty-four motionless rectangles
 * therefore cost nothing per scan beyond the boxes a sprite is compared to.
 */
static akerr_ErrorContext *spr_solid(akbasic_SpriteBackend *self, int id, bool define,
				     double x1, double y1, double x2, double y2)
{
    PREPARE_ERROR(errctx);
    akbasic_AkglSprites *state = NULL;
    SDL_FRect body;
    int i = 0;

    PASS(errctx, state_of(self, &state));
    FAIL_ZERO_RETURN(errctx, (id >= 1 && id <= AKBASIC_MAX_SOLIDS), AKBASIC_ERR_BOUNDS,
		     "Static shape %d is outside 1..%d", id, AKBASIC_MAX_SOLIDS);
    i = id - 1;

    /*
     * Anything that changes the geometry invalidates the cached answer. Cheapest
     * possible correctness: the scan's own change detection watches the sprites,
     * and this is the one input it cannot see moving.
     */
    state->lastvalid = false;

    if ( !define ) {
	if ( state->solidproxies[i] != NULL ) {
	    PASS(errctx, akgl_heap_release_collision_proxy(state->solidproxies[i]));
	    state->solidproxies[i] = NULL;
	}
	state->solidactive[i] = false;
	SUCCEED_RETURN(errctx);
    }

    body.x = 0.0f;
    body.y = 0.0f;
    body.w = (float32_t)(x2 - x1);
    body.h = (float32_t)(y2 - y1);
    PASS(errctx, akgl_collision_shape_box(&state->solidshapes[i], &body, 0.0f));
    state->solidshapes[i].flags |= AKGL_COLLISION_FLAG_STATIC;
    state->solidshapes[i].layermask = AKGL_COLLISION_LAYER_STATIC;
    state->solidshapes[i].collidemask = AKGL_COLLISION_LAYER_NONE;

    if ( state->solidproxies[i] == NULL ) {
	/* Adjacent, with nothing between: the slot is free until initialize takes it. */
	PASS(errctx, akgl_heap_next_collision_proxy(&state->solidproxies[i]));
	PASS(errctx, akgl_collision_proxy_initialize(state->solidproxies[i], NULL,
						     &state->solidshapes[i],
						     (float32_t)x1, (float32_t)y1, 0.0f));
    } else {
	PASS(errctx, akgl_collision_proxy_sync(state->solidproxies[i], &state->solidshapes[i],
					       (float32_t)x1, (float32_t)y1, 0.0f));
    }
    state->solidbox[i].x = (float32_t)x1;
    state->solidbox[i].y = (float32_t)y1;
    state->solidbox[i].w = body.w;
    state->solidbox[i].h = body.h;
    state->solidactive[i] = true;
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext *spr_collisions(akbasic_SpriteBackend *self, uint16_t *mask)
{
    PREPARE_ERROR(errctx);
    akbasic_AkglSprites *state = NULL;

    PASS(errctx, state_of(self, &state));
    FAIL_ZERO_RETURN(errctx, (mask != NULL), AKERR_NULLPOINTER, "NULL mask in collisions");
    PASS(errctx, run_scan(state));
    *mask = state->lastmask;
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext *spr_solids(akbasic_SpriteBackend *self, uint16_t *mask)
{
    PREPARE_ERROR(errctx);
    akbasic_AkglSprites *state = NULL;

    PASS(errctx, state_of(self, &state));
    FAIL_ZERO_RETURN(errctx, (mask != NULL), AKERR_NULLPOINTER, "NULL mask in solids");
    PASS(errctx, run_scan(state));
    *mask = state->lastsolidmask;
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akbasic_sprite_init_akgl(akbasic_SpriteBackend *obj, akbasic_AkglSprites *state, akgl_RenderBackend *renderer, akbasic_AkglGraphics *graphics)
{
    PREPARE_ERROR(errctx);
    int i = 0;

    FAIL_ZERO_RETURN(errctx, (obj != NULL && state != NULL && renderer != NULL),
		     AKERR_NULLPOINTER, "NULL argument in sprite_init_akgl");
    /* Before anything else in libakgl, so every AKGL_ERR_* has a name to print. */
    PASS(errctx, akgl_error_init());

    PASS(errctx, aksl_memset(state, 0, sizeof(*state)));
    state->renderer = renderer;
    state->graphics = graphics;

    /*
     * The eight collision proxies, claimed here and held for the life of the
     * backend.
     *
     * Up front rather than on demand because the pool is shared: a host game
     * embedding this interpreter draws its own shaped actors from the same
     * AKGL_MAX_HEAP_COLLISION_PROXY. Claiming late would mean a collision scan
     * discovering mid-game that a host had taken the last slot, and reporting it
     * against whichever BASIC line was unlucky. Claiming here makes it an
     * initialization failure that names the pool, before a program has run.
     *
     * A default box shape so a proxy is never initialized from an uninitialized
     * shape -- libakgl fixed exactly that defect in 3a6569e, where filing a proxy
     * under half-extents that had not been written yet was invisible to its
     * tests and visible only to a memory checker. Real geometry arrives on the
     * first sync_proxy(); this is only ever a placeholder.
     */
    for ( i = 0; i < AKBASIC_MAX_SPRITES; i++ ) {
	SDL_FRect placeholder = { 0.0f, 0.0f, 1.0f, 1.0f };

	/*
	 * A box fitting the frame, which is what every sprite collided with
	 * before SPRHIT existed and is what one nobody has shaped must go on
	 * collided with.
	 */
	state->shapekind[i] = AKBASIC_SHAPE_BOX;

	PASS(errctx, akgl_collision_shape_box(&state->shapes[i], &placeholder, 0.0f));
	/*
	 * Acquire and initialize adjacent, with nothing between them: the slot
	 * is free until initialize takes the reference, and libakgl's heap says
	 * so where next_collision_proxy is declared.
	 */
	PASS(errctx, akgl_heap_next_collision_proxy(&state->proxies[i]));
	PASS(errctx, akgl_collision_proxy_initialize(state->proxies[i], NULL,
						     &state->shapes[i], 0.0f, 0.0f, 0.0f));
    }

    PASS(errctx, aksl_memset(obj, 0, sizeof(*obj)));
    obj->self		= state;
    obj->define		= spr_define;
    obj->define_shape	= spr_define_shape;
    obj->define_file	= spr_define_file;
    obj->show		= spr_show;
    obj->move		= spr_move;
    obj->configure	= spr_configure;
    obj->shared_colors	= spr_shared_colors;
    obj->collisions	= spr_collisions;
    obj->shape		= spr_shape;
    obj->solid		= spr_solid;
    obj->solids		= spr_solids;
    obj->contact	= spr_contact;
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akbasic_sprite_akgl_render(akbasic_SpriteBackend *obj)
{
    PREPARE_ERROR(errctx);
    akbasic_AkglSprites *state = NULL;
    int i = 0;

    PASS(errctx, state_of(obj, &state));
    for ( i = 0; i < AKBASIC_MAX_SPRITES; i++ ) {
	if ( state->actors[i] == NULL ) {
	    continue;
	}
	/*
	 * PASS rather than CATCH: this is a loop, and CATCH expands to a break.
	 * Through the actor's own renderfunc rather than calling spr_render_actor
	 * directly, so a host that has replaced it gets what it asked for.
	 */
	PASS(errctx, state->actors[i]->renderfunc(state->actors[i]));
    }
    SUCCEED_RETURN(errctx);
}

void akbasic_sprite_akgl_shutdown(akbasic_SpriteBackend *obj)
{
    akbasic_AkglSprites *state = NULL;
    int i = 0;

    if ( obj == NULL || obj->self == NULL ) {
	return;
    }
    state = (akbasic_AkglSprites *)obj->self;
    for ( i = 0; i < AKBASIC_MAX_SPRITES; i++ ) {
	IGNORE(release_slot(state, i));
	/*
	 * The proxies go back too. They are not registered with any partitioner,
	 * so there is nothing to unlink first -- which is the one ordering trap
	 * here, and the reason worth writing down: releasing a proxy that *is*
	 * registered leaves stale cell entries holding a pool index, and the
	 * damage only shows when the slot is handed out again.
	 */
	if ( state->proxies[i] != NULL ) {
	    IGNORE(akgl_heap_release_collision_proxy(state->proxies[i]));
	    state->proxies[i] = NULL;
	}
    }
    for ( i = 0; i < AKBASIC_MAX_SOLIDS; i++ ) {
	if ( state->solidproxies[i] != NULL ) {
	    IGNORE(akgl_heap_release_collision_proxy(state->solidproxies[i]));
	    state->solidproxies[i] = NULL;
	}
    }
}
