/**
 * @file actors.c
 * @brief Everything level1.tmj places that is not the player.
 *
 * None of these are created here. The map's object layer creates all seven
 * actors, registers them under the names Tiled gave them, and binds each one to
 * the character its `character` property names -- so this file's whole job is to
 * find them again by name and give them behaviour.
 *
 * Two of the three behaviours raise #AKGL_ERR_LOGICINTERRUPT, which is the one
 * status in libakgl that is not a failure. Raised from a `movementlogicfunc` it
 * means "skip the rest of this tick for me": no gravity, no drag, no move for
 * that actor this step, and akgl_physics_simulate carries on to the next one.
 * That is exactly what an actor that has just placed itself wants.
 */

#include <math.h>

#include <akstdlib.h>

#include <akgl/heap.h>
#include <akgl/registry.h>

#include "sidescroller.h"

/** @brief The blob's collision box, offset into its 32x32 frame. */
static SDL_FRect ss_blob_body = { .x = 8.0f, .y = 0.0f, .w = 16.0f, .h = 32.0f };

/**
 * @brief Per-actor data for everything in this file, one slot per actor.
 *
 * A fixed table rather than an allocation, for the same reason the library has
 * pools rather than a `malloc`: the level places a known number of things, and a
 * game that cannot run out of memory at runtime is one fewer failure mode.
 */
static ss_ActorData ss_hazard_data[SS_HAZARD_COUNT];

/**
 * @brief Look an actor up by the name its Tiled object carried.
 *
 * A miss here means the map and the code disagree about what the level
 * contains, which is worth failing on rather than working around.
 */
static akerr_ErrorContext *find_actor(char *name, akgl_Actor **dest)
{
    PREPARE_ERROR(errctx);
    FAIL_ZERO_RETURN(errctx, name, AKERR_NULLPOINTER, "name");
    FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "dest");

    *dest = SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, name, NULL);
    FAIL_ZERO_RETURN(errctx, *dest, AKERR_KEY, "The map placed no actor called %s", name);
    SUCCEED_RETURN(errctx);
}

/**
 * @brief A `movementlogicfunc` for something that does not move at all.
 *
 * The coins need this. Their character declares no speed and no acceleration, so
 * they cannot thrust -- but gravity is not thrust. `akgl_physics_arcade_gravity`
 * accumulates into `ey` for every actor it is handed, so a coin left to the
 * default logic falls out of the level along with everything else.
 *
 * Raising #AKGL_ERR_LOGICINTERRUPT is how an actor opts out of the rest of its
 * step. It is not an error and nothing logs it.
 */
static akerr_ErrorContext *ss_static_movement(akgl_Actor *obj, float32_t dt)
{
    PREPARE_ERROR(errctx);
    (void)dt;
    FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "obj");
    FAIL_RETURN(errctx, AKGL_ERR_LOGICINTERRUPT, "%s does not simulate", (char *)obj->name);
}

/**
 * @brief The blob: walk, and turn around at a wall or a ledge.
 *
 * This one stays inside the physics step -- it walks on the ground under the
 * map's gravity like the player does, and the library resolves it the same way.
 * All this hook decides is which way to face next.
 */
static akerr_ErrorContext *ss_blob_movement(akgl_Actor *obj, float32_t dt)
{
    ss_ActorData *data = NULL;
    SDL_FRect ahead;
    float32_t probe_x = 0.0f;
    float32_t probe_y = 0.0f;
    float32_t step = 0.0f;
    bool floor_ahead = false;
    bool wall_ahead = false;
    bool grounded = false;
    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;

    AKGL_BITMASK_DEL(obj->state, (AKGL_ACTOR_STATE_FACE_ALL | AKGL_ACTOR_STATE_MOVING_ALL));
    if ( data->facing < 0.0f ) {
	AKGL_BITMASK_ADD(obj->state, (AKGL_ACTOR_STATE_FACE_LEFT | AKGL_ACTOR_STATE_MOVING_LEFT));
    } else {
	AKGL_BITMASK_ADD(obj->state, (AKGL_ACTOR_STATE_FACE_RIGHT | AKGL_ACTOR_STATE_MOVING_RIGHT));
    }
    /* Signs the acceleration from the movement bits just set. */
    PASS(errctx, akgl_actor_logic_movement(obj, dt));

    step = 1.0f;
    if ( data->facing < 0.0f ) {
	step = -1.0f;
    }

    /* Three probes, all taken from where the last step left the blob. The blob
     * turns on a wall or a ledge, and a contact from the resolver answers
     * neither question: it says something pushed back, not which side of the
     * blob it was on, and it says nothing at all about a floor that is missing. */
    PASS(errctx, akgl_collision_shape_bounds(&obj->shape, (obj->x + step), obj->y, &ahead));
    PASS(errctx, akgl_collision_box_blocked(&ss_collision, &ahead, AKGL_COLLISION_LAYER_STATIC, &wall_ahead));
    PASS(errctx, ss_grounded(&obj->shape, obj->x, obj->y, &grounded));

    /* One pixel past the leading edge of the box and one pixel below its feet:
     * no floor there means the next step walks off. */
    probe_y = obj->y + ss_blob_body.y + ss_blob_body.h + 1.0f;
    if ( data->facing < 0.0f ) {
	probe_x = obj->x + ss_blob_body.x - 1.0f;
    } else {
	probe_x = obj->x + ss_blob_body.x + ss_blob_body.w + 1.0f;
    }
    PASS(errctx, akgl_collision_solid_at(&ss_collision, probe_x, probe_y, &floor_ahead));

    if ( (wall_ahead == true) || ((grounded == true) && (floor_ahead == false)) ) {
	data->facing = -data->facing;
	/* The library's response removes the component of `tx` going into a
	 * wall, but nothing removes it on the ledge path -- and without this the
	 * blob keeps its old momentum through the turn. */
	obj->tx = 0.0f;
    }
    SUCCEED_RETURN(errctx);
}

/**
 * @brief The moth: a figure-eight around where the map put it.
 *
 * It flies, so it wants no gravity -- and the map's physics has gravity, because
 * the player needs it. Rather than fighting the backend the moth writes its own
 * position and then opts out of the rest of the step, which is the second thing
 * #AKGL_ERR_LOGICINTERRUPT is for.
 */
static akerr_ErrorContext *ss_moth_movement(akgl_Actor *obj, float32_t dt)
{
    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;

    data->phase += dt;
    obj->x = data->home_x + (sinf(data->phase) * 64.0f);
    obj->y = data->home_y + (sinf(data->phase * 2.0f) * 24.0f);

    AKGL_BITMASK_DEL(obj->state, AKGL_ACTOR_STATE_FACE_ALL);
    if ( cosf(data->phase) < 0.0f ) {
	AKGL_BITMASK_ADD(obj->state, AKGL_ACTOR_STATE_FACE_LEFT);
    } else {
	AKGL_BITMASK_ADD(obj->state, AKGL_ACTOR_STATE_FACE_RIGHT);
    }

    FAIL_RETURN(errctx, AKGL_ERR_LOGICINTERRUPT, "%s flies itself", (char *)obj->name);
}

akerr_ErrorContext *ss_actors_bind(void)
{
    char name[32];
    int count = 0;
    int i = 0;
    PREPARE_ERROR(errctx);

    for ( i = 0; i < SS_COIN_COUNT; i++ ) {
	PASS(errctx, aksl_snprintf(&count, (char *)&name, sizeof(name), "coin%d", (i + 1)));
	PASS(errctx, find_actor((char *)&name, &ss_game.coins[i]));
	ss_game.coins[i]->movementlogicfunc = &ss_static_movement;
    }

    PASS(errctx, find_actor("blob1", &ss_game.hazards[0]));
    PASS(errctx, akgl_collision_shape_box(&ss_game.hazards[0]->shape, &ss_blob_body, 0.0f));
    ss_game.hazards[0]->shape_override = true;
    PASS(errctx, akgl_collision_settle(&ss_collision, &ss_game.hazards[0]->shape,
				       &ss_game.hazards[0]->x, &ss_game.hazards[0]->y, 0));
    ss_hazard_data[0].home_x = ss_game.hazards[0]->x;
    ss_hazard_data[0].home_y = ss_game.hazards[0]->y;
    ss_hazard_data[0].facing = -1.0f;
    ss_game.hazards[0]->actorData = (void *)&ss_hazard_data[0];
    ss_game.hazards[0]->movementlogicfunc = &ss_blob_movement;

    PASS(errctx, find_actor("moth1", &ss_game.hazards[1]));
    ss_hazard_data[1].home_x = ss_game.hazards[1]->x;
    ss_hazard_data[1].home_y = ss_game.hazards[1]->y;
    ss_hazard_data[1].facing = 1.0f;
    ss_game.hazards[1]->actorData = (void *)&ss_hazard_data[1];
    ss_game.hazards[1]->movementlogicfunc = &ss_moth_movement;

    SUCCEED_RETURN(errctx);
}
