/**
 * @file collision_shape.c
 * @brief Building collision shapes, and the one invariant that keeps 2D flat.
 *
 * Nothing here touches the narrowphase. A shape is data, and this file is the
 * only place that decides what a well-formed one looks like -- which is why the
 * extrusion rule lives in the setters rather than at the point of use, where
 * every caller would have to remember it.
 */

#include <string.h>

#include <akgl/collision.h>
#include <akgl/error.h>

/**
 * @brief Fill in the depth, unless the caller asked for a specific one.
 *
 * Called last by every setter, once `hx` and `hy` are known. See collision.h for
 * why the ratio is what it is; the short version is that a thin extrusion makes
 * z the cheapest axis to separate on, and an actor pushed along z goes nowhere a
 * player can see while falling through the floor.
 */
static akerr_ErrorContext *shape_extrude(akgl_CollisionShape *dest, float32_t depth)
{
    float32_t largest = 0.0f;

    PREPARE_ERROR(errctx);
    FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL shape reference");
    FAIL_NONZERO_RETURN(errctx, (depth < 0.0f), AKERR_VALUE,
			"Shape depth %f is negative", depth);

    if ( depth > 0.0f ) {
	dest->hz = depth;
	SUCCEED_RETURN(errctx);
    }

    largest = dest->hx;
    if ( dest->hy > largest ) {
	largest = dest->hy;
    }
    dest->hz = AKGL_COLLISION_DEPTH_RATIO * largest;
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Zero a shape and give it the defaults every setter shares.
 *
 * The masks are the interesting part. An actor that has been given a shape and
 * nothing else collides with map geometry and with no other actor, because
 * "everything with a hitbox shoves everything else" is a surprising default for
 * a town full of scenery and a painful one to discover after the fact. Opting
 * in to actor-versus-actor is one added bit; opting out of it would have been a
 * hunt through every NPC a game spawns.
 */
static akerr_ErrorContext *shape_defaults(akgl_CollisionShape *dest, uint8_t kind)
{
    PREPARE_ERROR(errctx);
    FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL shape reference");

    memset(dest, 0x00, sizeof(akgl_CollisionShape));
    dest->kind = kind;
    dest->layermask = AKGL_COLLISION_LAYER_ACTOR;
    dest->collidemask = AKGL_COLLISION_LAYER_STATIC;
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_collision_shape_box(akgl_CollisionShape *dest, SDL_FRect *body, float32_t depth)
{
    PREPARE_ERROR(errctx);
    FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL shape reference");
    FAIL_ZERO_RETURN(errctx, body, AKERR_NULLPOINTER, "NULL rectangle reference");
    FAIL_NONZERO_RETURN(errctx, (body->w <= 0.0f), AKERR_VALUE,
			"Box width %f is not positive", body->w);
    FAIL_NONZERO_RETURN(errctx, (body->h <= 0.0f), AKERR_VALUE,
			"Box height %f is not positive", body->h);

    PASS(errctx, shape_defaults(dest, AKGL_COLLISION_SHAPE_BOX));

    // Top-left-and-size in, centre-and-half-extent out. Done once here so that
    // nothing on the frame path has to do it per test.
    dest->hx = body->w / 2.0f;
    dest->hy = body->h / 2.0f;
    dest->ox = body->x + dest->hx;
    dest->oy = body->y + dest->hy;

    PASS(errctx, shape_extrude(dest, depth));
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_collision_shape_circle(akgl_CollisionShape *dest, float32_t ox, float32_t oy, float32_t radius, float32_t depth)
{
    PREPARE_ERROR(errctx);
    FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL shape reference");
    FAIL_NONZERO_RETURN(errctx, (radius <= 0.0f), AKERR_VALUE,
			"Circle radius %f is not positive", radius);

    PASS(errctx, shape_defaults(dest, AKGL_COLLISION_SHAPE_CIRCLE));
    dest->ox = ox;
    dest->oy = oy;
    dest->hx = radius;
    dest->hy = radius;
    PASS(errctx, shape_extrude(dest, depth));
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_collision_shape_capsule(akgl_CollisionShape *dest, SDL_FRect *body, uint8_t axis, float32_t depth)
{
    PREPARE_ERROR(errctx);
    FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL shape reference");
    FAIL_ZERO_RETURN(errctx, body, AKERR_NULLPOINTER, "NULL rectangle reference");
    FAIL_NONZERO_RETURN(
	errctx,
	((axis != AKGL_COLLISION_SHAPE_CAPSULE_X) && (axis != AKGL_COLLISION_SHAPE_CAPSULE_Y)),
	AKERR_VALUE,
	"Capsule axis %u is neither AKGL_COLLISION_SHAPE_CAPSULE_X nor _Y",
	(unsigned int)axis
	);
    FAIL_NONZERO_RETURN(errctx, (body->w <= 0.0f), AKERR_VALUE,
			"Capsule width %f is not positive", body->w);
    FAIL_NONZERO_RETURN(errctx, (body->h <= 0.0f), AKERR_VALUE,
			"Capsule height %f is not positive", body->h);

    /*
     * The capped axis has to be the longer one. A capsule whose caps are wider
     * than the body is long is a circle with extra steps, and accepting it would
     * mean the shape a caller described and the shape the narrowphase built were
     * different -- silently, and only for some sizes.
     */
    if ( axis == AKGL_COLLISION_SHAPE_CAPSULE_X ) {
	FAIL_NONZERO_RETURN(errctx, (body->w <= body->h), AKERR_VALUE,
			    "An x-axis capsule is %fx%f; the capped axis must be the longer one",
			    body->w, body->h);
    } else {
	FAIL_NONZERO_RETURN(errctx, (body->h <= body->w), AKERR_VALUE,
			    "A y-axis capsule is %fx%f; the capped axis must be the longer one",
			    body->w, body->h);
    }

    PASS(errctx, shape_defaults(dest, axis));
    dest->hx = body->w / 2.0f;
    dest->hy = body->h / 2.0f;
    dest->ox = body->x + dest->hx;
    dest->oy = body->y + dest->hy;
    PASS(errctx, shape_extrude(dest, depth));
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_collision_shape_bounds(akgl_CollisionShape *shape, float32_t x, float32_t y, SDL_FRect *dest)
{
    PREPARE_ERROR(errctx);
    FAIL_ZERO_RETURN(errctx, shape, AKERR_NULLPOINTER, "NULL shape reference");
    FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL rectangle reference");

    dest->x = x + shape->ox - shape->hx;
    dest->y = y + shape->oy - shape->hy;
    dest->w = shape->hx * 2.0f;
    dest->h = shape->hy * 2.0f;
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_collision_shape_interacts(akgl_CollisionShape *self, akgl_CollisionShape *other, bool *dest)
{
    PREPARE_ERROR(errctx);
    FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "NULL shape reference");
    FAIL_ZERO_RETURN(errctx, other, AKERR_NULLPOINTER, "NULL other shape reference");
    FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL result reference");

    *dest = false;
    if ( (self->kind == AKGL_COLLISION_SHAPE_NONE) ||
	 (other->kind == AKGL_COLLISION_SHAPE_NONE) ) {
	SUCCEED_RETURN(errctx);
    }
    if ( ((self->flags & AKGL_COLLISION_FLAG_DISABLED) != 0) ||
	 ((other->flags & AKGL_COLLISION_FLAG_DISABLED) != 0) ) {
	SUCCEED_RETURN(errctx);
    }

    // One direction only. The mirrored question is asked separately, and may
    // legitimately have a different answer.
    *dest = ((other->layermask & self->collidemask) != 0);
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_collision_proxy_initialize(akgl_CollisionProxy *obj, struct akgl_Actor *owner, akgl_CollisionShape *shape, float32_t x, float32_t y, float32_t z)
{
    PREPARE_ERROR(errctx);
    FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL proxy reference");
    FAIL_ZERO_RETURN(errctx, shape, AKERR_NULLPOINTER, "NULL shape reference");

    memset(obj, 0x00, sizeof(akgl_CollisionProxy));
    obj->owner = owner;
    // -1 rather than 0: 0 is a valid cell-entry index, so a zeroed proxy would
    // read as already being in the grid at entry zero.
    obj->first = -1;
    obj->cx1 = -1;
    obj->cy1 = -1;

    /*
     * The reference is taken here and not in akgl_heap_next_collision_proxy,
     * matching four of the five pools that came before. Until this runs the slot
     * still reads as free -- which is also the property that keeps an acquire
     * abandoned before this point from leaking the slot.
     */
    obj->refcount = 1;

    PASS(errctx, akgl_collision_proxy_sync(obj, shape, x, y, z));
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_collision_proxy_sync(akgl_CollisionProxy *obj, akgl_CollisionShape *shape, float32_t x, float32_t y, float32_t z)
{
    PREPARE_ERROR(errctx);
    FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL proxy reference");
    FAIL_ZERO_RETURN(errctx, shape, AKERR_NULLPOINTER, "NULL shape reference");

    // A copy, not a pointer. See the note on akgl_CollisionProxy.
    obj->shape = *shape;
    obj->x = x;
    obj->y = y;
    obj->z = z;
    PASS(errctx, akgl_collision_shape_bounds(&obj->shape, x, y, &obj->bounds));
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_collision_cell_initialize(akgl_CollisionCell *obj, int16_t proxyidx, int32_t cell)
{
    PREPARE_ERROR(errctx);
    FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL collision cell reference");

    memset(obj, 0x00, sizeof(akgl_CollisionCell));
    obj->refcount = 1;
    obj->proxy = proxyidx;
    obj->cell = cell;
    obj->next = -1;
    obj->prev = -1;
    obj->ownernext = -1;
    SUCCEED_RETURN(errctx);
}
