/**
 * @file script.c
 * @brief The boundary: everything that touches the interpreter lives here.
 *
 * One runtime, one script, three host types, three bindings. The engine calls
 * exactly one thing per enemy per frame -- galaga_script_update_enemy() -- and
 * that function is the whole protocol: rebind, call, recover, reset.
 *
 * The structure types are declared once, in C, right below. The script never
 * declares a TYPE of its own; akbasic_host_register_type() makes these structs
 * *be* the BASIC types, offsets taken from offsetof() so the two sides cannot
 * drift (include/akbasic/host.h).
 */

#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>

#include <SDL3/SDL.h>

#include <akerror.h>

#include <akbasic/environment.h>
#include <akbasic/error.h>
#include <akbasic/host.h>
#include <akbasic/runtime.h>
#include <akbasic/sink.h>

#include "galaga.h"

/* The interpreter. Static because an akbasic_Runtime is far too big for a
 * stack frame -- 2.40 MiB on this branch. */
static akbasic_Runtime SCRIPT;
static akbasic_TextSink SINK;
static akbasic_StdioSink SINKSTATE;

/** @brief Longest galaga.bas this loader will accept. */
#define GALAGA_MAX_SCRIPT_BYTES 16384

static char SOURCE[GALAGA_MAX_SCRIPT_BYTES];

/*
 * Enemy kind -> BASIC function name. Dispatch is a table, not a conditional:
 * adding a kind is one row here and one DEF in galaga.bas.
 */
static const char *UPDATE_FUNCTION[GALAGA_ENEMY_KINDS] = {
    "UPDATEBEE",	/* GALAGA_ENEMY_BEE       */
    "UPDATEBFLY",	/* GALAGA_ENEMY_BUTTERFLY */
    "UPDATEBOSS"	/* GALAGA_ENEMY_BOSS      */
};

/* ---------------------------------------------------------- host types --- */

static const akbasic_HostField ENEMY_FIELDS[] = {
    /*                  struct        member  BASIC name  C representation          */
    AKBASIC_HOST_FIELD( galaga_Enemy, kind,   "KIND#",    AKBASIC_HOSTFIELD_INT32 ),
    AKBASIC_HOST_FIELD( galaga_Enemy, state,  "STATE#",   AKBASIC_HOSTFIELD_INT32 ),
    AKBASIC_HOST_FIELD( galaga_Enemy, homex,  "HOMEX%",   AKBASIC_HOSTFIELD_FLOAT ),
    AKBASIC_HOST_FIELD( galaga_Enemy, homey,  "HOMEY%",   AKBASIC_HOSTFIELD_FLOAT ),
    AKBASIC_HOST_FIELD( galaga_Enemy, t,      "T%",       AKBASIC_HOSTFIELD_FLOAT ),
    AKBASIC_HOST_FIELD( galaga_Enemy, hp,     "HP#",      AKBASIC_HOSTFIELD_INT32 ),
    AKBASIC_HOST_FIELD( galaga_Enemy, fire,   "FIRE#",    AKBASIC_HOSTFIELD_INT32 ),
    AKBASIC_HOST_FIELD( galaga_Enemy, rnd,    "ROLL%",    AKBASIC_HOSTFIELD_FLOAT )
};
static const akbasic_HostType ENEMY_TYPE = {
    "ENEMY", sizeof(galaga_Enemy), ENEMY_FIELDS, 8
};

/*
 * The live actor itself. This is the demonstrative point of the whole
 * example: the script writes the engine's *real* actor memory -- the same x
 * the renderer reads -- with no copy in either direction.
 */
static const akbasic_HostField ACTOR_FIELDS[] = {
    AKBASIC_HOST_FIELD( akgl_Actor, x,       "X%",       AKBASIC_HOSTFIELD_FLOAT ),
    AKBASIC_HOST_FIELD( akgl_Actor, y,       "Y%",       AKBASIC_HOSTFIELD_FLOAT ),
    AKBASIC_HOST_FIELD( akgl_Actor, state,   "STATE#",   AKBASIC_HOSTFIELD_INT32 ),
    AKBASIC_HOST_FIELD( akgl_Actor, visible, "VISIBLE#", AKBASIC_HOSTFIELD_BOOL  )
};
static const akbasic_HostType ACTOR_TYPE = {
    "ACTOR", sizeof(akgl_Actor), ACTOR_FIELDS, 4
};

static const akbasic_HostField GAME_FIELDS[] = {
    AKBASIC_HOST_FIELD( galaga_Shared, playerx, "PLAYERX%", AKBASIC_HOSTFIELD_FLOAT ),
    AKBASIC_HOST_FIELD( galaga_Shared, playery, "PLAYERY%", AKBASIC_HOSTFIELD_FLOAT ),
    AKBASIC_HOST_FIELD( galaga_Shared, wave,    "WAVE#",    AKBASIC_HOSTFIELD_INT32 ),
    AKBASIC_HOST_FIELD( galaga_Shared, rnd,     "ROLL%",    AKBASIC_HOSTFIELD_FLOAT )
};
static const akbasic_HostType GAME_TYPE = {
    "GAME", sizeof(galaga_Shared), GAME_FIELDS, 4
};

/* Placeholders the boot bindings point at until the first real rebind. A
 * binding is borrowed, never copied, so these must be static storage. */
static galaga_Enemy SCRATCH_ENEMY;
static akgl_Actor SCRATCH_ACTOR;

/* ---------------------------------------------------------------- boot --- */

static akerr_ErrorContext *read_script(char *path)
{
    FILE *fp = NULL;
    size_t got = 0;
    PREPARE_ERROR(errctx);

    FAIL_ZERO_RETURN(errctx, path, AKERR_NULLPOINTER, "path");
    fp = fopen(path, "rb");
    FAIL_ZERO_RETURN(errctx, fp, AKERR_IO, "Cannot open the enemy script %s", path);
    ATTEMPT {
	got = fread(SOURCE, 1, sizeof(SOURCE) - 1, fp);
	SOURCE[got] = '\0';
	FAIL_NONZERO_BREAK(errctx, (got >= sizeof(SOURCE) - 1), AKERR_OUTOFBOUNDS,
			   "%s does not fit in the %d byte script buffer",
			   path, GALAGA_MAX_SCRIPT_BYTES);
    } CLEANUP {
	fclose(fp);
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

/**
 * @brief One dry call of every dispatch-table function, at boot.
 *
 * A missing or misspelled DEF fails here, at startup, with the function's name
 * in the message -- not on frame one of the first wave. The scratch enemy's
 * state word is zero, so no maneuver block runs and nothing moves.
 */
static akerr_ErrorContext *dry_run(void)
{
    akbasic_Value dt;
    akbasic_Value *argp[1];
    akbasic_Value *result = NULL;
    int i = 0;
    PREPARE_ERROR(errctx);

    memset(&SCRATCH_ENEMY, 0, sizeof(SCRATCH_ENEMY));
    memset(&dt, 0, sizeof(dt));
    dt.valuetype = AKBASIC_TYPE_FLOAT;
    dt.floatval = 0.0;
    argp[0] = &dt;

    for ( i = 0; i < GALAGA_ENEMY_KINDS; i++ ) {
	PASS(errctx, akbasic_host_rebind(&SCRIPT, "SELF@", &SCRATCH_ENEMY));
	PASS(errctx, akbasic_host_rebind(&SCRIPT, "ACTOR@", &SCRATCH_ACTOR));
	PASS(errctx, akbasic_runtime_call_function(&SCRIPT, (char *)UPDATE_FUNCTION[i],
						   argp, 1, &result));
	/*
	 * A body that died reports through the sink and answers zero; the
	 * dropped mode is the only signal C gets. At boot that must be fatal
	 * and must say which function -- not frame one of the first wave.
	 */
	FAIL_NONZERO_RETURN(errctx, (SCRIPT.mode != AKBASIC_MODE_RUN), AKERR_VALUE,
			    "%s died during the boot dry run; the interpreter's report"
			    " is above", UPDATE_FUNCTION[i]);
	PASS(errctx, akbasic_environment_zero(SCRIPT.environment));
    }
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *galaga_script_boot(char *path)
{
    PREPARE_ERROR(errctx);

    FAIL_ZERO_RETURN(errctx, path, AKERR_NULLPOINTER, "path");

    PASS(errctx, akbasic_error_register());
    PASS(errctx, akbasic_sink_init_stdio(&SINK, &SINKSTATE, stdout, NULL));
    PASS(errctx, akbasic_runtime_init(&SCRIPT, &SINK));

    PASS(errctx, akbasic_host_register_type(&SCRIPT, &ENEMY_TYPE));
    PASS(errctx, akbasic_host_register_type(&SCRIPT, &ACTOR_TYPE));
    PASS(errctx, akbasic_host_register_type(&SCRIPT, &GAME_TYPE));

    PASS(errctx, akbasic_host_bind(&SCRIPT, "SELF@", "ENEMY", &SCRATCH_ENEMY));
    PASS(errctx, akbasic_host_bind(&SCRIPT, "ACTOR@", "ACTOR", &SCRATCH_ACTOR));
    PASS(errctx, akbasic_host_bind(&SCRIPT, "GAME@", "GAME", &galaga_shared));

    PASS(errctx, read_script(path));
    PASS(errctx, akbasic_runtime_load(&SCRIPT, SOURCE));

    /*
     * A "no top level code" script still has to run once: executing the DEF
     * statements is what files the functions. The run is bounded because a
     * script that is all definitions has no business taking more than a step
     * per line, and an accidental loop at boot should be a diagnosis, not a
     * hang.
     */
    PASS(errctx, akbasic_runtime_start(&SCRIPT, AKBASIC_MODE_RUN));
    PASS(errctx, akbasic_runtime_run(&SCRIPT, 4 * AKBASIC_MAX_SOURCE_LINES));

    /*
     * The program has now ended and the runtime sits in QUIT mode, where a
     * multi-line DEF called from the host returns a silent zero. Forcing the
     * mode back makes the bodies run, and it stays put because nothing here
     * ever steps the runtime again. Issue #8 tracks making this unnecessary.
     */
    PASS(errctx, akbasic_runtime_set_mode(&SCRIPT, AKBASIC_MODE_RUN));

    PASS(errctx, dry_run());
    SUCCEED_RETURN(errctx);
}

/* ------------------------------------------------------------ per frame --- */

akerr_ErrorContext *galaga_script_update_enemy(galaga_Enemy *enemy, akgl_Actor *actor, float dt)
{
    akbasic_Value dtval;
    akbasic_Value *argp[1];
    akbasic_Value *result = NULL;
    PREPARE_ERROR(errctx);

    FAIL_ZERO_RETURN(errctx, enemy, AKERR_NULLPOINTER, "enemy");
    FAIL_ZERO_RETURN(errctx, actor, AKERR_NULLPOINTER, "actor");
    FAIL_NONZERO_RETURN(errctx, (enemy->kind < 0 || enemy->kind >= GALAGA_ENEMY_KINDS),
			AKERR_VALUE, "Enemy kind %d has no update function", enemy->kind);

    PASS(errctx, akbasic_host_rebind(&SCRIPT, "SELF@", enemy));
    PASS(errctx, akbasic_host_rebind(&SCRIPT, "ACTOR@", actor));

    memset(&dtval, 0, sizeof(dtval));
    dtval.valuetype = AKBASIC_TYPE_FLOAT;
    dtval.floatval = (double)dt;
    argp[0] = &dtval;

    /*
     * An error in an enemy's function is that script's problem, not the
     * engine's: the enemy goes dumb -- cleared to a formation hold it will
     * never leave -- and the frame lives. HANDLE_DEFAULT absorbs whatever the
     * interpreter raised; the first failure is logged with the function's
     * name, the rest are counted, because sixty a second of the same message
     * is how a log stops being read.
     */
    ATTEMPT {
	CATCH(errctx, akbasic_runtime_call_function(&SCRIPT, (char *)UPDATE_FUNCTION[enemy->kind],
						    argp, 1, &result));
    } CLEANUP {
    } PROCESS(errctx) {
    } HANDLE_DEFAULT(errctx) {
	if ( galaga_game.script_errors == 0 ) {
	    LOG_ERROR_WITH_MESSAGE(errctx, "first script error; this enemy is now dumb");
	}
	galaga_game.script_errors += 1;
	enemy->state = GALAGA_ES_FORMATION;
	enemy->fire = 0;
    } FINISH(errctx, true);

    /*
     * A BASIC-level error in the body is quieter than an interpreter error:
     * it reports through the sink, the call answers a stale value, and the
     * runtime falls out of RUN mode -- after which every later call is a
     * silent no-op. The mode is the tell. Revival is two calls:
     * clear_error(), because a run's first error latches and every line is
     * skipped while it stands, and the same set_mode(RUN) the boot needed
     * (issue #8's mechanics). The enemy is treated exactly like the
     * interpreter-error case above.
     */
    if ( SCRIPT.mode != AKBASIC_MODE_RUN ) {
	if ( galaga_game.script_errors == 0 ) {
	    SDL_Log("first script error (reported by the interpreter above);"
		    " enemy %s is now dumb", UPDATE_FUNCTION[enemy->kind]);
	}
	galaga_game.script_errors += 1;
	enemy->state = GALAGA_ES_FORMATION;
	enemy->fire = 0;
	PASS(errctx, akbasic_runtime_clear_error(&SCRIPT));
	PASS(errctx, akbasic_runtime_set_mode(&SCRIPT, AKBASIC_MODE_RUN));
    }

    /*
     * Load-bearing: akbasic_runtime_call_function() parks every result in the
     * caller environment's per-line value scratch, and a host calling in a
     * loop never crosses the line boundary that would reset it. Without this
     * the pool drains in under two frames of a 40-enemy wave.
     */
    PASS(errctx, akbasic_environment_zero(SCRIPT.environment));
    SUCCEED_RETURN(errctx);
}
