/**
 * @file frontend_akgl.c
 * @brief The standalone program's SDL host, ported from the Go frontend.
 *
 * The reference's main.go opens an 800x600 window and a Commodore font and hands
 * both to the runtime, which then owns the process until MODE_QUIT
 * (basicruntime.go:682). Goal 3 forbids the second half of that outright, so
 * what is ported here is the first half plus the loop the reference never had to
 * write: pump events, run a bounded number of interpreter steps, draw, present,
 * repeat.
 *
 * **This file is a host.** It is the only thing in the repository that creates a
 * window, and it is in its own target so that fact is visible in the build graph
 * rather than only in a comment. A game embedding the interpreter does not link
 * it -- it already has a window, and it calls the four initializers in
 * akbasic/akgl.h against its own.
 *
 * Everything the four adaptors need is created here and nowhere else:
 *
 *     window + renderer ---> the akgl sink and the graphics backend
 *     SDL event pump     ---> akgl_controller_handle_event, the input backend
 *     the frame          ---> akbasic_sink_akgl_render, and the line editor
 */

#include <stddef.h>

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

#include <akgl/controller.h>
#include <akgl/error.h>
/*
 * game.h purely for the `akgl_renderer` global and its `akgl_default_renderer`
 * storage.
 * akgl_text_rendertextat() takes no renderer argument and reads that global, so
 * a host that never calls akgl_game_init() -- which is every host embedding this
 * interpreter, since owning the game loop is exactly what goal 3 forbids -- has
 * to populate it itself. deps/libakgl/tests/draw.c does the same.
 */
#include <akgl/game.h>
#include <akgl/heap.h>
#include <akgl/registry.h>
#include <akgl/renderer.h>

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

akerr_ErrorContext *akbasic_frontend_akgl_init(akbasic_AkglFrontend *obj, const char *title, int w, int h, const char *fontpath, int fontsize, FILE *mirror, FILE *in)
{
    PREPARE_ERROR(errctx);
    akbasic_TextSink *reader = NULL;

    FAIL_ZERO_RETURN(errctx, (obj != NULL && title != NULL && fontpath != NULL),
		     AKERR_NULLPOINTER, "NULL argument in frontend_akgl_init");
    FAIL_ZERO_RETURN(errctx, (w > 0 && h > 0 && fontsize > 0), AKBASIC_ERR_VALUE,
		     "A %dx%d window at %d points is not a window", w, h, fontsize);

    PASS(errctx, aksl_memset(obj, 0, sizeof(*obj)));
    obj->width = w;
    obj->height = h;

    /* Before anything else in libakgl, so every AKGL_ERR_* has a name to print. */
    PASS(errctx, akgl_error_init());

    /*
     * INIT_VIDEO only. The reference asks for INIT_EVERYTHING, which on a
     * headless machine fails on a subsystem BASIC never touches; audio is opened
     * separately below and is allowed to fail.
     */
    FAIL_ZERO_RETURN(errctx, SDL_Init(SDL_INIT_VIDEO), AKGL_ERR_SDL,
		     "Couldn't initialize SDL: %s", SDL_GetError());
    obj->ownssdl = true;
    FAIL_ZERO_RETURN(errctx, TTF_Init(), AKGL_ERR_SDL,
		     "Couldn't initialize SDL_ttf: %s", SDL_GetError());

    obj->renderer = &akgl_default_renderer;
    akgl_renderer = obj->renderer;
    FAIL_ZERO_RETURN(errctx,
		     SDL_CreateWindowAndRenderer(title, w, h, 0,
						 &obj->window, &obj->renderer->sdl_renderer),
		     AKGL_ERR_SDL, "Couldn't create the window: %s", SDL_GetError());
    akgl_window = obj->window;

    /*
     * Without this SDL sends no SDL_EVENT_TEXT_INPUT at all, every keystroke
     * reaches the ring with an empty `text`, and the line editor has nothing to
     * type -- akgl/controller.h says so directly. It is off by default in SDL3
     * and is per-window, so it is the host's job and this is the host.
     *
     * Not fatal if it fails: the editor falls back to the keycode, which is a
     * worse keyboard rather than no keyboard. Failing to start a window over an
     * input method that is not there would be the wrong trade.
     */
    if ( !SDL_StartTextInput(obj->window) ) {
	SDL_Log("Could not start text input (%s); typing falls back to keycodes",
		SDL_GetError());
    }
    /*
     * Bind the 2D methods onto the renderer we just made. akgl_render_2d_init()
     * would do this too, but it creates its own window from the game properties
     * first, which is the akgl_game_init() path a host with its own window is
     * not on. libakgl 0.3.0 split the vtable half out for exactly this caller --
     * it was API-gap item 7, and until it landed these six pointers were
     * assigned by hand here and in tests/akgl_backends.c.
     */
    PASS(errctx, akgl_render_2d_bind(obj->renderer));

    /*
     * The object pools and the name registries. Every akgl_*_initialize ends in
     * a registry write and fails AKERR_KEY without these, so anything drawn as
     * an actor -- which is what a BASIC sprite is -- needs them. akgl_game_init()
     * is where they would normally be done, and it is the function goal 3 keeps
     * this host out of.
     *
     * Unconditional, and it has to be. akgl_registry_init() overwrites the
     * property-set ids without destroying the old sets, so repeating it would
     * normally leak -- but this frontend owns SDL_Init and SDL_Quit, and
     * SDL_Quit destroys every property set while leaving libakgl's ids pointing
     * at them. So the previous registries are already gone, and skipping the
     * call on the strength of a non-zero id is what actually breaks: the second
     * frontend of a process gets ids that name nothing, and the first
     * akgl_sprite_initialize fails AKERR_KEY.
     *
     * A game that embeds the interpreter does not link this file. It has already
     * called akgl_game_init(), which does both of these itself.
     */
    PASS(errctx, akgl_heap_init());
    PASS(errctx, akgl_registry_init());

    /*
     * And the camera. akgl_actor_render() dereferences this global to translate
     * world coordinates into screen ones and does not check it, so an unset
     * camera is a crash the first time a sprite is drawn rather than an error
     * anybody could act on. One screen's worth at the origin: BASIC has no verb
     * that scrolls a view, so the world and the screen are the same thing here.
     */
    akgl_camera = &akgl_default_camera;
    akgl_camera->x = 0.0f;
    akgl_camera->y = 0.0f;
    akgl_camera->w = (float)w;
    akgl_camera->h = (float)h;

    obj->font = TTF_OpenFont(fontpath, (float)fontsize);
    FAIL_ZERO_RETURN(errctx, (obj->font != NULL), AKGL_ERR_SDL,
		     "Couldn't open the font %s: %s", fontpath, SDL_GetError());

    /*
     * The two halves of the output, and then the tee that joins them. Which one
     * answers readline is the whole difference between the two modes: a program
     * read from a file comes in through the stdio half, and typed lines come in
     * through the akgl half's line editor.
     */
    PASS(errctx, akbasic_sink_init_akgl(&obj->akglsink, &obj->akglstate,
					obj->renderer, obj->font, w, h));
    PASS(errctx, akbasic_sink_init_stdio(&obj->stdiosink, &obj->stdiostate,
					 (mirror != NULL ? mirror : stdout), in));
    reader = (in != NULL ? &obj->stdiosink : &obj->akglsink);
    PASS(errctx, akbasic_sink_init_tee(&obj->sink, &obj->teestate,
				       &obj->akglsink, &obj->stdiosink, reader));

    /*
     * The editor waits for a typed line by borrowing frames from this very
     * frontend -- which is why pump() has the akbasic_AkglPump signature and can
     * be installed as-is.
     */
    PASS(errctx, akbasic_sink_akgl_set_pump(&obj->akglsink,
					    akbasic_frontend_akgl_pump, obj));

    PASS(errctx, akbasic_graphics_init_akgl(&obj->graphics, &obj->graphicsstate,
					    obj->renderer));
    PASS(errctx, akbasic_input_init_akgl(&obj->input));
    /*
     * The graphics state goes with it so SPRSAV can turn an SSHAPE handle into a
     * sprite. The two devices stay separate records -- a host may lend one and
     * withhold the other -- and this is the one place they meet.
     */
    PASS(errctx, akbasic_sprite_init_akgl(&obj->sprites, &obj->spritesstate,
					  obj->renderer, &obj->graphicsstate));
    /*
     * And the UI, which loads the same font file again -- under a registry name
     * of its own, at the same size. That is not a duplicate by accident: the
     * sink holds a TTF_Font it opened itself and never registers it, and clay
     * addresses a font by registry name, so the two cannot share one entry.
     */
    PASS(errctx, akbasic_ui_init_akgl(&obj->ui, &obj->uistate, obj->renderer,
				      fontpath, fontsize, w, h));

    /*
     * Audio last, in its own subsystem, and allowed to fail. The reference asks
     * for INIT_EVERYTHING up front, which makes a machine with no sound card
     * refuse to run BASIC at all; here the failure costs only SOUND and PLAY,
     * which are then refused with AKBASIC_ERR_DEVICE -- exactly what a stdio
     * build does. The subsystem is tried first because akgl_audio_init() cannot
     * open a device without it, and reporting "no audio device" for what is
     * really "audio was never initialized" would send somebody looking at their
     * hardware.
     */
    if ( !SDL_InitSubSystem(SDL_INIT_AUDIO) ) {
	SDL_Log("No audio subsystem (%s); SOUND and PLAY will be refused", SDL_GetError());
	obj->audioready = false;
    } else {
	ATTEMPT {
	    CATCH(errctx, akbasic_audio_init_akgl(&obj->audio));
	    obj->audioready = true;
	} CLEANUP {
	} PROCESS(errctx) {
	} HANDLE_DEFAULT(errctx) {
	    LOG_ERROR_WITH_MESSAGE(errctx, "No audio device; SOUND and PLAY will be refused");
	    obj->audioready = false;
	} FINISH(errctx, false);
    }

    obj->running = true;
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akbasic_frontend_akgl_attach(akbasic_AkglFrontend *obj, akbasic_Runtime *rt)
{
    PREPARE_ERROR(errctx);

    FAIL_ZERO_RETURN(errctx, (obj != NULL && rt != NULL), AKERR_NULLPOINTER,
		     "NULL argument in frontend_akgl_attach");
    PASS(errctx, akbasic_runtime_init(rt, &obj->sink));
    PASS(errctx, akbasic_runtime_set_devices(rt, &obj->graphics,
					     (obj->audioready ? &obj->audio : NULL),
					     &obj->input, &obj->sprites));
    PASS(errctx, akbasic_runtime_set_ui(rt, &obj->ui));
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Drain the SDL event queue into libakgl's keystroke ring.
 *
 * Its own function because it is a loop, and CATCH inside one escapes only the
 * loop -- PASS is the only thing that may appear in here.
 */
static akerr_ErrorContext AKERR_NOIGNORE *pump_events(akbasic_AkglFrontend *obj)
{
    PREPARE_ERROR(errctx);
    SDL_Event event;
    bool consumed = false;

    while ( SDL_PollEvent(&event) ) {
	if ( event.type == SDL_EVENT_QUIT ||
	     event.type == SDL_EVENT_WINDOW_CLOSE_REQUESTED ) {
	    obj->running = false;
	    continue;
	}
	/*
	 * The UI gets first refusal. The mouse half never takes a keystroke, but
	 * a menu with entries takes Up, Down and Return -- which is why this has
	 * to come *before* the ring is filled, and why retiring a menu is how a
	 * program gives those keys back to the line editor.
	 */
	PASS(errctx, akbasic_ui_akgl_handle_event(&obj->ui, &event, &consumed));
	if ( consumed ) {
	    continue;
	}
	/*
	 * Everything else goes to libakgl, which pushes key-downs into the ring
	 * the input backend reads before it consults its own control maps. The
	 * appstate is the frontend, which nothing downstream reads -- but a NULL
	 * one is refused outright.
	 */
	PASS(errctx, akgl_controller_handle_event(obj, &event));
    }
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akbasic_frontend_akgl_pump(void *self, bool *running)
{
    PREPARE_ERROR(errctx);
    akbasic_AkglFrontend *obj = (akbasic_AkglFrontend *)self;
    bool resume = false;

    FAIL_ZERO_RETURN(errctx, (obj != NULL && running != NULL), AKERR_NULLPOINTER,
		     "NULL argument in frontend_akgl_pump");

    PASS(errctx, pump_events(obj));
    *running = obj->running;
    if ( !obj->running ) {
	SUCCEED_RETURN(errctx);
    }

    /*
     * **Take the drawing layer off the target before presenting.**
     *
     * SDL refuses to present while a render target is current, and this function
     * is called from two places with different answers to "is one current". The
     * frame loop calls it between steps, with the layer off. The sink's *line
     * editor* calls it from inside a step -- borrowing a frame while it waits
     * for a typed line, which is how a REPL redraws without owning a loop -- and
     * there the layer is very much on.
     *
     * So end it here and put it back afterwards. `resume` remembers which case
     * this was, because unconditionally beginning would leave the layer current
     * after a frame-loop pump and every subsequent draw would land in it at the
     * wrong time.
     */
    resume = akbasic_graphics_akgl_layer_active(&obj->graphics);
    PASS(errctx, akbasic_graphics_akgl_end(&obj->graphics));

    /*
     * No clear. The text layer is drawn over whatever is already there, which is
     * also how a C128 stacks its text plane on its bitmap plane -- and the
     * drawing layer beneath it is composited rather than accumulated, so there
     * is nothing here that a clear would fix.
     */
    /*
     * The drawing layer first, under everything. A drawing verb rendered into a
     * texture rather than into the back buffer -- see
     * akbasic_graphics_akgl_begin() -- so this is where what the program drew
     * reaches the frame, and it is why a picture no longer has to be redrawn
     * every frame or captured into a sprite to be kept.
     */
    PASS(errctx, akbasic_graphics_akgl_render(&obj->graphics));
    PASS(errctx, akbasic_sink_akgl_render(&obj->akglsink));
    /*
     * Sprites last, so they are over the text as well as over the drawing. That
     * is what a C128 does with a high-priority sprite, and the low-priority case
     * -- behind the bitmap plane -- has nowhere to go here: see the note in
     * spr_configure().
     */
    PASS(errctx, akbasic_sprite_akgl_render(&obj->sprites));
    /*
     * The UI last of all, over the sprites as well as over the text. A menu the
     * player is being asked to act on is not something a sprite may cover.
     */
    PASS(errctx, akbasic_ui_akgl_render(&obj->ui));
    FAIL_ZERO_RETURN(errctx, SDL_RenderPresent(obj->renderer->sdl_renderer),
		     AKGL_ERR_SDL, "Couldn't present the frame: %s", SDL_GetError());
    if ( resume ) {
	PASS(errctx, akbasic_graphics_akgl_begin(&obj->graphics));
    }
    SUCCEED_RETURN(errctx);
}

/**
 * @brief The frame loop, in its own function for the usual reason: it is a loop.
 */
static akerr_ErrorContext AKERR_NOIGNORE *drive_loop(akbasic_AkglFrontend *obj, akbasic_Runtime *rt)
{
    PREPARE_ERROR(errctx);
    bool running = true;

    while ( running && rt->mode != AKBASIC_MODE_QUIT ) {
	/*
	 * The host owns the clock -- the library reads none, because it owns no
	 * loop and must not block. SDL_GetTicks() is monotonic milliseconds,
	 * which is exactly what settime wants and saves this file a
	 * clock_gettime and a _POSIX_C_SOURCE.
	 */
	PASS(errctx, akbasic_runtime_settime(rt, (int64_t)SDL_GetTicks()));
	/*
	 * The steps run with the drawing layer current, so a DRAW lands in a
	 * texture that outlives the frame. Bracketed here rather than inside
	 * each verb: one pair of SDL_SetRenderTarget calls a frame instead of
	 * one per statement, and it is also what makes SSHAPE read back what the
	 * program has just drawn.
	 */
	PASS(errctx, akbasic_graphics_akgl_begin(&obj->graphics));
	PASS(errctx, akbasic_runtime_run(rt, AKBASIC_FRONTEND_STEPS_PER_FRAME));
	PASS(errctx, akbasic_graphics_akgl_end(&obj->graphics));
	PASS(errctx, akbasic_frontend_akgl_pump(obj, &running));
    }
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akbasic_frontend_akgl_drive(akbasic_AkglFrontend *obj, akbasic_Runtime *rt)
{
    PREPARE_ERROR(errctx);

    FAIL_ZERO_RETURN(errctx, (obj != NULL && rt != NULL), AKERR_NULLPOINTER,
		     "NULL argument in frontend_akgl_drive");
    PASS(errctx, drive_loop(obj, rt));
    SUCCEED_RETURN(errctx);
}

void akbasic_frontend_akgl_shutdown(akbasic_AkglFrontend *obj)
{
    if ( obj == NULL ) {
	return;
    }
    /* Before the renderer goes: these are textures it created. */
    akbasic_ui_akgl_shutdown(&obj->ui);
    akbasic_sprite_akgl_shutdown(&obj->sprites);
    akbasic_graphics_akgl_shutdown(&obj->graphics);
    if ( obj->font != NULL ) {
	TTF_CloseFont(obj->font);
	obj->font = NULL;
    }
    if ( obj->renderer != NULL && obj->renderer->sdl_renderer != NULL ) {
	SDL_DestroyRenderer(obj->renderer->sdl_renderer);
	obj->renderer->sdl_renderer = NULL;
    }
    if ( obj->window != NULL ) {
	SDL_StopTextInput(obj->window);
	SDL_DestroyWindow(obj->window);
	obj->window = NULL;
	akgl_window = NULL;
    }
    TTF_Quit();
    if ( obj->ownssdl ) {
	SDL_Quit();
	obj->ownssdl = false;
    }
    obj->running = false;
}
