/**
 * @file actor.c
 * @brief Implements the actor subsystem.
 */

#include <SDL3/SDL.h>
#include <SDL3_image/SDL_image.h>
#include <string.h>
#include <akerror.h>
#include <akstdlib.h>

#include <akgl/physics.h>
#include <akgl/game.h>
#include <akgl/sprite.h>
#include <akgl/actor.h>
#include <akgl/heap.h>
#include <akgl/registry.h>
#include <akgl/staticstring.h>
#include <akgl/iterator.h>

akerr_ErrorContext *akgl_actor_initialize(akgl_Actor *obj, char *name)
{
    PREPARE_ERROR(errctx);
    FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "akgl_actor_initialize received null actor pointer");
    FAIL_ZERO_RETURN(errctx, name, AKERR_NULLPOINTER, "akgl_actor_initialize received null name string pointer");

    memset(obj, 0x00, sizeof(akgl_Actor));
    // aksl_strncpy always terminates; strncpy at exactly the field width does
    // not, and this field is a registry key. n is one less than the field so an
    // over-long name still truncates rather than being refused, which is the
    // documented contract.
    PASS(errctx, aksl_strncpy((char *)obj->name, sizeof(obj->name), name, sizeof(obj->name) - 1));
    obj->curSpriteReversing = false;
    obj->scale = 1.0;
    obj->movement_controls_face = true;

    obj->updatefunc = &akgl_actor_update;
    obj->renderfunc = &akgl_actor_render;
    obj->facefunc = &akgl_actor_automatic_face;
    obj->movementlogicfunc = &akgl_actor_logic_movement;
    obj->changeframefunc = &akgl_actor_logic_changeframe;
    obj->addchild = &akgl_actor_add_child;
    obj->collidefunc = &akgl_actor_collide_block;

    FAIL_ZERO_RETURN(
	errctx,
	SDL_SetPointerProperty(AKGL_REGISTRY_ACTOR, name, (void *)obj),
	AKERR_KEY,
	"Unable to add actor to registry"
	);
    obj->refcount += 1;
    SDL_Log("Actor %s initialized and added to the registry", (char *)obj->name);
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_actor_set_character(akgl_Actor *obj, char *basecharname)
{
    PREPARE_ERROR(errctx);
    FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "Null actor reference");
    FAIL_ZERO_RETURN(errctx, basecharname, AKERR_NULLPOINTER, "Null character reference");

    obj->basechar = SDL_GetPointerProperty(AKGL_REGISTRY_CHARACTER, basecharname, NULL);

    FAIL_ZERO_RETURN(errctx, obj->basechar, AKERR_NULLPOINTER, "Character not found in the registry");
    obj->ax = 0;
    obj->ay = 0;
    obj->sx = obj->basechar->sx;
    obj->sy = obj->basechar->sy;

    /*
     * The character's shape is the template, and rebinding a character brings
     * its shape with it -- unless the game has set one on this actor and said
     * so. Without the flag there is no way to express "this one is different"
     * that survives the next call, and no way to tell it apart from "this one
     * has not been set up yet".
     */
    if ( obj->shape_override == false ) {
	obj->shape = obj->basechar->shape;
    }
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_actor_automatic_face(akgl_Actor *obj)
{
    PREPARE_ERROR(errctx);
    FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "Null actor reference");
    ATTEMPT {
	if ( obj->movement_controls_face == true ) {
	    // TODO : This doesn't really work properly
	    AKGL_BITMASK_DEL(obj->state, AKGL_ACTOR_STATE_FACE_ALL);
	    if ( AKGL_BITMASK_HAS(obj->state, AKGL_ACTOR_STATE_MOVING_LEFT) ) {
		AKGL_BITMASK_ADD(obj->state, AKGL_ACTOR_STATE_FACE_LEFT);
	    } else if ( AKGL_BITMASK_HAS(obj->state, AKGL_ACTOR_STATE_MOVING_RIGHT) ) {
		AKGL_BITMASK_ADD(obj->state, AKGL_ACTOR_STATE_FACE_RIGHT);
	    } else if ( AKGL_BITMASK_HAS(obj->state, AKGL_ACTOR_STATE_MOVING_UP) ) {
		AKGL_BITMASK_ADD(obj->state, AKGL_ACTOR_STATE_FACE_UP);
	    } else if ( AKGL_BITMASK_HAS(obj->state, AKGL_ACTOR_STATE_MOVING_DOWN) ) {
		AKGL_BITMASK_ADD(obj->state, AKGL_ACTOR_STATE_FACE_DOWN);
	    }
	}
    } CLEANUP {
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_actor_logic_changeframe(akgl_Actor *obj, akgl_Sprite *curSprite, SDL_Time curtime)
{
    PREPARE_ERROR(errctx);
    FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "Null actor reference");
    ATTEMPT {
	// are we currently looping in reverse?
	if ( curSprite->loop == true && obj->curSpriteReversing == true ) {
	    // are we at the beginning of the loop?
	    if ( obj->curSpriteFrameId == 0 ) {
		obj->curSpriteReversing = false;
		obj->curSpriteFrameId += 1;
	    } else {
		obj->curSpriteFrameId -= 1;
	    }
	    // are we at the end of the animation?
	} else if ( obj->curSpriteFrameId >= (curSprite->frames-1) ) {
	    // are we set to loop in reverse?
	    if ( curSprite->loop == true && curSprite->loopReverse == true ) {
		obj->curSpriteReversing = true;
		obj->curSpriteFrameId -= 1;
		// are we set to loop forward?
	    } else {
		// we are at the end of the animation and we either loop forward or do not loop
		obj->curSpriteFrameId = 0;
	    }
	    // we are not looping in reverse and we are not at the end of the animation
	} else {
	    obj->curSpriteFrameId += 1;
	}
    } CLEANUP {
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

// raises AKGL_ERR_LOGICINTERRUPT if we don't want the physics simulator to process us
akerr_ErrorContext *akgl_actor_logic_movement(akgl_Actor *actor, float32_t dt)
{
    PREPARE_ERROR(errctx);
    FAIL_ZERO_RETURN(errctx, actor, AKERR_NULLPOINTER, "actor");
    FAIL_ZERO_RETURN(errctx, actor->basechar, AKERR_NULLPOINTER, "actor->basechar");
    actor->sx = actor->basechar->sx;
    actor->sy = actor->basechar->sy;
    actor->sz = actor->basechar->sz;
    if ( AKGL_BITMASK_HAS(actor->state, AKGL_ACTOR_STATE_MOVING_LEFT) ) {
	actor->ax = -actor->basechar->ax;
    } else if ( AKGL_BITMASK_HAS(actor->state, AKGL_ACTOR_STATE_MOVING_RIGHT) ) {
	actor->ax = actor->basechar->ax;
    }
    if ( AKGL_BITMASK_HAS(actor->state, AKGL_ACTOR_STATE_MOVING_UP) ) {
	actor->ay = -actor->basechar->ay;
    } else if ( AKGL_BITMASK_HAS(actor->state, AKGL_ACTOR_STATE_MOVING_DOWN) ) {
	actor->ay = actor->basechar->ay;
    }
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_actor_update(akgl_Actor *obj)
{
    PREPARE_ERROR(errctx);
    SDL_Time curtime = 0;
    akgl_Sprite *curSprite = NULL;

    FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor reference");
    FAIL_ZERO_RETURN(errctx, obj->basechar, AKERR_NULLPOINTER, "Actor has NULL base character reference");

    SDL_GetCurrentTime(&curtime);
    PASS(errctx, obj->facefunc(obj));

    ATTEMPT {
	CATCH(errctx, akgl_character_sprite_get(obj->basechar, obj->state, &curSprite));
	if ( ((curtime) - obj->curSpriteFrameTimer) >= curSprite->speed) {
	    CATCH(errctx, obj->changeframefunc(obj, curSprite, curtime));
	    obj->curSpriteFrameTimer = curtime;
	}
    } CLEANUP {
    } PROCESS(errctx) {
    } HANDLE(errctx, AKERR_KEY) {
	// TODO : Why are we passing this error? It could only come from akgl_character_sprite_get
	// or changeframefunc, both of which should never return AKERR_KEY...
	SUCCEED_RETURN(errctx);
    } FINISH(errctx, true);

    SUCCEED_RETURN(errctx);
}

/**
 * @brief Decide whether an actor is worth drawing this frame.
 *
 * Two questions at once: is the actor on camera, and does it want to be drawn.
 * The camera test allows a sprite's own width and height on the near edges, so
 * an actor partly on screen still counts as visible rather than popping in once
 * its origin crosses the boundary.
 *
 * An actor with no sprite for its current state is reported as not visible
 * rather than as an error -- there is nothing to draw, which is an answer.
 *
 * @param obj     The actor to test. Required, along with its `basechar`.
 * @param camera  The visible rectangle in map coordinates. Required in practice;
 *                not checked, and dereferenced once a sprite has been found.
 * @param visible Receives the verdict. Required; not checked. Written on the
 *                no-sprite path as well as the ordinary ones.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKERR_NULLPOINTER If @p obj or `obj->basechar` is `NULL`.
 */
static akerr_ErrorContext *actor_visible(akgl_Actor *obj, SDL_FRect *camera, bool *visible)
{
    PREPARE_ERROR(errctx);
    akgl_Sprite *curSprite = NULL;

    FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor");
    FAIL_ZERO_RETURN(errctx, obj->basechar, AKERR_NULLPOINTER, "Actor has NULL base character reference");

    ATTEMPT {
	CATCH(errctx, akgl_character_sprite_get(obj->basechar, obj->state, &curSprite));
    } CLEANUP {
    } PROCESS(errctx) {
    } HANDLE(errctx, AKERR_KEY) {
	// TODO: Actor has no sprite matching the current state. Should we treat this as an error and throw?
	*visible = false;
	SUCCEED_RETURN(errctx);
    } FINISH(errctx, true);

    if ( (obj->x < (camera->x - curSprite->width)) ||
	 (obj->x > (camera->x + camera->w)) ||
	 (obj->y < (camera->y - curSprite->height)) ||
	 (obj->y > (camera->y + camera->h)) ) {
	*visible = false;
    } else {
	*visible = obj->visible;
    }
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_actor_render(akgl_Actor *obj)
{
    PREPARE_ERROR(errctx);
    akgl_Sprite *curSprite = NULL;
    bool visible = false;
    SDL_FRect src;
    SDL_FRect dest;

    FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor");
    FAIL_ZERO_RETURN(errctx, obj->basechar, AKERR_NULLPOINTER, "Actor has NULL base character reference");

    ATTEMPT {
	CATCH(errctx, akgl_character_sprite_get(obj->basechar, obj->state, &curSprite));
	CATCH(errctx, actor_visible(obj, akgl_camera, &visible));
    } CLEANUP {
    } PROCESS(errctx) {
    } HANDLE(errctx, AKERR_KEY) {
    } HANDLE_GROUP(errctx, AKERR_OUTOFBOUNDS) {
	// If an actor doesn't have a sprite for a state, just log it and move on
	LOG_ERROR(errctx);
    } FINISH(errctx, true);

    if ( ! visible ) {
	SUCCEED_RETURN(errctx);
    }

    if ( (obj->curSpriteFrameId > curSprite->frames) ) {
	// This isn't necessarily an error - this actor's frame index is outside the range of
	// their current sprite. There are a number of reasons this could happen, and it will
	// get cleaned up on the next logic update. Just pass on rendering them this frame.
	SUCCEED_RETURN(errctx);
    }

    ATTEMPT {
	CATCH(errctx,
	      akgl_spritesheet_coords_for_frame(
		  curSprite,
		  &src,
		  obj->curSpriteFrameId)
	    );
    } CLEANUP {
    } PROCESS(errctx) {
    } FINISH(errctx, true);

    if ( obj->parent != NULL ) {
	dest.x = (obj->parent->x + obj->x - akgl_camera->x);
	dest.y = (obj->parent->y + obj->y - akgl_camera->y);
    } else {
	dest.x = (obj->x - akgl_camera->x);
	dest.y = (obj->y - akgl_camera->y);
    }
    dest.w = curSprite->width * obj->scale;
    dest.h = curSprite->height * obj->scale;

    PASS(errctx, akgl_renderer->draw_texture(akgl_renderer, curSprite->sheet->texture, &src, &dest, 0, NULL, SDL_FLIP_NONE));
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_actor_add_child(akgl_Actor *obj, akgl_Actor *child)
{
    int i = 0;
    PREPARE_ERROR(errctx);
    FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL parent pointer");
    FAIL_ZERO_RETURN(errctx, child, AKERR_NULLPOINTER, "NULL child pointer");

    FAIL_NONZERO_RETURN(errctx, child->parent, AKERR_RELATIONSHIP, "Child object already has a parent");
    for ( i = 0; i < AKGL_ACTOR_MAX_CHILDREN ; i++ ) {
	if ( obj->children[i] == NULL ) {
	    obj->children[i] = child;
	    child->parent = obj;
	    child->refcount += 1;
	    SUCCEED_RETURN(errctx);
	}
    }
    FAIL_RETURN(errctx, AKERR_OUTOFBOUNDS, "Parent object has no remaining child slots left");
}

void akgl_registry_iterate_actor(void *userdata, SDL_PropertiesID registry, const char *name)
{
    PREPARE_ERROR(errctx);
    akgl_Iterator *opflags = (akgl_Iterator *)userdata;
    akgl_Actor *obj = NULL;

    ATTEMPT {
	FAIL_ZERO_BREAK(errctx, name, AKERR_NULLPOINTER, "registry_iterate_actor received NULL property name");
	FAIL_ZERO_BREAK(errctx, opflags, AKERR_NULLPOINTER, "received NULL iterator flags");
	obj = SDL_GetPointerProperty(registry, name, NULL);
	FAIL_ZERO_BREAK(errctx, obj, AKERR_KEY, "registry_iterate_actor received property name that was not in the registry");
	if (AKGL_BITMASK_HAS(opflags->flags, AKGL_ITERATOR_OP_LAYERMASK) && obj->layer != opflags->layerid) {
	    break;
	}
	if (AKGL_BITMASK_HAS(opflags->flags, AKGL_ITERATOR_OP_UPDATE)) {
	    CATCH(errctx, obj->updatefunc(obj));
	}
	if (AKGL_BITMASK_HAS(opflags->flags, AKGL_ITERATOR_OP_TILEMAPSCALE)) {
	    CATCH(errctx, akgl_tilemap_scale_actor(akgl_gamemap, obj));
	} else {
	    obj->scale = 1.0;
	}
	if (AKGL_BITMASK_HAS(opflags->flags, AKGL_ITERATOR_OP_RENDER)) {
	    CATCH(errctx, obj->renderfunc(obj));
	}
    } CLEANUP {
    } PROCESS(errctx) {
    } FINISH_NORETURN(errctx);
}

akerr_ErrorContext *akgl_actor_cmhf_left_on(akgl_Actor *obj, SDL_Event *event)
{
    PREPARE_ERROR(errctx);
    FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "actor");
    FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "event");
    FAIL_ZERO_RETURN(errctx, obj->basechar, AKERR_NULLPOINTER, "actor->basechar");
    //SDL_Log("event %d (button %d / key %d) moves actor left", event->type, event->gbutton.which, event->key.key);
    AKGL_BITMASK_DEL(obj->state, (AKGL_ACTOR_STATE_FACE_ALL | AKGL_ACTOR_STATE_MOVING_ALL));
    obj->ax = -(obj->basechar->ax);
    AKGL_BITMASK_ADD(obj->state, (AKGL_ACTOR_STATE_MOVING_LEFT | AKGL_ACTOR_STATE_FACE_LEFT));
    //SDL_Log("new target actor state: %b", obj->state);
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_actor_cmhf_left_off(akgl_Actor *obj, SDL_Event *event)
{
    PREPARE_ERROR(errctx);
    FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor");
    FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
    //SDL_Log("event %d (button %d / key %d) stops moving actor left", event->type, event->gbutton.which, event->key.key);
    obj->ax = 0;
    obj->tx = 0;
    AKGL_BITMASK_DEL(obj->state, AKGL_ACTOR_STATE_MOVING_LEFT);
    //SDL_Log("new target actor state: %b", obj->state);
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_actor_cmhf_right_on(akgl_Actor *obj, SDL_Event *event)
{
    PREPARE_ERROR(errctx);
    FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor");
    FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
    FAIL_ZERO_RETURN(errctx, obj->basechar, AKERR_NULLPOINTER, "actor->basechar");
    //SDL_Log("event %d (button %d / key %d) moves actor right", event->type, event->gbutton.which, event->key.key);
    obj->ax = obj->basechar->ax;
    AKGL_BITMASK_DEL(obj->state, (AKGL_ACTOR_STATE_FACE_ALL | AKGL_ACTOR_STATE_MOVING_ALL));
    AKGL_BITMASK_ADD(obj->state, (AKGL_ACTOR_STATE_MOVING_RIGHT | AKGL_ACTOR_STATE_FACE_RIGHT));
    //SDL_Log("new target actor state: %b", obj->state);
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_actor_cmhf_right_off(akgl_Actor *obj, SDL_Event *event)
{
    PREPARE_ERROR(errctx);
    FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor");
    FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
    //SDL_Log("event %d (button %d / key %d) stops moving actor right", event->type, event->gbutton.which, event->key.key);
    obj->ax = 0;
    obj->tx = 0;
    AKGL_BITMASK_DEL(obj->state, AKGL_ACTOR_STATE_MOVING_RIGHT);
    //SDL_Log("new target actor state: %b", obj->state);
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_actor_cmhf_up_on(akgl_Actor *obj, SDL_Event *event)
{
    PREPARE_ERROR(errctx);
    FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor");
    FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
    FAIL_ZERO_RETURN(errctx, obj->basechar, AKERR_NULLPOINTER, "actor->basechar");
    //SDL_Log("event %d (button %d / key %d) moves actor up", event->type, event->gbutton.which, event->key.key);
    obj->ay = -(obj->basechar->ay);
    AKGL_BITMASK_DEL(obj->state, (AKGL_ACTOR_STATE_FACE_ALL | AKGL_ACTOR_STATE_MOVING_ALL));
    AKGL_BITMASK_ADD(obj->state, (AKGL_ACTOR_STATE_FACE_UP | AKGL_ACTOR_STATE_MOVING_UP));
    //SDL_Log("new target actor state: %b", obj->state);
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_actor_cmhf_up_off(akgl_Actor *obj, SDL_Event *event)
{
    PREPARE_ERROR(errctx);
    FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor");
    FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
    //SDL_Log("event %d (button %d / key %d) stops moving actor up", event->type, event->gbutton.which, event->key.key);
    obj->ay = 0;
    obj->ty = 0;
    AKGL_BITMASK_DEL(obj->state, AKGL_ACTOR_STATE_MOVING_UP);
    //SDL_Log("new target actor state: %b", obj->state);
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_actor_cmhf_down_on(akgl_Actor *obj, SDL_Event *event)
{
    PREPARE_ERROR(errctx);
    FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor");
    FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
    FAIL_ZERO_RETURN(errctx, obj->basechar, AKERR_NULLPOINTER, "actor->basechar");
    //SDL_Log("event %d (button %d / key %d) moves actor down", event->type, event->gbutton.which, event->key.key);
    obj->ay = obj->basechar->ay;
    AKGL_BITMASK_DEL(obj->state, (AKGL_ACTOR_STATE_FACE_ALL | AKGL_ACTOR_STATE_MOVING_ALL));
    AKGL_BITMASK_ADD(obj->state, (AKGL_ACTOR_STATE_MOVING_DOWN | AKGL_ACTOR_STATE_FACE_DOWN));
    //SDL_Log("new target actor state: %b", obj->state);
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_actor_cmhf_down_off(akgl_Actor *obj, SDL_Event *event)
{
    PREPARE_ERROR(errctx);
    FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor");
    FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
    //SDL_Log("event %d (button %d / key %d) stops moving actor down", event->type, event->gbutton.which, event->key.key);
    obj->ay = 0;
    obj->ty = 0;
    AKGL_BITMASK_DEL(obj->state, AKGL_ACTOR_STATE_MOVING_DOWN);
    //SDL_Log("new target actor state: %b", obj->state);
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_actor_collide_block(akgl_Actor *obj, akgl_Contact *contact)
{
    float32_t en = 0.0f;
    float32_t tn = 0.0f;

    PREPARE_ERROR(errctx);
    FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor reference");
    FAIL_ZERO_RETURN(errctx, contact, AKERR_NULLPOINTER, "NULL contact reference");

    // A sensor reports and never pushes. Walking through a coin is not being
    // blocked by it.
    if ( contact->sensor == true ) {
	SUCCEED_RETURN(errctx);
    }

    obj->x += contact->nx * contact->depth;
    obj->y += contact->ny * contact->depth;
    obj->z += contact->nz * contact->depth;

    /*
     * The environmental and thrust terms, separately, and never `v`. See the
     * long note on this function in actor.h: simulate recomputes `v` as `e + t`
     * every step, so writing `v` here is writing to something nobody reads --
     * and treating the two terms separately is what lets an actor pressing into
     * a floor lose its gravity without losing the run it is also doing.
     *
     * A component already pointing out of the surface is left alone. Removing a
     * separating velocity is how an actor sticks to a wall it is walking away
     * from.
     */
    en = (obj->ex * contact->nx) + (obj->ey * contact->ny) + (obj->ez * contact->nz);
    if ( en < 0.0f ) {
	obj->ex -= en * contact->nx;
	obj->ey -= en * contact->ny;
	obj->ez -= en * contact->nz;
    }

    tn = (obj->tx * contact->nx) + (obj->ty * contact->ny) + (obj->tz * contact->nz);
    if ( tn < 0.0f ) {
	obj->tx -= tn * contact->nx;
	obj->ty -= tn * contact->ny;
	obj->tz -= tn * contact->nz;
    }

    obj->vx = obj->ex + obj->tx;
    obj->vy = obj->ey + obj->ty;
    obj->vz = obj->ez + obj->tz;
    SUCCEED_RETURN(errctx);
}
