/**
 * @file screenshot.c
 * @brief Run one BASIC program against an offscreen renderer and save a PNG.
 *
 * The documentation's graphics and sprite chapters describe what a verb draws,
 * and a sentence is a poor way to describe a picture. This is what turns the
 * listing in a chapter into the image beside it -- tools/docs_screenshots.sh
 * extracts the listing from the markdown and hands it here, so the picture is
 * generated from the exact program the reader is looking at rather than from a
 * copy that can drift away from it.
 *
 * **It is a host, like src/frontend_akgl.c**, and it is deliberately a separate
 * one: the frontend opens a real window, pumps events and never stops until the
 * script quits, none of which a build step wants. What this does instead is the
 * shortest path to pixels -- the dummy video driver, a software renderer, run to
 * completion, read the target back, write it out. deps/libakgl/tests/draw.c and
 * tests/akgl_backends.c set that pattern; this is the third user of it.
 *
 * **The text layer is not drawn unless a font is named.** A screenshot in the
 * graphics chapter is of the drawing, and a `READY` in the corner is noise in a
 * figure about `CIRCLE`; leaving the layer out also means no font has to be
 * found, which keeps this runnable from a build tree with no assets resolved.
 *
 * A figure of a program whose *output is text* needs the opposite, and
 * docs/17-tutorial-breakout.md is one: that game's wall, HUD and messages are
 * characters in the grid, so a picture without the text layer is a picture of
 * two sprites on a black field. Naming a font as the fifth argument swaps the
 * stdio sink for the akgl one and renders the grid before the sprites, in the
 * same order src/frontend_akgl.c uses. The fence attribute that asks for it is
 * `text=1`; tools/docs_screenshots.sh turns that into this argument.
 *
 * The sink is the akgl one *alone* rather than a tee, so a program's output
 * lands in the picture rather than on stdout -- which is what the caller reads
 * to decide a figure failed. Nothing is lost: `screenshot=` blocks are also run
 * by tests/docs_examples.sh, which is where a raised error is caught.
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <SDL3/SDL.h>
#include <SDL3_image/SDL_image.h>
#include <SDL3_ttf/SDL_ttf.h>

#include <akerror.h>

#include <akgl/error.h>
/*
 * game.h for the `renderer` and `camera` globals: akgl_actor_render() reads
 * both without taking either as an argument, so a host that never calls
 * akgl_game_init() -- which is every host here, because owning the game loop is
 * what goal 3 forbids -- populates them itself. Same three lines as
 * src/frontend_akgl.c and tests/akgl_backends.c, and each says so.
 */
#include <akgl/game.h>
#include <akgl/heap.h>
#include <akgl/registry.h>
#include <akgl/renderer.h>

#include <akbasic/akgl.h>
#include <akbasic/error.h>
#include <akbasic/frontend.h>
#include <akbasic/runtime.h>
#include <akbasic/sink.h>

/** @brief The interpreter carries every pool it owns, so it does not fit on a stack. */
static akbasic_Runtime RUNTIME;
static akbasic_TextSink SINK;
static akbasic_StdioSink SINKSTATE;
/** @brief The text grid, and the font behind it. Only used when one is named. */
static akbasic_AkglSink GRIDSTATE;
static TTF_Font *FONT = NULL;
static akbasic_GraphicsBackend GRAPHICS;
static akbasic_AkglGraphics GRAPHICSSTATE;
static akbasic_SpriteBackend SPRITES;
static akbasic_AkglSprites SPRITESSTATE;
static akbasic_UiBackend UI;
static akbasic_AkglUi UISTATE;
/*
 * No `window` of our own: akgl/game.h declares one as an unprefixed extern --
 * libakgl's TODO calls that a defect and it is -- so a static here shadows it
 * and the file will not compile. Writing the global is what a host does anyway.
 */
/** @brief The whole program, read in one go. Longer than any figure's listing. */
static char SOURCE[65536];

static void usage(void)
{
    fprintf(stderr,
	    "usage: akbasic_screenshot <program.bas> <output.png> [width] [height] [font.ttf] [grid]\n"
	    "\n"
	    "  Runs the program against an offscreen renderer of the given size\n"
	    "  (default 320x200) and writes what it drew as a PNG.\n"
	    "\n"
	    "  Naming a font lends the program a UI device, because the widgets\n"
	    "  draw text and cannot come up without one. Passing 1 for `grid` as\n"
	    "  well draws the text layer, for a figure of a program whose output\n"
	    "  is characters rather than drawing.\n");
}

/** @brief Slurp the program. A figure's listing is small; a partial read is not tolerated. */
static akerr_ErrorContext AKERR_NOIGNORE *read_program(const char *path)
{
    PREPARE_ERROR(errctx);
    FILE *in = NULL;
    size_t got = 0;

    memset(SOURCE, 0, sizeof(SOURCE));
    in = fopen(path, "r");
    FAIL_ZERO_RETURN(errctx, (in != NULL), AKERR_IO, "could not open %s", path);
    got = fread(SOURCE, 1, sizeof(SOURCE) - 1, in);
    fclose(in);
    FAIL_ZERO_RETURN(errctx, (got > 0), AKERR_IO, "%s is empty", path);
    FAIL_ZERO_RETURN(errctx, (got < sizeof(SOURCE) - 1), AKERR_OUTOFBOUNDS,
		     "%s is longer than %zu bytes", path, sizeof(SOURCE) - 1);
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Everything between SDL being up and the pixels being readable.
 *
 * Split out so the ATTEMPT in main() has one thing to CATCH and the SDL
 * teardown has one place to happen.
 */
static akerr_ErrorContext AKERR_NOIGNORE *draw_program(const char *outpath, int w, int h,
						       const char *fontpath, bool grid)
{
    PREPARE_ERROR(errctx);
    SDL_Surface *shot = NULL;

    PASS(errctx, akgl_error_init());
    akgl_renderer = &akgl_default_renderer;

    FAIL_ZERO_RETURN(errctx, SDL_Init(SDL_INIT_VIDEO), AKGL_ERR_SDL,
		     "Couldn't initialize SDL: %s", SDL_GetError());
    FAIL_ZERO_RETURN(errctx,
		     SDL_CreateWindowAndRenderer("net/aklabs/akbasic/screenshot", w, h, 0,
						 &akgl_window, &akgl_renderer->sdl_renderer),
		     AKGL_ERR_SDL, "Couldn't create window/renderer: %s", SDL_GetError());
    PASS(errctx, akgl_render_2d_bind(akgl_renderer));
    PASS(errctx, akgl_heap_init());
    PASS(errctx, akgl_registry_init());
    akgl_camera = &akgl_default_camera;
    akgl_camera->x = 0.0f;
    akgl_camera->y = 0.0f;
    akgl_camera->w = (float32_t)w;
    akgl_camera->h = (float32_t)h;

    /*
     * Opaque black, so a figure has a defined background rather than whatever
     * the driver left in the buffer. GRAPHIC's own clear is a filled rectangle
     * over the output for a reason src/graphics_akgl.c explains -- clearing is
     * the host's prerogative -- and this is the host doing it.
     */
    FAIL_ZERO_RETURN(errctx, SDL_SetRenderDrawColor(akgl_renderer->sdl_renderer, 0, 0, 0, 0xff),
		     AKGL_ERR_SDL, "%s", SDL_GetError());
    FAIL_ZERO_RETURN(errctx, SDL_RenderClear(akgl_renderer->sdl_renderer),
		     AKGL_ERR_SDL, "%s", SDL_GetError());

    /*
     * With no font: stdout, so a program that errors puts its
     * "? line : CLASS message" where the caller reads it. With one: the grid,
     * so the characters the program writes are in the picture. The file comment
     * explains why the two are not teed together.
     */
    /*
     * The font and the text grid are two separate questions, and they were one
     * until the UI verbs arrived. A widget draws text, so a UI figure needs a
     * font -- but it does not want the grid, which owns every pixel of the rows
     * it covers and would paint the picture black before the widgets landed on
     * it. So `fontpath` says "there is a font" and `grid` says "draw the text
     * layer too".
     */
    if ( fontpath == NULL ) {
	PASS(errctx, akbasic_sink_init_stdio(&SINK, &SINKSTATE, stdout, NULL));
    } else {
	FAIL_ZERO_RETURN(errctx, TTF_Init(), AKGL_ERR_SDL,
			 "Couldn't initialize SDL_ttf: %s", SDL_GetError());
	FONT = TTF_OpenFont(fontpath, (float)AKBASIC_FRONTEND_FONT_SIZE);
	FAIL_ZERO_RETURN(errctx, (FONT != NULL), AKGL_ERR_SDL,
			 "Couldn't open the font %s: %s", fontpath, SDL_GetError());
	if ( grid ) {
	    PASS(errctx, akbasic_sink_init_akgl(&SINK, &GRIDSTATE, akgl_renderer, FONT, w, h));
	} else {
	    PASS(errctx, akbasic_sink_init_stdio(&SINK, &SINKSTATE, stdout, NULL));
	}
    }
    PASS(errctx, akbasic_runtime_init(&RUNTIME, &SINK));
    PASS(errctx, akbasic_graphics_init_akgl(&GRAPHICS, &GRAPHICSSTATE, akgl_renderer));
    PASS(errctx, akbasic_sprite_init_akgl(&SPRITES, &SPRITESSTATE, akgl_renderer, &GRAPHICSSTATE));
    PASS(errctx, akbasic_runtime_set_devices(&RUNTIME, &GRAPHICS, NULL, NULL, &SPRITES));
    if ( fontpath != NULL ) {
	PASS(errctx, akbasic_ui_init_akgl(&UI, &UISTATE, akgl_renderer, fontpath,
					  AKBASIC_FRONTEND_FONT_SIZE, w, h));
	PASS(errctx, akbasic_runtime_set_ui(&RUNTIME, &UI));
    }
    PASS(errctx, akbasic_runtime_load(&RUNTIME, SOURCE));
    PASS(errctx, akbasic_runtime_start(&RUNTIME, AKBASIC_MODE_RUN));
    /* The same layer bracket and composition order the standalone frontend uses. */
    PASS(errctx, akbasic_graphics_akgl_begin(&GRAPHICS));
    PASS(errctx, akbasic_runtime_run(&RUNTIME, 0));
    PASS(errctx, akbasic_graphics_akgl_end(&GRAPHICS));

    /*
     * The drawing layer, text grid and sprites all redraw from retained state.
     * Keep this in the frontend's order so a documentation image exercises the
     * same GRAPHIC composition users run rather than a convenient direct path.
     */
    PASS(errctx, akbasic_graphics_akgl_render(&GRAPHICS));
    if ( grid ) {
	PASS(errctx, akbasic_sink_akgl_render(&SINK));
    }
    PASS(errctx, akbasic_sprite_akgl_render(&SPRITES));
    /* Last, and over everything, exactly as src/frontend_akgl.c orders it. */
    if ( fontpath != NULL ) {
	PASS(errctx, akbasic_ui_akgl_render(&UI));
    }

    shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
    FAIL_ZERO_RETURN(errctx, (shot != NULL), AKGL_ERR_SDL,
		     "Couldn't read the target back: %s", SDL_GetError());
    if ( !IMG_SavePNG(shot, outpath) ) {
	SDL_DestroySurface(shot);
	FAIL_RETURN(errctx, AKGL_ERR_SDL, "Couldn't write %s: %s", outpath, SDL_GetError());
    }
    SDL_DestroySurface(shot);
    SUCCEED_RETURN(errctx);
}

int main(int argc, char **argv)
{
    PREPARE_ERROR(errctx);
    int w = 320;
    int h = 200;
    const char *fontpath = NULL;
    bool grid = false;

    if ( argc < 3 ) {
	usage();
	return 2;
    }
    if ( argc > 3 ) {
	w = atoi(argv[3]);
    }
    if ( argc > 4 ) {
	h = atoi(argv[4]);
    }
    if ( argc > 5 && argv[5][0] != '\0' ) {
	fontpath = argv[5];
    }
    if ( argc > 6 && argv[6][0] == '1' ) {
	grid = true;
    }
    if ( w <= 0 || h <= 0 ) {
	usage();
	return 2;
    }

    /*
     * Set here rather than left to the environment so the tool answers the same
     * on a developer's desktop as it does in CI. A figure regenerated over a
     * real GPU could differ by a pixel of antialiasing and turn every rebuild
     * into a diff.
     */
    SDL_SetHint(SDL_HINT_VIDEO_DRIVER, "dummy");
    SDL_SetHint(SDL_HINT_RENDER_DRIVER, "software");

    ATTEMPT {
	CATCH(errctx, read_program(argv[1]));
	CATCH(errctx, draw_program(argv[2], w, h, fontpath, grid));
    } CLEANUP {
	akbasic_ui_akgl_shutdown(&UI);
	if ( FONT != NULL ) {
	    TTF_CloseFont(FONT);
	    FONT = NULL;
	    TTF_Quit();
	}
	if ( akgl_window != NULL ) {
	    SDL_DestroyWindow(akgl_window);
	    akgl_window = NULL;
	}
	SDL_Quit();
    } PROCESS(errctx) {
    } HANDLE_DEFAULT(errctx) {
	LOG_ERROR_WITH_MESSAGE(errctx, "could not draw the screenshot");
	return 1;
	/*
	 * FINISH_NORETURN rather than FINISH, matching src/main.c and
	 * tests/akgl_backends.c: FINISH expands a `return __err_context` that
	 * this int-returning function cannot compile even where the branch is
	 * unreachable.
	 */
    } FINISH_NORETURN(errctx);

    return 0;
}
