/**
 * @file enemies.c
 * @brief The formation, the wave, and the hook that hands each enemy to BASIC.
 *
 * C owns the grid, the wave table and the spawn timing; BASIC owns everything
 * an enemy does after it exists. The formation slot arrives in SELF@.HOMEX% /
 * HOMEY%, so even the idle breathing of the grid is the script's, computed
 * relative to home.
 */

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

#include <SDL3/SDL.h>

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

#include <akgl/actor.h>
#include <akgl/error.h>
#include <akgl/game.h>
#include <akgl/heap.h>

#include "galaga.h"

galaga_Enemy galaga_enemies[GALAGA_MAX_ENEMIES];
akgl_Actor *galaga_enemy_actors[GALAGA_MAX_ENEMIES];

/* Explosion lifetimes, indexed by heap slot. An explosion is an actor with
 * nothing to decide, so its whole state is one countdown. */
static float BOOM_TTL[AKGL_MAX_HEAP_ACTOR];

/* A spawn serial per shot so registry names never collide while two shots
 * with the same slot number are briefly both alive. */
static uint32_t SHOT_SERIAL = 0;
static uint32_t BOOM_SERIAL = 0;

/*
 * The wave, one row per formation row. Columns are 0..9 at GALAGA_COL_PITCH;
 * `first` and `count` say which columns the row fills. 40 enemies: 4 bosses,
 * 16 butterflies, 20 bees -- 59 of the 64 actor heap slots at peak, counting
 * the player, two player shots, eight enemy shots and eight explosions.
 */
static const struct
{
    int32_t kind;	/* GALAGA_ENEMY_*	*/
    int     row;	/* formation row	*/
    int     first;	/* first column filled	*/
    int     count;	/* columns filled	*/
    int32_t hp;
}
WAVE_ROWS[] = {
    /*  kind                    row  first  count  hp  */
    { GALAGA_ENEMY_BOSS,	 0,     3,     4,   2 },
    { GALAGA_ENEMY_BUTTERFLY,	 1,     1,     8,   1 },
    { GALAGA_ENEMY_BUTTERFLY,	 2,     1,     8,   1 },
    { GALAGA_ENEMY_BEE,		 3,     0,    10,   1 },
    { GALAGA_ENEMY_BEE,		 4,     0,    10,   1 }
};
#define WAVE_ROW_COUNT ((int)(sizeof(WAVE_ROWS) / sizeof(WAVE_ROWS[0])))

/* Enemy kind -> character name, the render half of the dispatch table. */
static char *ENEMY_CHARACTER[GALAGA_ENEMY_KINDS] = {
    "galaga_bee",
    "galaga_butterfly",
    "galaga_boss"
};

/* --------------------------------------------------------------- random --- */

/*
 * The engine is this script's source of randomness: it refreshes GAME@.ROLL%
 * each frame and SELF@.ROLL% each call from this PRNG. A hand-rolled LCG
 * rather than rand() so a headless run is the same game on every libc, which
 * is what lets interop_test.c assert exact counts. The dialect gained a native
 * RND (issue #16) after this example was written; the field stays because RND
 * would reintroduce exactly the per-machine variation this avoids.
 *
 * The BASIC-visible name is ROLL%, not RND%: host field names share a
 * namespace with verbs and functions, so RND stopped being available as one.
 */
static uint32_t PRNG_STATE = 0x12345678u;

float galaga_random(void)
{
    PRNG_STATE = PRNG_STATE * 1664525u + 1013904223u;
    return (float)(PRNG_STATE >> 8) / (float)0x01000000u;
}

/* -------------------------------------------------------------- helpers --- */

static akerr_ErrorContext *release_actor(akgl_Actor *actor)
{
    PREPARE_ERROR(errctx);

    FAIL_ZERO_RETURN(errctx, actor, AKERR_NULLPOINTER, "actor");
    PASS(errctx, akgl_heap_release_actor(actor));
    SUCCEED_RETURN(errctx);
}

/* ---------------------------------------------------------------- shots --- */

/**
 * @brief Move an enemy shot; release it once it has left the screen.
 */
static akerr_ErrorContext *enemy_shot_update(akgl_Actor *obj)
{
    PREPARE_ERROR(errctx);

    FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "obj");
    obj->y += 380.0f * galaga_game.dt;
    if ( obj->y > (float)GALAGA_VIEW_HEIGHT + 60.0f ) {
	galaga_game.enemy_shots_live -= 1;
	PASS(errctx, release_actor(obj));
    }
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Consume an enemy's fire flag: take an actor and aim it downward.
 *
 * The script only raises a flag. Spawning takes a slot from the actor heap,
 * and pool exhaustion must be a C-side refusal with the house error context --
 * so C consumes the flag and does the spawn. The engine also enforces the
 * eight-shot cap by simply not consuming the flag's wish.
 */
static akerr_ErrorContext *enemy_fire(galaga_Enemy *enemy, akgl_Actor *from)
{
    akgl_Actor *shot = NULL;
    char name[32];
    int count = 0;
    PREPARE_ERROR(errctx);

    FAIL_ZERO_RETURN(errctx, enemy, AKERR_NULLPOINTER, "enemy");
    FAIL_ZERO_RETURN(errctx, from, AKERR_NULLPOINTER, "from");

    enemy->fire = 0;
    if ( galaga_game.enemy_shots_live >= GALAGA_MAX_ENEMY_SHOTS ) {
	SUCCEED_RETURN(errctx);
    }

    SHOT_SERIAL += 1;
    PASS(errctx, aksl_snprintf(&count, name, sizeof(name), "eshot%u", SHOT_SERIAL));
    PASS(errctx, akgl_heap_next_actor(&shot));
    PASS(errctx, akgl_actor_initialize(shot, name));
    PASS(errctx, akgl_actor_set_character(shot, "galaga_enemyshot"));
    /* AFTER initialize: it resets all seven hooks. */
    shot->updatefunc = &enemy_shot_update;
    shot->movement_controls_face = false;
    shot->state = AKGL_ACTOR_STATE_ALIVE;
    /* akgl_actor_initialize() does not raise `visible`; a hand-spawned actor
     * that skips this line exists, moves and collides -- invisibly. */
    shot->visible = true;
    /* Actor x/y is a sprite's top-left corner; the shot leaves the enemy's
     * midline. Enemy sprites run 93..104 wide, the shot is 9. */
    shot->x = from->x + 46.0f;
    shot->y = from->y + 60.0f;

    galaga_game.enemy_shots_live += 1;
    galaga_game.shots[enemy->kind] += 1;
    SUCCEED_RETURN(errctx);
}

/* ----------------------------------------------------------- explosions --- */

static akerr_ErrorContext *boom_update(akgl_Actor *obj)
{
    ptrdiff_t slot = 0;
    PREPARE_ERROR(errctx);

    FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "obj");
    slot = obj - akgl_heap_actors;
    BOOM_TTL[slot] -= galaga_game.dt;
    if ( BOOM_TTL[slot] <= 0.0f ) {
	PASS(errctx, release_actor(obj));
    }
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *galaga_boom_spawn(float x, float y)
{
    akgl_Actor *boom = NULL;
    char name[32];
    int count = 0;
    PREPARE_ERROR(errctx);

    ATTEMPT {
	CATCH(errctx, akgl_heap_next_actor(&boom));
	BOOM_SERIAL += 1;
	CATCH(errctx, aksl_snprintf(&count, name, sizeof(name), "boom%u", BOOM_SERIAL));
	CATCH(errctx, akgl_actor_initialize(boom, name));
	CATCH(errctx, akgl_actor_set_character(boom, "galaga_boom"));
	boom->updatefunc = &boom_update;
	boom->movement_controls_face = false;
	boom->state = AKGL_ACTOR_STATE_ALIVE;
	boom->visible = true;
	boom->x = x;
	boom->y = y;
	BOOM_TTL[boom - akgl_heap_actors] = 0.25f;
    } CLEANUP {
    } PROCESS(errctx) {
    } HANDLE(errctx, AKGL_ERR_HEAP) {
	/* Explosions are decoration. When the heap is momentarily full the
	 * right outcome is no explosion, not a dead frame -- this is the one
	 * spawn that absorbs exhaustion. */
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

/* --------------------------------------------------------------- enemies --- */

/**
 * @brief The custom update hook: one enemy, once per frame, thought in BASIC.
 *
 * The whole body is the protocol from docs/20: refresh the inbox, hand the
 * pair to the script, consume the outbox. akgl_game_update() calls this in
 * place of akgl_actor_update() because spawn replaced the hook.
 */
static akerr_ErrorContext *enemy_update(akgl_Actor *obj)
{
    galaga_Enemy *enemy = NULL;
    PREPARE_ERROR(errctx);

    FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "obj");
    enemy = (galaga_Enemy *)obj->actorData;
    FAIL_ZERO_RETURN(errctx, enemy, AKERR_NULLPOINTER, "an enemy actor with no galaga_Enemy attached");

    enemy->rnd = galaga_random();
    PASS(errctx, galaga_script_update_enemy(enemy, obj, galaga_game.dt));
    if ( enemy->fire != 0 ) {
	PASS(errctx, enemy_fire(enemy, obj));
    }
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *galaga_wave_spawn(void)
{
    akgl_Actor *actor = NULL;
    galaga_Enemy *enemy = NULL;
    char name[32];
    int row = 0;
    int col = 0;
    int index = 0;
    int count = 0;
    PREPARE_ERROR(errctx);

    for ( row = 0; row < WAVE_ROW_COUNT; row++ ) {
	for ( col = 0; col < WAVE_ROWS[row].count; col++ ) {
	    FAIL_NONZERO_RETURN(errctx, (index >= GALAGA_MAX_ENEMIES), AKERR_OUTOFBOUNDS,
				"The wave table places more than %d enemies", GALAGA_MAX_ENEMIES);
	    enemy = &galaga_enemies[index];
	    memset(enemy, 0, sizeof(*enemy));
	    enemy->kind = WAVE_ROWS[row].kind;
	    enemy->state = GALAGA_ES_ENTERING;
	    enemy->homex = (float)(GALAGA_FORM_LEFT
				   + (WAVE_ROWS[row].first + col) * GALAGA_COL_PITCH);
	    enemy->homey = (float)(GALAGA_FORM_TOP + WAVE_ROWS[row].row * GALAGA_ROW_PITCH);
	    enemy->hp = WAVE_ROWS[row].hp;
	    /* Stagger the entries: each enemy's clock starts in the past, and
	     * the script holds still until its own t crosses zero. */
	    enemy->t = -0.08f * (float)index;

	    PASS(errctx, aksl_snprintf(&count, name, sizeof(name), "enemy%02d", index));
	    PASS(errctx, akgl_heap_next_actor(&actor));
	    PASS(errctx, akgl_actor_initialize(actor, name));
	    PASS(errctx, akgl_actor_set_character(actor, ENEMY_CHARACTER[enemy->kind]));
	    /* AFTER initialize: it resets all seven hooks. */
	    actor->updatefunc = &enemy_update;
	    actor->actorData = enemy;
	    /* Nothing here moves by state bits, and an actor whose state word
	     * matches no character mapping is silently not drawn -- so facing
	     * stays entirely out of the state word. */
	    actor->movement_controls_face = false;
	    actor->state = AKGL_ACTOR_STATE_ALIVE;
	    /* akgl_actor_initialize() does not raise `visible` -- the map
	     * loader copies it from map data, and there is no map here. Skip
	     * this and the whole wave exists, moves, fires and dies without
	     * ever being drawn. */
	    actor->visible = true;
	    /* Off screen above, pouring in from whichever side is closer. */
	    actor->x = (enemy->homex < (float)GALAGA_VIEW_WIDTH / 2.0f)
		? -80.0f : (float)GALAGA_VIEW_WIDTH + 80.0f;
	    actor->y = -80.0f;

	    galaga_enemy_actors[index] = actor;
	    index += 1;
	}
    }
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *galaga_wave_release(void)
{
    int i = 0;
    PREPARE_ERROR(errctx);

    for ( i = 0; i < GALAGA_MAX_ENEMIES; i++ ) {
	if ( galaga_enemy_actors[i] != NULL ) {
	    PASS(errctx, release_actor(galaga_enemy_actors[i]));
	    galaga_enemy_actors[i] = NULL;
	}
    }
    SUCCEED_RETURN(errctx);
}

int galaga_enemies_alive(void)
{
    int i = 0;
    int alive = 0;

    for ( i = 0; i < GALAGA_MAX_ENEMIES; i++ ) {
	if ( galaga_enemy_actors[i] != NULL ) {
	    alive += 1;
	}
    }
    return alive;
}
