/**
 * @file player.c
 * @brief The player's ship, its shots, and every collision in the game.
 *
 * Bullets and collision are C forever -- they are engine, not behavior. The
 * per-frame budget for the script is spent on the forty things that think;
 * nothing here thinks, it just moves and intersects.
 */

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

#include <SDL3/SDL.h>

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

#include <akgl/actor.h>
#include <akgl/controller.h>
#include <akgl/game.h>
#include <akgl/heap.h>
#include <akgl/registry.h>
#include <akgl/util.h>

#include "galaga.h"

/* Points per kill, indexed by enemy kind. */
static const int KILL_SCORE[GALAGA_ENEMY_KINDS] = {
    /* bee  butterfly  boss */
       50,        80,   150
};

static uint32_t PSHOT_SERIAL = 0;

/* -------------------------------------------------------------- hitboxes --- */

/*
 * Actor x/y is a sprite's top-left corner. Every box is inset from the
 * artwork's rectangle, because the PNGs carry transparent margin and wing
 * tips that should not kill anybody.
 */
static void player_box(akgl_Actor *actor, SDL_FRect *dest)
{
    dest->x = actor->x + 12.0f;
    dest->y = actor->y + 8.0f;
    dest->w = 75.0f;
    dest->h = 60.0f;
}

static void enemy_box(akgl_Actor *actor, SDL_FRect *dest)
{
    dest->x = actor->x + 8.0f;
    dest->y = actor->y + 8.0f;
    dest->w = 78.0f;
    dest->h = 68.0f;
}

static void shot_box(akgl_Actor *actor, SDL_FRect *dest)
{
    dest->x = actor->x;
    dest->y = actor->y;
    dest->w = 9.0f;
    dest->h = 54.0f;
}

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

/**
 * @brief Kill one enemy: score it, blow it up, free its slot.
 */
static akerr_ErrorContext *kill_enemy(int index)
{
    akgl_Actor *actor = NULL;
    galaga_Enemy *enemy = NULL;
    PREPARE_ERROR(errctx);

    FAIL_NONZERO_RETURN(errctx, (index < 0 || index >= GALAGA_MAX_ENEMIES),
			AKERR_OUTOFBOUNDS, "enemy index %d", index);
    actor = galaga_enemy_actors[index];
    FAIL_ZERO_RETURN(errctx, actor, AKERR_NULLPOINTER, "enemy %d is already gone", index);
    enemy = &galaga_enemies[index];

    galaga_game.score += KILL_SCORE[enemy->kind];
    galaga_game.kills[enemy->kind] += 1;
    PASS(errctx, galaga_boom_spawn(actor->x + 20.0f, actor->y + 20.0f));
    PASS(errctx, akgl_heap_release_actor(actor));
    galaga_enemy_actors[index] = NULL;
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Move a player shot and test it against every live enemy.
 *
 * The classic O(shots x enemies) sweep: at most 2 x 40 rectangle tests a
 * frame, which is noise. A hit costs the enemy a point of hp; the boss's
 * second point is the script's business to survive, not this file's.
 */
static akerr_ErrorContext *player_shot_update(akgl_Actor *obj)
{
    SDL_FRect mine;
    SDL_FRect theirs;
    bool hit = false;
    int i = 0;
    PREPARE_ERROR(errctx);

    FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "obj");
    obj->y -= 900.0f * galaga_game.dt;
    if ( obj->y < -60.0f ) {
	galaga_game.player_shots_live -= 1;
	PASS(errctx, akgl_heap_release_actor(obj));
	SUCCEED_RETURN(errctx);
    }

    shot_box(obj, &mine);
    for ( i = 0; i < GALAGA_MAX_ENEMIES; i++ ) {
	if ( galaga_enemy_actors[i] == NULL ) {
	    continue;
	}
	enemy_box(galaga_enemy_actors[i], &theirs);
	PASS(errctx, akgl_collide_rectangles(&mine, &theirs, &hit));
	if ( !hit ) {
	    continue;
	}
	galaga_enemies[i].hp -= 1;
	if ( galaga_enemies[i].hp <= 0 ) {
	    PASS(errctx, kill_enemy(i));
	}
	galaga_game.player_shots_live -= 1;
	PASS(errctx, akgl_heap_release_actor(obj));
	SUCCEED_RETURN(errctx);
    }
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext *player_fire(akgl_Actor *player)
{
    akgl_Actor *shot = NULL;
    char name[32];
    int count = 0;
    PREPARE_ERROR(errctx);

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

    PSHOT_SERIAL += 1;
    PASS(errctx, aksl_snprintf(&count, name, sizeof(name), "pshot%u", PSHOT_SERIAL));
    PASS(errctx, akgl_heap_next_actor(&shot));
    PASS(errctx, akgl_actor_initialize(shot, name));
    PASS(errctx, akgl_actor_set_character(shot, "galaga_playershot"));
    /* AFTER initialize: it resets all seven hooks. */
    shot->updatefunc = &player_shot_update;
    shot->movement_controls_face = false;
    shot->state = AKGL_ACTOR_STATE_ALIVE;
    shot->visible = true;
    shot->x = player->x + 45.0f;
    shot->y = player->y - 44.0f;

    galaga_game.player_shots_live += 1;
    galaga_game.fire_cooldown = 0.22f;
    SUCCEED_RETURN(errctx);
}

/* ----------------------------------------------------------- player hit --- */

static akerr_ErrorContext *player_hit(akgl_Actor *player)
{
    PREPARE_ERROR(errctx);

    FAIL_ZERO_RETURN(errctx, player, AKERR_NULLPOINTER, "player");
    galaga_game.lives -= 1;
    galaga_game.respawn_timer = 2.0f;
    PASS(errctx, galaga_boom_spawn(player->x + 25.0f, player->y + 10.0f));
    player->x = (float)GALAGA_VIEW_WIDTH / 2.0f - 50.0f;
    SUCCEED_RETURN(errctx);
}

/**
 * @brief The player's own update hook: motion, fire, and what can kill it.
 */
static akerr_ErrorContext *player_update(akgl_Actor *obj)
{
    SDL_FRect mine;
    SDL_FRect theirs;
    bool hit = false;
    float dx = 0.0f;
    int i = 0;
    PREPARE_ERROR(errctx);

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

    dx = 0.0f;
    if ( galaga_game.moveleft ) {
	dx -= GALAGA_PLAYER_SPEED;
    }
    if ( galaga_game.moveright ) {
	dx += GALAGA_PLAYER_SPEED;
    }
    obj->x += dx * galaga_game.dt;
    if ( obj->x < GALAGA_PLAYER_MARGIN ) {
	obj->x = GALAGA_PLAYER_MARGIN;
    }
    if ( obj->x > (float)GALAGA_VIEW_WIDTH - GALAGA_PLAYER_MARGIN - 99.0f ) {
	obj->x = (float)GALAGA_VIEW_WIDTH - GALAGA_PLAYER_MARGIN - 99.0f;
    }

    galaga_game.fire_cooldown -= galaga_game.dt;
    if ( galaga_game.firing
	 && galaga_game.fire_cooldown <= 0.0f
	 && galaga_game.player_shots_live < GALAGA_MAX_PLAYER_SHOTS
	 && galaga_game.screen == GALAGA_SCREEN_PLAY ) {
	PASS(errctx, player_fire(obj));
    }

    /*
     * Respawn grace: two seconds of blinking invulnerability. The blink is
     * the `visible` flag, which is deliberate hiding -- the actor still
     * updates, it just is not drawn on the off frames.
     */
    if ( galaga_game.respawn_timer > 0.0f ) {
	galaga_game.respawn_timer -= galaga_game.dt;
	obj->visible = ((galaga_game.frame / 6) % 2) == 0;
	SUCCEED_RETURN(errctx);
    }
    obj->visible = true;

    player_box(obj, &mine);

    /* Enemy shots. */
    for ( i = 0; i < AKGL_MAX_HEAP_ACTOR; i++ ) {
	if ( akgl_heap_actors[i].refcount == 0 ) {
	    continue;
	}
	if ( strncmp(akgl_heap_actors[i].name, "eshot", 5) != 0 ) {
	    continue;
	}
	shot_box(&akgl_heap_actors[i], &theirs);
	PASS(errctx, akgl_collide_rectangles(&mine, &theirs, &hit));
	if ( hit ) {
	    galaga_game.enemy_shots_live -= 1;
	    PASS(errctx, akgl_heap_release_actor(&akgl_heap_actors[i]));
	    PASS(errctx, player_hit(obj));
	    SUCCEED_RETURN(errctx);
	}
    }

    /* Diving enemies. The formation never reaches this low, so testing all
     * forty is the same answer as testing the divers, without a state read. */
    for ( i = 0; i < GALAGA_MAX_ENEMIES; i++ ) {
	if ( galaga_enemy_actors[i] == NULL ) {
	    continue;
	}
	enemy_box(galaga_enemy_actors[i], &theirs);
	PASS(errctx, akgl_collide_rectangles(&mine, &theirs, &hit));
	if ( hit ) {
	    PASS(errctx, kill_enemy(i));
	    PASS(errctx, player_hit(obj));
	    SUCCEED_RETURN(errctx);
	}
    }
    SUCCEED_RETURN(errctx);
}

/* -------------------------------------------------------------- controls --- */

static akerr_ErrorContext *left_on(akgl_Actor *obj, SDL_Event *event)
{
    PREPARE_ERROR(errctx);
    (void)obj; (void)event;
    galaga_game.moveleft = true;
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext *left_off(akgl_Actor *obj, SDL_Event *event)
{
    PREPARE_ERROR(errctx);
    (void)obj; (void)event;
    galaga_game.moveleft = false;
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext *right_on(akgl_Actor *obj, SDL_Event *event)
{
    PREPARE_ERROR(errctx);
    (void)obj; (void)event;
    galaga_game.moveright = true;
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext *right_off(akgl_Actor *obj, SDL_Event *event)
{
    PREPARE_ERROR(errctx);
    (void)obj; (void)event;
    galaga_game.moveright = false;
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext *fire_on(akgl_Actor *obj, SDL_Event *event)
{
    PREPARE_ERROR(errctx);
    (void)obj; (void)event;
    galaga_game.firing = true;
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext *fire_off(akgl_Actor *obj, SDL_Event *event)
{
    PREPARE_ERROR(errctx);
    (void)obj; (void)event;
    galaga_game.firing = false;
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *galaga_player_controls(void)
{
    akgl_Control control;
    PREPARE_ERROR(errctx);

    memset(&control, 0, sizeof(control));
    control.event_on = SDL_EVENT_KEY_DOWN;
    control.event_off = SDL_EVENT_KEY_UP;

    control.key = SDLK_LEFT;
    control.handler_on = &left_on;
    control.handler_off = &left_off;
    PASS(errctx, akgl_controller_pushmap(0, &control));

    control.key = SDLK_RIGHT;
    control.handler_on = &right_on;
    control.handler_off = &right_off;
    PASS(errctx, akgl_controller_pushmap(0, &control));

    control.key = SDLK_SPACE;
    control.handler_on = &fire_on;
    control.handler_off = &fire_off;
    PASS(errctx, akgl_controller_pushmap(0, &control));

    akgl_controlmaps[0].target = galaga_game.player;
    SUCCEED_RETURN(errctx);
}

/* ----------------------------------------------------------------- spawn --- */

akerr_ErrorContext *galaga_player_spawn(void)
{
    akgl_Actor *player = NULL;
    PREPARE_ERROR(errctx);

    PASS(errctx, akgl_heap_next_actor(&player));
    PASS(errctx, akgl_actor_initialize(player, "player"));
    PASS(errctx, akgl_actor_set_character(player, "galaga_player"));
    /* AFTER initialize: it resets all seven hooks. */
    player->updatefunc = &player_update;
    player->movement_controls_face = false;
    player->state = AKGL_ACTOR_STATE_ALIVE;
    player->visible = true;
    player->x = (float)GALAGA_VIEW_WIDTH / 2.0f - 50.0f;
    player->y = GALAGA_PLAYER_Y;

    galaga_game.player = player;
    SUCCEED_RETURN(errctx);
}

/* -------------------------------------------------------------- autoplay --- */

/**
 * @brief Send one synthetic key event through the controller.
 *
 * Through akgl_controller_handle_event(), never the handlers directly: the
 * point of autoplay is to exercise the same path a keyboard does.
 */
static akerr_ErrorContext *synth_key(SDL_Keycode key, bool down)
{
    SDL_Event event;
    PREPARE_ERROR(errctx);

    memset(&event, 0, sizeof(event));
    event.type = (down ? SDL_EVENT_KEY_DOWN : SDL_EVENT_KEY_UP);
    event.key.key = key;
    PASS(errctx, akgl_controller_handle_event((void *)&akgl_game.state, &event));
    SUCCEED_RETURN(errctx);
}

/**
 * @brief The scripted pilot for headless runs: hold fire, sweep the floor.
 */
akerr_ErrorContext *galaga_player_autoplay(int frame)
{
    int phase = 0;
    PREPARE_ERROR(errctx);

    /* Hold fire until the wave has mostly assembled: shooting the entry
     * stream point-blank empties the formation before it exists, which makes
     * both the game and its figure worse. */
    if ( frame == 300 ) {
	PASS(errctx, synth_key(SDLK_SPACE, true));
    }
    phase = frame % 240;
    if ( phase == 30 ) {
	PASS(errctx, synth_key(SDLK_LEFT, true));
    } else if ( phase == 90 ) {
	PASS(errctx, synth_key(SDLK_LEFT, false));
	PASS(errctx, synth_key(SDLK_RIGHT, true));
    } else if ( phase == 210 ) {
	PASS(errctx, synth_key(SDLK_RIGHT, false));
    }
    SUCCEED_RETURN(errctx);
}
