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

#include <SDL3/SDL.h>
#include <SDL3_image/SDL_image.h>
#include <SDL3_mixer/SDL_mixer.h>
#include <akerror.h>
#include <akgl/draw.h>
#include <akgl/error.h>
#include <akgl/game.h>

/** @brief One horizontal run of pixels the flood fill has still to examine. */
typedef struct {
    int x1;     /**< Leftmost column of the run, inclusive. */
    int x2;     /**< Rightmost column of the run, inclusive. */
    int y;      /**< The row the run is on. */
} FloodSpan;

/*
 * The flood fill's working stack. File scope and fixed size rather than a local
 * array because AKGL_DRAW_MAX_FLOOD_SPANS spans is 48 KB, which does not belong
 * on the stack of a function a game may call every frame. The consequence is
 * that akgl_draw_flood_fill is not reentrant -- it is a single-threaded
 * immediate-mode operation on a single render target, and so is everything else
 * that touches an SDL_Renderer.
 */
static FloodSpan floodspans[AKGL_DRAW_MAX_FLOOD_SPANS];

/**
 * @brief Remember the renderer's draw color and replace it with @p color.
 *
 * @p previous is written before anything that can fail, so a caller may restore
 * it unconditionally from a CLEANUP block.
 *
 * @param self     The backend. Assumed non-`NULL` with a live `sdl_renderer`;
 *                 every caller has already checked both.
 * @param color    The colour to install.
 * @param previous Receives the colour that was in place. Assumed non-`NULL`.
 *                 Pre-filled with opaque black, so it is safe to restore from
 *                 even if the query below fails.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKGL_ERR_SDL If the current draw colour cannot be read or the new one
 *         cannot be set. The message carries `SDL_GetError()`.
 */
static akerr_ErrorContext *push_draw_color(akgl_RenderBackend *self, SDL_Color color, SDL_Color *previous)
{
    PREPARE_ERROR(errctx);
    previous->r = 0x00;
    previous->g = 0x00;
    previous->b = 0x00;
    previous->a = SDL_ALPHA_OPAQUE;
    FAIL_ZERO_RETURN(
	errctx,
	SDL_GetRenderDrawColor(self->sdl_renderer, &previous->r, &previous->g, &previous->b, &previous->a),
	AKGL_ERR_SDL,
	"%s",
	SDL_GetError());
    FAIL_ZERO_RETURN(
	errctx,
	SDL_SetRenderDrawColor(self->sdl_renderer, color.r, color.g, color.b, color.a),
	AKGL_ERR_SDL,
	"%s",
	SDL_GetError());
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Put back the draw color push_draw_color() recorded.
 *
 * Called from `CLEANUP` blocks under `IGNORE()`, so its error is logged rather
 * than propagated -- failing to restore a colour must not mask the failure the
 * cleanup is unwinding from.
 *
 * @param self     The backend. Assumed non-`NULL` with a live `sdl_renderer`.
 * @param previous The colour push_draw_color() recorded. Assumed non-`NULL`.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKGL_ERR_SDL If the draw colour cannot be set.
 */
static akerr_ErrorContext *pop_draw_color(akgl_RenderBackend *self, SDL_Color *previous)
{
    PREPARE_ERROR(errctx);
    FAIL_ZERO_RETURN(
	errctx,
	SDL_SetRenderDrawColor(self->sdl_renderer, previous->r, previous->g, previous->b, previous->a),
	AKGL_ERR_SDL,
	"%s",
	SDL_GetError());
    SUCCEED_RETURN(errctx);
}

/* Draw a Gimpish background pattern to show transparency in the image */
akerr_ErrorContext *akgl_draw_background(akgl_RenderBackend *self, int w, int h)
{
    PREPARE_ERROR(errctx);
    SDL_Color col[2] = {
	{ 0x66, 0x66, 0x66, 0xff },
	{ 0x99, 0x99, 0x99, 0xff },
    };
    SDL_Color previous;
    SDL_FRect rect;
    const int dx = 8, dy = 8;
    bool pushed = false;
    bool drawfailed = false;
    int i = 0;
    int x = 0;
    int y = 0;

    FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
    FAIL_ZERO_RETURN(errctx, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");

    rect.w = (float)dx;
    rect.h = (float)dy;

    ATTEMPT {
	CATCH(errctx, push_draw_color(self, col[0], &previous));
	pushed = true;

	// The two SDL calls are checked through a flag rather than with
	// FAIL_ZERO_BREAK. Inside these loops a `break` would leave the loop,
	// not the ATTEMPT block, and the fill would carry on with the failure
	// unnoticed -- the hazard AGENTS.md describes for CATCH inside a loop.
	for ( y = 0; (y < h) && (drawfailed == false); y += dy ) {
	    for ( x = 0; x < w; x += dx ) {
		/* use an 8x8 checkerboard pattern */
		i = (((x ^ y) >> 3) & 1);
		if ( SDL_SetRenderDrawColor(self->sdl_renderer, col[i].r, col[i].g, col[i].b, col[i].a) == false ) {
		    drawfailed = true;
		    break;
		}
		rect.x = (float)x;
		rect.y = (float)y;
		if ( SDL_RenderFillRect(self->sdl_renderer, &rect) == false ) {
		    drawfailed = true;
		    break;
		}
	    }
	}
	FAIL_NONZERO_BREAK(errctx, drawfailed, AKGL_ERR_SDL, "%s", SDL_GetError());
    } CLEANUP {
	if ( pushed == true ) {
	    IGNORE(pop_draw_color(self, &previous));
	}
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Fill the four-connected region of @p oldpixel around a seed pixel.
 *
 * A scanline fill: each entry on the stack is a run of pixels on one row that
 * still has to be examined. Finding a matching pixel expands it to the whole
 * run it belongs to, fills that run, and pushes the rows above and below.
 * Filled pixels no longer match @p oldpixel, which is what terminates it.
 *
 * @p surface must be SDL_PIXELFORMAT_RGBA32; the fill compares and writes whole
 * 32-bit words rather than going through SDL_ReadSurfacePixel per pixel.
 *
 * @p dirty is set to the bounding box of everything written, so the caller can
 * put back only the pixels that changed.
 *
 * Running out of stack leaves the region partially filled and reports
 * AKERR_OUTOFBOUNDS. There is no way to unwind a partial fill short of keeping
 * a copy of the whole surface, and the caller asked for a bounded operation.
 *
 * @param surface  The pixels to fill, in SDL_PIXELFORMAT_RGBA32. Assumed
 *                 non-`NULL` and locked-or-lockless; the caller converts.
 * @param x        Seed column. Assumed inside the surface -- akgl_draw_flood_fill
 *                 has already range-checked it.
 * @param y        Seed row, likewise.
 * @param oldpixel The packed pixel value the region is made of. Everything else
 *                 is a boundary.
 * @param newpixel The packed pixel value to write. Must differ from @p oldpixel:
 *                 if they are equal the fill never terminates making progress,
 *                 which is why the caller checks that case first.
 * @param dirty    Receives the bounding box of everything written, so the caller
 *                 can put back only the pixels that changed. Assumed non-`NULL`.
 *                 Left describing an empty box if nothing matched.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKERR_OUTOFBOUNDS If the region needs more than
 *         #AKGL_DRAW_MAX_FLOOD_SPANS pending spans. @p dirty is then not written
 *         and the surface is partially filled.
 */
static akerr_ErrorContext *flood_region(SDL_Surface *surface, int x, int y, uint32_t oldpixel, uint32_t newpixel, SDL_Rect *dirty)
{
    uint32_t *pixels = (uint32_t *)surface->pixels;
    int pitch = surface->pitch / (int)sizeof(uint32_t);
    int count = 0;
    int col = 0;
    int left = 0;
    int right = 0;
    int i = 0;
    int minx = surface->w;
    int miny = surface->h;
    int maxx = -1;
    int maxy = -1;
    FloodSpan span;

    PREPARE_ERROR(errctx);

    floodspans[0].x1 = x;
    floodspans[0].x2 = x;
    floodspans[0].y = y;
    count = 1;

    while ( count > 0 ) {
	count -= 1;
	span = floodspans[count];
	col = span.x1;
	while ( col <= span.x2 ) {
	    if ( pixels[(span.y * pitch) + col] != oldpixel ) {
		col += 1;
		continue;
	    }
	    left = col;
	    while ( left > 0 && pixels[(span.y * pitch) + (left - 1)] == oldpixel ) {
		left -= 1;
	    }
	    right = col;
	    while ( right < (surface->w - 1) && pixels[(span.y * pitch) + (right + 1)] == oldpixel ) {
		right += 1;
	    }
	    for ( i = left; i <= right; i++ ) {
		pixels[(span.y * pitch) + i] = newpixel;
	    }
	    if ( left < minx ) {
		minx = left;
	    }
	    if ( right > maxx ) {
		maxx = right;
	    }
	    if ( span.y < miny ) {
		miny = span.y;
	    }
	    if ( span.y > maxy ) {
		maxy = span.y;
	    }
	    // Two pushes per run, so the check is for room for both.
	    FAIL_NONZERO_RETURN(
		errctx,
		((count + 2) > AKGL_DRAW_MAX_FLOOD_SPANS),
		AKERR_OUTOFBOUNDS,
		"Region needs more than %d pending spans; it is partially filled",
		AKGL_DRAW_MAX_FLOOD_SPANS);
	    if ( span.y > 0 ) {
		floodspans[count].x1 = left;
		floodspans[count].x2 = right;
		floodspans[count].y = span.y - 1;
		count += 1;
	    }
	    if ( span.y < (surface->h - 1) ) {
		floodspans[count].x1 = left;
		floodspans[count].x2 = right;
		floodspans[count].y = span.y + 1;
		count += 1;
	    }
	    col = right + 1;
	}
    }

    dirty->x = minx;
    dirty->y = miny;
    dirty->w = (maxx - minx) + 1;
    dirty->h = (maxy - miny) + 1;
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_draw_point(akgl_RenderBackend *self, float32_t x, float32_t y, SDL_Color color)
{
    SDL_Color previous;

    PREPARE_ERROR(errctx);
    FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
    FAIL_ZERO_RETURN(errctx, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
    ATTEMPT {
	CATCH(errctx, push_draw_color(self, color, &previous));
	FAIL_ZERO_BREAK(
	    errctx,
	    SDL_RenderPoint(self->sdl_renderer, x, y),
	    AKGL_ERR_SDL,
	    "%s",
	    SDL_GetError());
    } CLEANUP {
	IGNORE(pop_draw_color(self, &previous));
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_draw_line(akgl_RenderBackend *self, float32_t x1, float32_t y1, float32_t x2, float32_t y2, SDL_Color color)
{
    SDL_Color previous;

    PREPARE_ERROR(errctx);
    FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
    FAIL_ZERO_RETURN(errctx, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
    ATTEMPT {
	CATCH(errctx, push_draw_color(self, color, &previous));
	FAIL_ZERO_BREAK(
	    errctx,
	    SDL_RenderLine(self->sdl_renderer, x1, y1, x2, y2),
	    AKGL_ERR_SDL,
	    "%s",
	    SDL_GetError());
    } CLEANUP {
	IGNORE(pop_draw_color(self, &previous));
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_draw_rect(akgl_RenderBackend *self, SDL_FRect *rect, SDL_Color color)
{
    SDL_Color previous;

    PREPARE_ERROR(errctx);
    FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
    FAIL_ZERO_RETURN(errctx, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
    FAIL_ZERO_RETURN(errctx, rect, AKERR_NULLPOINTER, "rect");
    ATTEMPT {
	CATCH(errctx, push_draw_color(self, color, &previous));
	FAIL_ZERO_BREAK(
	    errctx,
	    SDL_RenderRect(self->sdl_renderer, rect),
	    AKGL_ERR_SDL,
	    "%s",
	    SDL_GetError());
    } CLEANUP {
	IGNORE(pop_draw_color(self, &previous));
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_draw_filled_rect(akgl_RenderBackend *self, SDL_FRect *rect, SDL_Color color)
{
    SDL_Color previous;

    PREPARE_ERROR(errctx);
    FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
    FAIL_ZERO_RETURN(errctx, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
    FAIL_ZERO_RETURN(errctx, rect, AKERR_NULLPOINTER, "rect");
    ATTEMPT {
	CATCH(errctx, push_draw_color(self, color, &previous));
	FAIL_ZERO_BREAK(
	    errctx,
	    SDL_RenderFillRect(self->sdl_renderer, rect),
	    AKGL_ERR_SDL,
	    "%s",
	    SDL_GetError());
    } CLEANUP {
	IGNORE(pop_draw_color(self, &previous));
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_draw_circle(akgl_RenderBackend *self, float32_t x, float32_t y, float32_t radius, SDL_Color color)
{
    SDL_Color previous;
    SDL_FPoint octants[8];
    int centerx = 0;
    int centery = 0;
    int r = 0;
    int offsetx = 0;
    int offsety = 0;
    int decision = 0;
    bool plotted = true;

    PREPARE_ERROR(errctx);
    FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
    FAIL_ZERO_RETURN(errctx, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
    FAIL_NONZERO_RETURN(errctx, (radius < 0), AKERR_OUTOFBOUNDS, "Negative radius %f", radius);

    centerx = (int)SDL_lroundf(x);
    centery = (int)SDL_lroundf(y);
    r = (int)SDL_lroundf(radius);
    offsety = r;
    // The midpoint decision variable, started so the first step chooses between
    // (0, r) and (1, r-1) correctly.
    decision = 1 - r;

    ATTEMPT {
	CATCH(errctx, push_draw_color(self, color, &previous));
	while ( offsety >= offsetx ) {
	    // Eight-way symmetry: one computed point in the second octant gives
	    // the seven others by reflection.
	    octants[0].x = (float)(centerx + offsetx);
	    octants[0].y = (float)(centery + offsety);
	    octants[1].x = (float)(centerx - offsetx);
	    octants[1].y = (float)(centery + offsety);
	    octants[2].x = (float)(centerx + offsetx);
	    octants[2].y = (float)(centery - offsety);
	    octants[3].x = (float)(centerx - offsetx);
	    octants[3].y = (float)(centery - offsety);
	    octants[4].x = (float)(centerx + offsety);
	    octants[4].y = (float)(centery + offsetx);
	    octants[5].x = (float)(centerx - offsety);
	    octants[5].y = (float)(centery + offsetx);
	    octants[6].x = (float)(centerx + offsety);
	    octants[6].y = (float)(centery - offsetx);
	    octants[7].x = (float)(centerx - offsety);
	    octants[7].y = (float)(centery - offsetx);
	    // A CATCH here would break this loop rather than leave the function,
	    // so failure is recorded and reported once the loop is done.
	    if ( !SDL_RenderPoints(self->sdl_renderer, octants, 8) ) {
		plotted = false;
	    }
	    offsetx += 1;
	    if ( decision < 0 ) {
		decision += (2 * offsetx) + 1;
	    } else {
		offsety -= 1;
		decision += 2 * (offsetx - offsety) + 1;
	    }
	}
	FAIL_ZERO_BREAK(errctx, plotted, AKGL_ERR_SDL, "%s", SDL_GetError());
    } CLEANUP {
	IGNORE(pop_draw_color(self, &previous));
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_draw_flood_fill(akgl_RenderBackend *self, int x, int y, SDL_Color color)
{
    SDL_Surface *target = NULL;
    SDL_Surface *rgba = NULL;
    SDL_Texture *patch = NULL;
    // Only written by a successful flood_region(), and only read after one, but
    // the paths in between are far enough apart that the compiler cannot see it.
    SDL_Rect dirty = { 0, 0, 0, 0 };
    SDL_FRect src;
    SDL_FRect dest;
    uint32_t *pixels = NULL;
    uint32_t oldpixel = 0;
    uint32_t newpixel = 0;
    int width = 0;
    int height = 0;

    PREPARE_ERROR(errctx);
    FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
    FAIL_ZERO_RETURN(errctx, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
    ATTEMPT {
	FAIL_ZERO_BREAK(
	    errctx,
	    SDL_GetCurrentRenderOutputSize(self->sdl_renderer, &width, &height),
	    AKGL_ERR_SDL,
	    "%s",
	    SDL_GetError());
	FAIL_NONZERO_BREAK(
	    errctx,
	    ((x < 0) || (y < 0) || (x >= width) || (y >= height)),
	    AKERR_OUTOFBOUNDS,
	    "Seed pixel %d,%d is outside the %dx%d render target",
	    x, y, width, height);

	target = SDL_RenderReadPixels(self->sdl_renderer, NULL);
	FAIL_ZERO_BREAK(errctx, target, AKGL_ERR_SDL, "%s", SDL_GetError());
	// The fill works on 32-bit words, so the layout has to be known rather
	// than whatever the render target happens to use.
	rgba = SDL_ConvertSurface(target, SDL_PIXELFORMAT_RGBA32);
	FAIL_ZERO_BREAK(errctx, rgba, AKGL_ERR_SDL, "%s", SDL_GetError());

	pixels = (uint32_t *)rgba->pixels;
	oldpixel = pixels[(y * (rgba->pitch / (int)sizeof(uint32_t))) + x];
	newpixel = SDL_MapSurfaceRGBA(rgba, color.r, color.g, color.b, color.a);
	if ( oldpixel == newpixel ) {
	    // Already the requested color. Walking it would compare filled
	    // pixels against themselves and find nothing, so say so up front.
	    SUCCEED_BREAK(errctx);
	}
	CATCH(errctx, flood_region(rgba, x, y, oldpixel, newpixel, &dirty));

	patch = SDL_CreateTextureFromSurface(self->sdl_renderer, rgba);
	FAIL_ZERO_BREAK(errctx, patch, AKGL_ERR_SDL, "%s", SDL_GetError());
	// Replace rather than blend: this is a framebuffer operation, and the
	// pixels being put back are the ones that were just read out of it.
	FAIL_ZERO_BREAK(
	    errctx,
	    SDL_SetTextureBlendMode(patch, SDL_BLENDMODE_NONE),
	    AKGL_ERR_SDL,
	    "%s",
	    SDL_GetError());

	// Only the bounding box of what changed goes back to the target.
	src.x = (float)dirty.x;
	src.y = (float)dirty.y;
	src.w = (float)dirty.w;
	src.h = (float)dirty.h;
	dest = src;
	FAIL_ZERO_BREAK(
	    errctx,
	    SDL_RenderTexture(self->sdl_renderer, patch, &src, &dest),
	    AKGL_ERR_SDL,
	    "%s",
	    SDL_GetError());
    } CLEANUP {
	if ( patch != NULL ) {
	    SDL_DestroyTexture(patch);
	}
	if ( rgba != NULL ) {
	    SDL_DestroySurface(rgba);
	}
	if ( target != NULL ) {
	    SDL_DestroySurface(target);
	}
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_draw_copy_region(akgl_RenderBackend *self, SDL_Rect *src, SDL_Surface **dest)
{
    SDL_Surface *saved = NULL;
    int width = 0;
    int height = 0;

    PREPARE_ERROR(errctx);
    FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
    FAIL_ZERO_RETURN(errctx, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
    FAIL_ZERO_RETURN(errctx, src, AKERR_NULLPOINTER, "src");
    FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "dest");
    ATTEMPT {
	FAIL_NONZERO_BREAK(
	    errctx,
	    ((src->w <= 0) || (src->h <= 0)),
	    AKERR_OUTOFBOUNDS,
	    "Region %dx%d has no area",
	    src->w, src->h);
	FAIL_ZERO_BREAK(
	    errctx,
	    SDL_GetCurrentRenderOutputSize(self->sdl_renderer, &width, &height),
	    AKGL_ERR_SDL,
	    "%s",
	    SDL_GetError());
	// SDL clips a read to the target and hands back a smaller surface than
	// was asked for, which a caller pasting it back would not notice.
	FAIL_NONZERO_BREAK(
	    errctx,
	    ((src->x < 0) || (src->y < 0) ||
	     ((src->x + src->w) > width) || ((src->y + src->h) > height)),
	    AKERR_OUTOFBOUNDS,
	    "Region %d,%d %dx%d does not fit inside the %dx%d render target",
	    src->x, src->y, src->w, src->h, width, height);

	saved = SDL_RenderReadPixels(self->sdl_renderer, src);
	FAIL_ZERO_BREAK(errctx, saved, AKGL_ERR_SDL, "%s", SDL_GetError());

	if ( *dest == NULL ) {
	    *dest = saved;
	    // Ownership has moved to the caller; CLEANUP must not free it.
	    saved = NULL;
	} else {
	    FAIL_NONZERO_BREAK(
		errctx,
		(((*dest)->w != src->w) || ((*dest)->h != src->h)),
		AKERR_OUTOFBOUNDS,
		"Destination surface is %dx%d, region is %dx%d",
		(*dest)->w, (*dest)->h, src->w, src->h);
	    FAIL_ZERO_BREAK(
		errctx,
		SDL_SetSurfaceBlendMode(saved, SDL_BLENDMODE_NONE),
		AKGL_ERR_SDL,
		"%s",
		SDL_GetError());
	    FAIL_ZERO_BREAK(
		errctx,
		SDL_BlitSurface(saved, NULL, *dest, NULL),
		AKGL_ERR_SDL,
		"%s",
		SDL_GetError());
	}
    } CLEANUP {
	if ( saved != NULL ) {
	    SDL_DestroySurface(saved);
	}
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_draw_paste_region(akgl_RenderBackend *self, SDL_Surface *src, float32_t x, float32_t y)
{
    SDL_Texture *patch = NULL;
    SDL_FRect dest;

    PREPARE_ERROR(errctx);
    FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
    FAIL_ZERO_RETURN(errctx, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
    FAIL_ZERO_RETURN(errctx, src, AKERR_NULLPOINTER, "src");
    ATTEMPT {
	patch = SDL_CreateTextureFromSurface(self->sdl_renderer, src);
	FAIL_ZERO_BREAK(errctx, patch, AKGL_ERR_SDL, "%s", SDL_GetError());
	// Replace what is on the target, the way GSHAPE does by default.
	FAIL_ZERO_BREAK(
	    errctx,
	    SDL_SetTextureBlendMode(patch, SDL_BLENDMODE_NONE),
	    AKGL_ERR_SDL,
	    "%s",
	    SDL_GetError());
	dest.x = x;
	dest.y = y;
	dest.w = (float)src->w;
	dest.h = (float)src->h;
	FAIL_ZERO_BREAK(
	    errctx,
	    SDL_RenderTexture(self->sdl_renderer, patch, NULL, &dest),
	    AKGL_ERR_SDL,
	    "%s",
	    SDL_GetError());
    } CLEANUP {
	if ( patch != NULL ) {
	    SDL_DestroyTexture(patch);
	}
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Fill one quarter-circle corner as a triangle fan.
 *
 * `SDL_RenderGeometry` colours from its vertices rather than the renderer's
 * draw colour, so this neither pushes nor pops it.
 *
 * @param self      The backend. Assumed non-`NULL` with a live `sdl_renderer`;
 *                  the caller has already checked both.
 * @param cx        Horizontal position of the corner's centre -- the point the
 *                  fan radiates from, one radius inside the rectangle.
 * @param cy        Vertical position of the centre.
 * @param radius    Radius of the quarter circle. Assumed positive.
 * @param start_deg Angle the quarter starts at; it sweeps 90 degrees clockwise
 *                  from there.
 * @param color     Colour to fill with.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKGL_ERR_SDL If the fan cannot be drawn.
 */
static akerr_ErrorContext *fill_corner_fan(akgl_RenderBackend *self, float32_t cx, float32_t cy, float32_t radius, float32_t start_deg, SDL_Color color)
{
    SDL_Vertex vertices[AKGL_DRAW_ROUNDED_RECT_SEGMENTS + 2];
    int indices[AKGL_DRAW_ROUNDED_RECT_SEGMENTS * 3];
    SDL_FColor fcolor;
    float32_t angle = 0.0f;
    int i = 0;

    PREPARE_ERROR(errctx);
    fcolor.r = (float)color.r / 255.0f;
    fcolor.g = (float)color.g / 255.0f;
    fcolor.b = (float)color.b / 255.0f;
    fcolor.a = (float)color.a / 255.0f;

    vertices[0].position.x = cx;
    vertices[0].position.y = cy;
    vertices[0].color = fcolor;
    vertices[0].tex_coord.x = 0.0f;
    vertices[0].tex_coord.y = 0.0f;
    for ( i = 0; i <= AKGL_DRAW_ROUNDED_RECT_SEGMENTS; i++ ) {
	angle = (start_deg + ((90.0f / AKGL_DRAW_ROUNDED_RECT_SEGMENTS) * (float)i)) * (SDL_PI_F / 180.0f);
	vertices[i + 1].position.x = cx + (radius * SDL_cosf(angle));
	vertices[i + 1].position.y = cy + (radius * SDL_sinf(angle));
	vertices[i + 1].color = fcolor;
	vertices[i + 1].tex_coord.x = 0.0f;
	vertices[i + 1].tex_coord.y = 0.0f;
    }
    for ( i = 0; i < AKGL_DRAW_ROUNDED_RECT_SEGMENTS; i++ ) {
	indices[(i * 3) + 0] = 0;
	indices[(i * 3) + 1] = i + 1;
	indices[(i * 3) + 2] = i + 2;
    }
    FAIL_ZERO_RETURN(
	errctx,
	SDL_RenderGeometry(
	    self->sdl_renderer,
	    NULL,
	    vertices,
	    AKGL_DRAW_ROUNDED_RECT_SEGMENTS + 2,
	    indices,
	    AKGL_DRAW_ROUNDED_RECT_SEGMENTS * 3),
	AKGL_ERR_SDL,
	"%s",
	SDL_GetError());
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_draw_filled_rounded_rect(akgl_RenderBackend *self, SDL_FRect *rect, float32_t radius, SDL_Color color)
{
    SDL_Color previous;
    SDL_FRect band;
    float32_t half = 0.0f;
    bool pushed = false;
    bool drawfailed = false;

    PREPARE_ERROR(errctx);
    FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
    FAIL_ZERO_RETURN(errctx, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
    FAIL_ZERO_RETURN(errctx, rect, AKERR_NULLPOINTER, "NULL rectangle");
    if ( (rect->w <= 0.0f) || (rect->h <= 0.0f) ) {
	SUCCEED_RETURN(errctx);
    }
    if ( radius <= 0.0f ) {
	PASS(errctx, akgl_draw_filled_rect(self, rect, color));
	SUCCEED_RETURN(errctx);
    }
    half = (((rect->w < rect->h) ? rect->w : rect->h) / 2.0f);
    if ( radius > half ) {
	radius = half;
    }

    ATTEMPT {
	CATCH(errctx, push_draw_color(self, color, &previous));
	pushed = true;

	// The body is three bands: one the full width between the corner rows,
	// and one between the corners along each of the top and bottom edges.
	// A radius of exactly half the shorter side leaves one or more of them
	// with no area, which SDL fills as nothing -- at the limit the whole
	// shape is the four fans.
	band.x = rect->x;
	band.y = rect->y + radius;
	band.w = rect->w;
	band.h = rect->h - (radius * 2.0f);
	if ( SDL_RenderFillRect(self->sdl_renderer, &band) == false ) {
	    drawfailed = true;
	}
	band.x = rect->x + radius;
	band.y = rect->y;
	band.w = rect->w - (radius * 2.0f);
	band.h = radius;
	if ( SDL_RenderFillRect(self->sdl_renderer, &band) == false ) {
	    drawfailed = true;
	}
	band.y = rect->y + rect->h - radius;
	if ( SDL_RenderFillRect(self->sdl_renderer, &band) == false ) {
	    drawfailed = true;
	}
	FAIL_NONZERO_BREAK(errctx, drawfailed, AKGL_ERR_SDL, "%s", SDL_GetError());

	CATCH(errctx, fill_corner_fan(self, rect->x + radius, rect->y + radius, radius, 180.0f, color));
	CATCH(errctx, fill_corner_fan(self, rect->x + rect->w - radius, rect->y + radius, radius, 270.0f, color));
	CATCH(errctx, fill_corner_fan(self, rect->x + rect->w - radius, rect->y + rect->h - radius, radius, 0.0f, color));
	CATCH(errctx, fill_corner_fan(self, rect->x + radius, rect->y + rect->h - radius, radius, 90.0f, color));
    } CLEANUP {
	if ( pushed == true ) {
	    IGNORE(pop_draw_color(self, &previous));
	}
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_draw_arc(akgl_RenderBackend *self, float32_t x, float32_t y, float32_t radius, float32_t start_deg, float32_t end_deg, float32_t thickness, SDL_Color color)
{
    SDL_Vertex vertices[AKGL_DRAW_ARC_MAX_POINTS];
    int indices[(AKGL_DRAW_ARC_MAX_POINTS - 2) * 3];
    SDL_FColor fcolor;
    float32_t span = 0.0f;
    float32_t inner = 0.0f;
    float32_t angle = 0.0f;
    int segments = 0;
    int base = 0;
    int i = 0;

    PREPARE_ERROR(errctx);
    FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
    FAIL_ZERO_RETURN(errctx, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
    FAIL_NONZERO_RETURN(errctx, (radius <= 0.0f), AKERR_OUTOFBOUNDS, "Arc radius %f is not positive", (double)radius);
    FAIL_NONZERO_RETURN(errctx, (thickness <= 0.0f), AKERR_OUTOFBOUNDS, "Arc thickness %f is not positive", (double)thickness);
    span = end_deg - start_deg;
    if ( span <= 0.0f ) {
	SUCCEED_RETURN(errctx);
    }
    if ( span > 360.0f ) {
	span = 360.0f;
    }
    inner = radius - thickness;
    if ( inner < 0.0f ) {
	inner = 0.0f;
    }

    // Steps in proportion to the angle swept, so a sliver of arc does not
    // spend the whole vertex budget and a full circle uses all of it: the
    // quarter-circle density of AKGL_DRAW_ROUNDED_RECT_SEGMENTS, capped by
    // what AKGL_DRAW_ARC_MAX_POINTS vertices can carry at two per step.
    segments = (int)((span / 90.0f) * (float)AKGL_DRAW_ROUNDED_RECT_SEGMENTS) + 1;
    if ( segments > ((AKGL_DRAW_ARC_MAX_POINTS / 2) - 1) ) {
	segments = (AKGL_DRAW_ARC_MAX_POINTS / 2) - 1;
    }

    fcolor.r = (float)color.r / 255.0f;
    fcolor.g = (float)color.g / 255.0f;
    fcolor.b = (float)color.b / 255.0f;
    fcolor.a = (float)color.a / 255.0f;
    for ( i = 0; i <= segments; i++ ) {
	angle = (start_deg + ((span / (float)segments) * (float)i)) * (SDL_PI_F / 180.0f);
	vertices[i * 2].position.x = x + (radius * SDL_cosf(angle));
	vertices[i * 2].position.y = y + (radius * SDL_sinf(angle));
	vertices[i * 2].color = fcolor;
	vertices[i * 2].tex_coord.x = 0.0f;
	vertices[i * 2].tex_coord.y = 0.0f;
	vertices[(i * 2) + 1].position.x = x + (inner * SDL_cosf(angle));
	vertices[(i * 2) + 1].position.y = y + (inner * SDL_sinf(angle));
	vertices[(i * 2) + 1].color = fcolor;
	vertices[(i * 2) + 1].tex_coord.x = 0.0f;
	vertices[(i * 2) + 1].tex_coord.y = 0.0f;
    }
    for ( i = 0; i < segments; i++ ) {
	base = i * 2;
	indices[(i * 6) + 0] = base;
	indices[(i * 6) + 1] = base + 2;
	indices[(i * 6) + 2] = base + 1;
	indices[(i * 6) + 3] = base + 1;
	indices[(i * 6) + 4] = base + 2;
	indices[(i * 6) + 5] = base + 3;
    }
    FAIL_ZERO_RETURN(
	errctx,
	SDL_RenderGeometry(
	    self->sdl_renderer,
	    NULL,
	    vertices,
	    (segments + 1) * 2,
	    indices,
	    segments * 6),
	AKGL_ERR_SDL,
	"%s",
	SDL_GetError());
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_draw_set_clip(akgl_RenderBackend *self, SDL_Rect *rect)
{
    PREPARE_ERROR(errctx);
    FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
    FAIL_ZERO_RETURN(errctx, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
    // NULL rect deliberately passes straight through: it is how the clip is
    // cleared, and the header says so.
    FAIL_ZERO_RETURN(
	errctx,
	SDL_SetRenderClipRect(self->sdl_renderer, rect),
	AKGL_ERR_SDL,
	"%s",
	SDL_GetError());
    SUCCEED_RETURN(errctx);
}
