/**
 * @file player.c
 * @brief The player's two hooks and its control bindings.
 *
 * Two of the six behaviour hooks on `akgl_Actor` are replaced here, and the
 * split between them is the frame's own order. `akgl_game_update` calls every
 * actor's `updatefunc`, then steps the physics, then draws -- so per-frame game
 * logic that is not movement (picking a coin up, falling in the pit) goes in
 * `updatefunc`, and anything that has to happen inside the physics step goes in
 * `movementlogicfunc`, which is the only hook the step calls.
 *
 * Two of the library's documented gaps are worked around here rather than
 * papered over. Both are issues #29 through #32.
 */

#include <math.h>

#include <akstdlib.h>

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

#include "sidescroller.h"

/** @brief The player's collision box, as an offset into its 32x32 sprite frame. */
static SDL_FRect ss_player_body = {
    .x = SS_PLAYER_BOX_X,
    .y = SS_PLAYER_BOX_Y,
    .w = SS_PLAYER_BOX_W,
    .h = SS_PLAYER_BOX_H
};

/** @brief Where the map put the player, so a death can put it back. */
static ss_ActorData ss_player_data;

/**
 * @brief The rectangle an actor is tested against, given a 32x32 sprite frame.
 *
 * Smaller than the frame on purpose. Sprite art does not reach the edges, and a
 * hazard box the full size of the frame kills a player who is visibly nowhere
 * near it.
 */
static akerr_ErrorContext *hitbox(akgl_Actor *obj, float32_t inset, SDL_FRect *dest)
{
    PREPARE_ERROR(errctx);
    FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "obj");
    FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "dest");

    dest->x = obj->x + inset;
    dest->y = obj->y + inset;
    dest->w = 32.0f - (inset * 2.0f);
    dest->h = 32.0f - (inset * 2.0f);
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Put the player back where the map placed it, at rest.
 *
 * Everything the simulation carries between steps has to be cleared, not just
 * the position: `ey` is where gravity has been accumulating, and a player who
 * respawns still holding a full-speed fall lands dead again immediately.
 */
static akerr_ErrorContext *respawn(akgl_Actor *obj)
{
    ss_ActorData *data = NULL;
    PREPARE_ERROR(errctx);

    FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "obj");
    FAIL_ZERO_RETURN(errctx, obj->actorData, AKERR_NULLPOINTER, "obj->actorData");

    data = (ss_ActorData *)obj->actorData;
    obj->x = data->home_x;
    obj->y = data->home_y;
    obj->ex = 0.0f;
    obj->ey = 0.0f;
    obj->tx = 0.0f;
    obj->ty = 0.0f;
    obj->vx = 0.0f;
    obj->vy = 0.0f;
    ss_game.deaths += 1;
    SDL_Log("Player died (%d) and respawned at %f, %f", ss_game.deaths, obj->x, obj->y);
    SUCCEED_RETURN(errctx);
}

/**
 * @brief The player's `movementlogicfunc`: friction and the jump.
 *
 * Called by `akgl_physics_simulate` once per actor per step, *before* gravity,
 * drag, the velocity recomputation and `move`. Terrain is no longer this
 * function's problem -- the library resolves it after the move, which is the
 * only place it can be resolved without predicting the arithmetic above.
 */
static akerr_ErrorContext *ss_player_movement(akgl_Actor *obj, float32_t dt)
{
    float32_t friction = 0.0f;
    PREPARE_ERROR(errctx);

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

    /* The default logic still has to run: it is what copies the character's
     * speeds onto the actor and signs its acceleration by the movement bits. */
    PASS(errctx, akgl_actor_logic_movement(obj, dt));

    /*
     * Workaround 1: there is no friction anywhere in the arcade backend.
     *
     * akgl_actor_cmhf_left_off zeroes `tx` outright, so releasing a direction
     * stops the actor dead inside one frame -- correct for a top-down Zelda,
     * wrong for a sidescroller. This game binds its own `_off` handlers that
     * leave `tx` alone (see below) and decays it here instead, faster on the
     * ground than in the air. Below a pixel per second it is snapped to zero,
     * because an exponential decay never actually arrives.
     */
    /* Where the last step left the player, one pixel down. This runs before the
     * step it is deciding about, so it is one frame stale by construction --
     * which is one frame of coyote time, and not a thing a player can feel. */
    PASS(errctx, ss_grounded(&obj->shape, obj->x, obj->y, &ss_game.grounded));

    if ( AKGL_BITMASK_HASNOT(obj->state, AKGL_ACTOR_STATE_MOVING_LEFT) &&
	 AKGL_BITMASK_HASNOT(obj->state, AKGL_ACTOR_STATE_MOVING_RIGHT) ) {
	friction = SS_FRICTION_AIR;
	if ( ss_game.grounded == true ) {
	    friction = SS_FRICTION_GROUND;
	}
	obj->tx -= obj->tx * friction * dt;
	if ( fabsf(obj->tx) < 1.0f ) {
	    obj->tx = 0.0f;
	}
    }

    /*
     * A jump is an impulse straight into the environmental term, because `ey` is
     * the axis gravity accumulates on and the two have to cancel for the arc to
     * come back down. Thrust would not: `ty` is capped against the character's
     * `speed_y`, which is 0 for this character, so a jump written as thrust is
     * scaled to nothing by the ellipse cap in akgl_physics_simulate.
     *
     * `ss_game.grounded` is the probe above.
     */
    if ( (ss_game.jump_requested == true) && (ss_game.grounded == true) ) {
	obj->ey = -SS_JUMP_SPEED;
    }
    ss_game.jump_requested = false;

    /*
     * The character maps MOVING_UP to the jump sprites, so the state word says
     * "in the air" whether the actor is rising or falling. Nothing else reads
     * the bit here: `speed_y` and `acceleration_y` are both 0 in
     * character_ss_player.json, so the thrust it would authorise is capped to
     * nothing.
     */
    if ( ss_game.grounded == true ) {
	AKGL_BITMASK_DEL(obj->state, AKGL_ACTOR_STATE_MOVING_UP);
    } else {
	AKGL_BITMASK_ADD(obj->state, AKGL_ACTOR_STATE_MOVING_UP);
    }
    SUCCEED_RETURN(errctx);
}

/**
 * @brief The player's `updatefunc`: the animation, then what it is touching.
 *
 * Runs in `akgl_game_update`'s actor sweep, before the physics step and before
 * anything is drawn.
 */
static akerr_ErrorContext *ss_player_update(akgl_Actor *obj)
{
    SDL_FRect player;
    SDL_FRect other;
    bool hit = false;
    int i = 0;
    PREPARE_ERROR(errctx);

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

    /* Facing, then the animation frame. Replacing a hook does not mean
     * reimplementing it. */
    PASS(errctx, akgl_actor_update(obj));

    /* Off the bottom of the world. Nothing in the library stops an actor
     * leaving the map -- akgl_physics_arcade_move does not clamp -- so falling
     * out of the level is a thing the game has to notice for itself. */
    if ( obj->y > (float32_t)(akgl_gamemap->height * akgl_gamemap->tileheight) ) {
	PASS(errctx, respawn(obj));
	SUCCEED_RETURN(errctx);
    }

    PASS(errctx, hitbox(obj, 8.0f, &player));

    for ( i = 0; i < SS_COIN_COUNT; i++ ) {
	if ( ss_game.coins[i] == NULL ) {
	    continue;
	}
	PASS(errctx, hitbox(ss_game.coins[i], 8.0f, &other));
	PASS(errctx, akgl_collide_rectangles(&player, &other, &hit));
	if ( hit == true ) {
	    /*
	     * Giving the pool slot back is what unregisters the actor and stops
	     * it being drawn; there is no "despawn" call. Releasing another
	     * actor from inside this sweep is safe -- akgl_game_update re-reads
	     * `refcount` at the top of every iteration and skips a slot that has
	     * gone free.
	     */
	    PASS(errctx, akgl_heap_release_actor(ss_game.coins[i]));
	    ss_game.coins[i] = NULL;
	    ss_game.coins_taken += 1;
	    SDL_Log("Collected coin %d of %d", ss_game.coins_taken, SS_COIN_COUNT);
	}
    }

    for ( i = 0; i < SS_HAZARD_COUNT; i++ ) {
	if ( ss_game.hazards[i] == NULL ) {
	    continue;
	}
	PASS(errctx, hitbox(ss_game.hazards[i], 6.0f, &other));
	PASS(errctx, akgl_collide_rectangles(&player, &other, &hit));
	if ( hit == true ) {
	    PASS(errctx, respawn(obj));
	    SUCCEED_RETURN(errctx);
	}
    }
    SUCCEED_RETURN(errctx);
}

/*
 * Workaround 2, the release half.
 *
 * akgl_actor_cmhf_left_off and _right_off clear the movement bit, zero `ax`
 * *and* zero `tx`. That last one is the "stops dead" behaviour; these two do
 * everything else it does and leave `tx` for ss_player_movement to decay.
 */
static akerr_ErrorContext *ss_control_left_off(akgl_Actor *obj, SDL_Event *event)
{
    PREPARE_ERROR(errctx);
    FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "obj");
    FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "event");
    obj->ax = 0.0f;
    AKGL_BITMASK_DEL(obj->state, AKGL_ACTOR_STATE_MOVING_LEFT);
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext *ss_control_right_off(akgl_Actor *obj, SDL_Event *event)
{
    PREPARE_ERROR(errctx);
    FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "obj");
    FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "event");
    obj->ax = 0.0f;
    AKGL_BITMASK_DEL(obj->state, AKGL_ACTOR_STATE_MOVING_RIGHT);
    SUCCEED_RETURN(errctx);
}

/** @brief Ask for a jump. Whether one is allowed is ss_player_movement's call. */
static akerr_ErrorContext *ss_control_jump_on(akgl_Actor *obj, SDL_Event *event)
{
    PREPARE_ERROR(errctx);
    FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "obj");
    FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "event");
    ss_game.jump_requested = true;
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Cut a rising jump short when the button is let go.
 *
 * Variable jump height for four lines, and it works because `ey` is an ordinary
 * field that nothing else owns between steps.
 */
static akerr_ErrorContext *ss_control_jump_off(akgl_Actor *obj, SDL_Event *event)
{
    PREPARE_ERROR(errctx);
    FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "obj");
    FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "event");
    if ( obj->ey < 0.0f ) {
	obj->ey *= 0.4f;
    }
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *ss_player_bind(akgl_Actor *obj)
{
    PREPARE_ERROR(errctx);
    FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "obj");

    ss_game.player = obj;

    /* The library's default response only stops an actor *entering* geometry;
     * one that starts inside it is simply stuck, so lift the spawn point clear
     * of the step it overlaps *before* recording it. */
    PASS(errctx, akgl_collision_shape_box(&obj->shape, &ss_player_body, 0.0f));
    obj->shape_override = true;
    PASS(errctx, akgl_collision_settle(&ss_collision, &obj->shape, &obj->x, &obj->y, 0));
    ss_player_data.home_x = obj->x;
    ss_player_data.home_y = obj->y;
    obj->actorData = (void *)&ss_player_data;

    /*
     * The default `facefunc` clears every facing bit and then sets one from the
     * movement bits -- so an actor that stops moving is left facing nowhere, its
     * state drops to bare ALIVE, and character_ss_player.json has no sprite for
     * that. The actor becomes invisible while standing still.
     *
     * Clearing this is the documented way out: akgl_actor_automatic_face leaves
     * an actor alone entirely when it is clear, and the facing bits stay
     * wherever the control handlers last put them.
     */
    obj->movement_controls_face = false;

    /* Replace the hooks after akgl_actor_initialize, never before -- it
     * overwrites all six. The tilemap loader has already run it here. */
    obj->movementlogicfunc = &ss_player_movement;
    obj->updatefunc = &ss_player_update;
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *ss_player_controls(int controlmapid, char *actorname)
{
    akgl_ControlMap *controlmap = NULL;
    akgl_Control control;
    PREPARE_ERROR(errctx);

    FAIL_ZERO_RETURN(errctx, actorname, AKERR_NULLPOINTER, "actorname");
    /* akgl_controller_pushmap checks the upper bound and not the lower one, so
     * a negative id indexes before the start of akgl_controlmaps. TODO.md,
     * "Defects". */
    FAIL_NONZERO_RETURN(
	errctx,
	((controlmapid < 0) || (controlmapid >= AKGL_MAX_CONTROL_MAPS)),
	AKERR_OUTOFBOUNDS,
	"Control map id %d is outside 0..%d",
	controlmapid,
	(AKGL_MAX_CONTROL_MAPS - 1)
	);
    PASS(errctx, aksl_memset((void *)&control, 0x00, sizeof(akgl_Control)));

    controlmap = &akgl_controlmaps[controlmapid];
    controlmap->target = SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, actorname, NULL);
    FAIL_ZERO_RETURN(
	errctx,
	controlmap->target,
	AKGL_ERR_REGISTRY,
	"Actor %s is not in AKGL_REGISTRY_ACTOR; bind controls after the map is loaded",
	actorname
	);

    /*
     * A control map listens to one keyboard and one gamepad, matched by id, and
     * 0 means "any". That is what a one-player game wants, and it is the only
     * thing that works: SDL_GetKeyboards() reports what is attached, but the id
     * a key event carries is chosen by the video backend, and on X11 it is
     * SDL_GLOBAL_KEYBOARD_ID (0) without XInput2 and the physical device's
     * sourceid with it -- never the SDL_DEFAULT_KEYBOARD_ID of 1 that SDL
     * registers when XInput2 is missing. This function used to bind
     * SDL_GetKeyboards()[0] and the arrow keys did nothing at all.
     *
     * Bind a specific id when two players share a machine on two keyboards.
     */
    controlmap->kbid = 0;
    controlmap->jsid = 0;

    /* ---- keyboard ---- */
    control.event_on = SDL_EVENT_KEY_DOWN;
    control.event_off = SDL_EVENT_KEY_UP;

    control.key = SDLK_LEFT;
    control.handler_on = &akgl_actor_cmhf_left_on;
    control.handler_off = &ss_control_left_off;
    PASS(errctx, akgl_controller_pushmap(controlmapid, &control));

    control.key = SDLK_RIGHT;
    control.handler_on = &akgl_actor_cmhf_right_on;
    control.handler_off = &ss_control_right_off;
    PASS(errctx, akgl_controller_pushmap(controlmapid, &control));

    control.key = SDLK_SPACE;
    control.handler_on = &ss_control_jump_on;
    control.handler_off = &ss_control_jump_off;
    PASS(errctx, akgl_controller_pushmap(controlmapid, &control));

    /* ---- gamepad ---- */
    /* Clear the keycode first: a keyboard event is matched on `key` whatever
     * else the binding carries, and 0 is a keycode like any other. */
    control.key = 0;
    control.event_on = SDL_EVENT_GAMEPAD_BUTTON_DOWN;
    control.event_off = SDL_EVENT_GAMEPAD_BUTTON_UP;

    control.button = SDL_GAMEPAD_BUTTON_DPAD_LEFT;
    control.handler_on = &akgl_actor_cmhf_left_on;
    control.handler_off = &ss_control_left_off;
    PASS(errctx, akgl_controller_pushmap(controlmapid, &control));

    control.button = SDL_GAMEPAD_BUTTON_DPAD_RIGHT;
    control.handler_on = &akgl_actor_cmhf_right_on;
    control.handler_off = &ss_control_right_off;
    PASS(errctx, akgl_controller_pushmap(controlmapid, &control));

    control.button = SDL_GAMEPAD_BUTTON_SOUTH;
    control.handler_on = &ss_control_jump_on;
    control.handler_off = &ss_control_jump_off;
    PASS(errctx, akgl_controller_pushmap(controlmapid, &control));

    SDL_Log("Bound %s to keyboard %d and gamepad %d", actorname, controlmap->kbid, controlmap->jsid);
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *ss_player_autoplay(int frame)
{
    SDL_Event synthetic;
    PREPARE_ERROR(errctx);

    FAIL_ZERO_RETURN(errctx, ss_game.player, AKERR_NULLPOINTER, "ss_game.player");
    PASS(errctx, aksl_memset((void *)&synthetic, 0x00, sizeof(SDL_Event)));
    /*
     * Dispatch through akgl_controller_handle_event rather than calling the
     * handlers directly, so a scripted run exercises the binding table the way a
     * player does. The first version of this called the handlers, which meant
     * the smoke test passed while the control map matched nothing at all and the
     * arrow keys did nothing -- a test that cannot fail is not a test.
     *
     * SS_AUTOPLAY_KBID is deliberately not 0: the map binds keyboard 0 meaning
     * "any", and driving it from a non-zero device id is what proves that.
     */
    if ( frame == 1 ) {
	synthetic.type = SDL_EVENT_KEY_DOWN;
	synthetic.key.which = SS_AUTOPLAY_KBID;
	synthetic.key.key = SDLK_RIGHT;
	PASS(errctx, akgl_controller_handle_event((void *)&akgl_game.state, &synthetic));
	FAIL_ZERO_RETURN(
	    errctx,
	    AKGL_BITMASK_HAS(ss_game.player->state, AKGL_ACTOR_STATE_MOVING_RIGHT),
	    AKGL_ERR_BEHAVIOR,
	    "the right arrow did not reach the player; the control map matched nothing"
	    );
    }

    if ( (frame % SS_AUTOPLAY_JUMP_PERIOD) == 0 ) {
	synthetic.type = SDL_EVENT_KEY_DOWN;
	synthetic.key.which = SS_AUTOPLAY_KBID;
	synthetic.key.key = SDLK_SPACE;
	PASS(errctx, akgl_controller_handle_event((void *)&akgl_game.state, &synthetic));
    } else if ( (frame % SS_AUTOPLAY_JUMP_PERIOD) == SS_AUTOPLAY_JUMP_HOLD ) {
	synthetic.type = SDL_EVENT_KEY_UP;
	synthetic.key.which = SS_AUTOPLAY_KBID;
	synthetic.key.key = SDLK_SPACE;
	PASS(errctx, akgl_controller_handle_event((void *)&akgl_game.state, &synthetic));
    }
    SUCCEED_RETURN(errctx);
}
