/**
 * @file graphics_akgl.c
 * @brief Wires the graphics backend record to libakgl's immediate-mode drawing.
 *
 * A thin adaptor and deliberately so: the interesting decisions about what a
 * BASIC graphics verb means live in src/runtime_graphics.c, and everything here
 * is coordinate and colour conversion plus the shape pool that SSHAPE hands out
 * handles into.
 *
 * The akgl_draw_* family is new in libakgl 42b60f7. Every one of its entry
 * points takes the akgl_RenderBackend the host already initialized rather than
 * reaching for a global, which is exactly what goal 3 needs.
 */

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

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

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

/** @brief The colour conversion, which is the whole impedance mismatch. */
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_GraphicsBackend *self, akbasic_AkglGraphics **dest)
{
    PREPARE_ERROR(errctx);

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

/**
 * @brief How big the renderer's output is, which is the space BASIC draws into.
 *
 * SDL_GetCurrentRenderOutputSize rather than the window size: the two differ on
 * a high-DPI display and on a renderer with a logical presentation set, and it
 * is the output that a coordinate handed to akgl_draw_point actually indexes.
 */
static akerr_ErrorContext *gfx_size(akbasic_GraphicsBackend *self, int *width, int *height)
{
    PREPARE_ERROR(errctx);
    akbasic_AkglGraphics *state = NULL;

    PASS(errctx, state_of(self, &state));
    FAIL_ZERO_RETURN(errctx, (width != NULL && height != NULL), AKERR_NULLPOINTER,
		     "NULL destination in gfx_size");
    FAIL_ZERO_RETURN(errctx,
		     SDL_GetCurrentRenderOutputSize(state->renderer->sdl_renderer, width, height),
		     AKGL_ERR_SDL, "%s", SDL_GetError());
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext *gfx_point(akbasic_GraphicsBackend *self, double x, double y, akbasic_Color color)
{
    PREPARE_ERROR(errctx);
    akbasic_AkglGraphics *state = NULL;

    PASS(errctx, state_of(self, &state));
    PASS(errctx, akgl_draw_point(state->renderer, (float32_t)x, (float32_t)y, to_sdl(color)));
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext *gfx_line(akbasic_GraphicsBackend *self, double x1, double y1, double x2, double y2, akbasic_Color color)
{
    PREPARE_ERROR(errctx);
    akbasic_AkglGraphics *state = NULL;

    PASS(errctx, state_of(self, &state));
    PASS(errctx, akgl_draw_line(state->renderer, (float32_t)x1, (float32_t)y1,
				(float32_t)x2, (float32_t)y2, to_sdl(color)));
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Turn two opposite corners into the rect SDL wants.
 *
 * BASIC gives two corners in whatever order the program felt like; SDL_FRect is
 * an origin and a size and does nothing useful with a negative one.
 */
static SDL_FRect corners_to_rect(double x1, double y1, double x2, double y2)
{
    SDL_FRect rect;

    rect.x = (float32_t)((x1 < x2) ? x1 : x2);
    rect.y = (float32_t)((y1 < y2) ? y1 : y2);
    rect.w = (float32_t)((x1 < x2) ? (x2 - x1) : (x1 - x2));
    rect.h = (float32_t)((y1 < y2) ? (y2 - y1) : (y1 - y2));
    return rect;
}

static akerr_ErrorContext *gfx_rect(akbasic_GraphicsBackend *self, double x1, double y1, double x2, double y2, akbasic_Color color)
{
    PREPARE_ERROR(errctx);
    akbasic_AkglGraphics *state = NULL;
    SDL_FRect rect;

    PASS(errctx, state_of(self, &state));
    rect = corners_to_rect(x1, y1, x2, y2);
    PASS(errctx, akgl_draw_rect(state->renderer, &rect, to_sdl(color)));
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext *gfx_filled_rect(akbasic_GraphicsBackend *self, double x1, double y1, double x2, double y2, akbasic_Color color)
{
    PREPARE_ERROR(errctx);
    akbasic_AkglGraphics *state = NULL;
    SDL_FRect rect;

    PASS(errctx, state_of(self, &state));
    rect = corners_to_rect(x1, y1, x2, y2);
    PASS(errctx, akgl_draw_filled_rect(state->renderer, &rect, to_sdl(color)));
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext *gfx_paint(akbasic_GraphicsBackend *self, int x, int y, akbasic_Color color)
{
    PREPARE_ERROR(errctx);
    akbasic_AkglGraphics *state = NULL;

    PASS(errctx, state_of(self, &state));
    /*
     * AKERR_OUTOFBOUNDS out of here means the fill exhausted its span stack and
     * left the region partly filled. It is passed straight through: PAINT is
     * where that is turned into something a BASIC program can see, because PAINT
     * is the only thing that knows a program is watching.
     */
    PASS(errctx, akgl_draw_flood_fill(state->renderer, x, y, to_sdl(color)));
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext *gfx_clear(akbasic_GraphicsBackend *self, akbasic_Color color)
{
    PREPARE_ERROR(errctx);
    akbasic_AkglGraphics *state = NULL;
    SDL_FRect rect;
    int w = 0;
    int h = 0;

    PASS(errctx, state_of(self, &state));
    PASS(errctx, gfx_size(self, &w, &h));

    /*
     * A filled rectangle over the output rather than SDL_RenderClear, because
     * clearing is the host's prerogative: a game that draws a background and
     * then hands the renderer to a script does not expect GRAPHIC to wipe it to
     * a colour of the script's choosing outside the area the script owns. This
     * covers exactly the render output and nothing beyond it.
     */
    rect.x = 0.0f;
    rect.y = 0.0f;
    rect.w = (float32_t)w;
    rect.h = (float32_t)h;
    PASS(errctx, akgl_draw_filled_rect(state->renderer, &rect, to_sdl(color)));
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext *gfx_save_shape(akbasic_GraphicsBackend *self, int x1, int y1, int x2, int y2, int *handle)
{
    PREPARE_ERROR(errctx);
    akbasic_AkglGraphics *state = NULL;
    SDL_Rect region;

    PASS(errctx, state_of(self, &state));
    FAIL_ZERO_RETURN(errctx, (handle != NULL), AKERR_NULLPOINTER,
		     "NULL handle in save_shape");
    FAIL_ZERO_RETURN(errctx, (state->shapecount < AKBASIC_AKGL_MAX_SHAPES),
		     AKBASIC_ERR_DEVICE,
		     "SSHAPE pool is full at %d saved regions; GRAPHIC CLR releases them",
		     AKBASIC_AKGL_MAX_SHAPES);

    region.x = (x1 < x2) ? x1 : x2;
    region.y = (y1 < y2) ? y1 : y2;
    region.w = (x1 < x2) ? (x2 - x1) : (x1 - x2);
    region.h = (y1 < y2) ? (y2 - y1) : (y1 - y2);

    /*
     * NULL through dest asks akgl_draw_copy_region to allocate the surface. That
     * is the one allocation in this file and it belongs to the pool below, which
     * is bounded -- so it is a fixed ceiling on memory, not an open one.
     */
    state->shapes[state->shapecount] = NULL;
    PASS(errctx, akgl_draw_copy_region(state->renderer, &region,
				       &state->shapes[state->shapecount]));
    *handle = state->shapecount;
    state->shapecount += 1;
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext *gfx_paste_shape(akbasic_GraphicsBackend *self, int handle, double x, double y)
{
    PREPARE_ERROR(errctx);
    akbasic_AkglGraphics *state = NULL;

    PASS(errctx, state_of(self, &state));
    FAIL_ZERO_RETURN(errctx, (handle >= 0 && handle < state->shapecount),
		     AKBASIC_ERR_BOUNDS, "No saved shape %d", handle);
    FAIL_ZERO_RETURN(errctx, (state->shapes[handle] != NULL), AKBASIC_ERR_STATE,
		     "Saved shape %d has been released", handle);
    PASS(errctx, akgl_draw_paste_region(state->renderer, state->shapes[handle],
					(float32_t)x, (float32_t)y));
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext *gfx_free_shapes(akbasic_GraphicsBackend *self)
{
    PREPARE_ERROR(errctx);
    akbasic_AkglGraphics *state = NULL;
    int i = 0;

    PASS(errctx, state_of(self, &state));
    for ( i = 0; i < state->shapecount; i++ ) {
	if ( state->shapes[i] != NULL ) {
	    SDL_DestroySurface(state->shapes[i]);
	    state->shapes[i] = NULL;
	}
    }
    state->shapecount = 0;
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akbasic_graphics_init_akgl(akbasic_GraphicsBackend *obj, akbasic_AkglGraphics *state, akgl_RenderBackend *renderer)
{
    PREPARE_ERROR(errctx);

    FAIL_ZERO_RETURN(errctx, (obj != NULL && state != NULL), AKERR_NULLPOINTER,
		     "NULL argument in graphics_init_akgl");
    FAIL_ZERO_RETURN(errctx, (renderer != NULL), AKERR_NULLPOINTER,
		     "NULL renderer in graphics_init_akgl: the host creates it, not this");
    PASS(errctx, akgl_error_init());

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

    obj->self		= state;
    obj->size		= gfx_size;
    obj->point		= gfx_point;
    obj->line		= gfx_line;
    obj->rect		= gfx_rect;
    obj->filled_rect	= gfx_filled_rect;
    obj->paint		= gfx_paint;
    obj->clear		= gfx_clear;
    obj->save_shape	= gfx_save_shape;
    obj->paste_shape	= gfx_paste_shape;
    obj->free_shapes	= gfx_free_shapes;
    SUCCEED_RETURN(errctx);
}

/* ----------------------------------------------------------- the layer -- */

/**
 * @brief Make sure the layer texture exists and is the size of the output.
 *
 * Created on demand rather than at init, because it is the size of the window
 * and a program that never draws should not pay for one. Recreated if the
 * output has since changed size, which is what a resized window looks like from
 * here.
 */
static akerr_ErrorContext AKERR_NOIGNORE *ensure_layer(akbasic_AkglGraphics *state)
{
    PREPARE_ERROR(errctx);
    SDL_Texture *previous = NULL;
    int w = 0;
    int h = 0;
    float texw = 0.0f;
    float texh = 0.0f;

    FAIL_ZERO_RETURN(errctx, SDL_GetCurrentRenderOutputSize(state->renderer->sdl_renderer, &w, &h),
		     AKGL_ERR_SDL, "Couldn't measure the render output: %s", SDL_GetError());
    if ( state->layer != NULL ) {
	if ( SDL_GetTextureSize(state->layer, &texw, &texh)
	     && (int)texw == w && (int)texh == h ) {
	    SUCCEED_RETURN(errctx);
	}
	SDL_DestroyTexture(state->layer);
	state->layer = NULL;
    }
    state->layer = SDL_CreateTexture(state->renderer->sdl_renderer,
				     SDL_PIXELFORMAT_RGBA8888,
				     SDL_TEXTUREACCESS_TARGET, w, h);
    FAIL_ZERO_RETURN(errctx, (state->layer != NULL), AKGL_ERR_SDL,
		     "Couldn't create the drawing layer: %s", SDL_GetError());
    /*
     * Blended, and cleared to fully transparent. The layer covers the whole
     * window and is composited *under* the text and the sprites, so anywhere the
     * program has not drawn has to let what is behind it through -- otherwise a
     * single DRAW would black out the entire frame.
     */
    FAIL_ZERO_RETURN(errctx, SDL_SetTextureBlendMode(state->layer, SDL_BLENDMODE_BLEND),
		     AKGL_ERR_SDL, "%s", SDL_GetError());
    /*
     * **And clear it, because a fresh target texture's contents are undefined.**
     * Skipping this puts whatever was in that memory on the screen under the
     * first frame's text, which is the kind of defect that looks like a driver
     * bug and is not.
     */
    previous = SDL_GetRenderTarget(state->renderer->sdl_renderer);
    FAIL_ZERO_RETURN(errctx, SDL_SetRenderTarget(state->renderer->sdl_renderer, state->layer),
		     AKGL_ERR_SDL, "%s", SDL_GetError());
    if ( !SDL_SetRenderDrawColor(state->renderer->sdl_renderer, 0, 0, 0, 0)
	 || !SDL_RenderClear(state->renderer->sdl_renderer) ) {
	/* Best effort: we are already failing, and leaving the target on the
	   layer would make the next unrelated draw land in the wrong place. */
	(void)SDL_SetRenderTarget(state->renderer->sdl_renderer, previous);
	FAIL_RETURN(errctx, AKGL_ERR_SDL, "Couldn't clear the drawing layer: %s", SDL_GetError());
    }
    FAIL_ZERO_RETURN(errctx, SDL_SetRenderTarget(state->renderer->sdl_renderer, previous),
		     AKGL_ERR_SDL, "%s", SDL_GetError());
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akbasic_graphics_akgl_begin(akbasic_GraphicsBackend *obj)
{
    PREPARE_ERROR(errctx);
    akbasic_AkglGraphics *state = NULL;

    if ( obj == NULL || obj->self == NULL ) {
	SUCCEED_RETURN(errctx);
    }
    state = (akbasic_AkglGraphics *)obj->self;
    PASS(errctx, ensure_layer(state));
    state->savedtarget = SDL_GetRenderTarget(state->renderer->sdl_renderer);
    FAIL_ZERO_RETURN(errctx, SDL_SetRenderTarget(state->renderer->sdl_renderer, state->layer),
		     AKGL_ERR_SDL, "Couldn't make the drawing layer current: %s", SDL_GetError());
    state->layeractive = true;
    SUCCEED_RETURN(errctx);
}

bool akbasic_graphics_akgl_layer_active(akbasic_GraphicsBackend *obj)
{
    if ( obj == NULL || obj->self == NULL ) {
	return false;
    }
    return ((akbasic_AkglGraphics *)obj->self)->layeractive;
}

akerr_ErrorContext *akbasic_graphics_akgl_end(akbasic_GraphicsBackend *obj)
{
    PREPARE_ERROR(errctx);
    akbasic_AkglGraphics *state = NULL;

    if ( obj == NULL || obj->self == NULL ) {
	SUCCEED_RETURN(errctx);
    }
    state = (akbasic_AkglGraphics *)obj->self;
    if ( !state->layeractive ) {
	SUCCEED_RETURN(errctx);
    }
    state->layeractive = false;
    FAIL_ZERO_RETURN(errctx,
		     SDL_SetRenderTarget(state->renderer->sdl_renderer, state->savedtarget),
		     AKGL_ERR_SDL, "Couldn't restore the render target: %s", SDL_GetError());
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akbasic_graphics_akgl_render(akbasic_GraphicsBackend *obj)
{
    PREPARE_ERROR(errctx);
    akbasic_AkglGraphics *state = NULL;

    if ( obj == NULL || obj->self == NULL ) {
	SUCCEED_RETURN(errctx);
    }
    state = (akbasic_AkglGraphics *)obj->self;
    /* Nothing drawn, nothing to composite: a text-only program pays nothing. */
    if ( state->layer == NULL ) {
	SUCCEED_RETURN(errctx);
    }
    FAIL_ZERO_RETURN(errctx,
		     SDL_RenderTexture(state->renderer->sdl_renderer, state->layer, NULL, NULL),
		     AKGL_ERR_SDL, "Couldn't draw the drawing layer: %s", SDL_GetError());
    SUCCEED_RETURN(errctx);
}

void akbasic_graphics_akgl_shutdown(akbasic_GraphicsBackend *obj)
{
    akbasic_AkglGraphics *state = NULL;

    if ( obj == NULL || obj->self == NULL ) {
	return;
    }
    state = (akbasic_AkglGraphics *)obj->self;
    if ( state->layer != NULL ) {
	SDL_DestroyTexture(state->layer);
	state->layer = NULL;
    }
}
