// AGI v2 animated-object table: state lifecycle + per-cycle ticks.
//
// The VM stores an AgiObjectT[16] inside AgiVmT; opcode handlers in
// agiVm.c mutate fields directly (set.view / position / step.size /
// etc.) and the host renders from the table each frame. This file
// owns the housekeeping the dispatcher would otherwise duplicate
// across many opcodes:
//
//   * reset on construction and on NEW_ROOM
//   * per-cycle cel cycling honoring start/stop.cycling, end.of.loop
//   * per-cycle motion: normal / wander / follow.ego / move.obj
//
// Every algorithm here is a re-implementation against the published
// AGI v2 actor-cycle / actor-motion spec; no third-party source is
// referenced.

#include "agi.h"

#include "joey/core.h"
#include "joey/debug.h"

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


#ifdef JOEYLIB_PLATFORM_IIGS
#endif


// AGI direction encoding. 0 means "no motion this cycle"; 1..8
// circle clockwise starting at north.
//   1=N    2=NE   3=E    4=SE
//   5=S    6=SW   7=W    8=NW
#define AGI_DIR_NONE        0u
#define AGI_DIR_N           1u
#define AGI_DIR_NE          2u
#define AGI_DIR_E           3u
#define AGI_DIR_SE          4u
#define AGI_DIR_S           5u
#define AGI_DIR_SW          6u
#define AGI_DIR_W           7u
#define AGI_DIR_NW          8u
#define AGI_DIR_COUNT       9u

#define WANDER_TICKS_MIN    4u
#define WANDER_TICKS_MAX    24u
#define MOVE_REACH_PX       2


// dx, dy per direction (subscript 0 unused; AGI directions are 1..8).
static const int8_t kDirDx[AGI_DIR_COUNT] = { 0,  0, +1, +1, +1,  0, -1, -1, -1 };
static const int8_t kDirDy[AGI_DIR_COUNT] = { 0, -1, -1,  0, +1, +1, +1,  0, -1 };


// ----- Prototypes -----

static void  advanceCycle(AgiVmT *vm, uint8_t objId);
static void  advanceMotion(AgiVmT *vm, uint8_t objId);
static uint8_t  celCount(const AgiVmT *vm, const AgiObjectT *obj);
static uint8_t  directionToward(int16_t fromX, int16_t fromY, int16_t toX, int16_t toY);


// ----- Internal helpers (alphabetical) -----

static void advanceCycle(AgiVmT *vm, uint8_t objId) {
    AgiObjectT  *obj;
    uint8_t      cels;
    uint8_t      next;

    obj = &vm->objects[objId];
    if ((obj->flags & AGI_OBJ_FLAG_ANIMATED) == 0u) {
        return;
    }
    if ((obj->flags & AGI_OBJ_FLAG_CYCLING) == 0u) {
        return;
    }
    // Erased objects don't cycle. KQ3 LOGIC.45 sparkle pattern depends
    // on this: erase(1..4) at wizard-scene start leaves their CYCLING
    // flag set (LOGIC.45 never explicitly stops it), but Sierra's
    // interpreter freezes the cel until the next draw(). Without this
    // gate, end.of.loop fires for invisible objs, sets flag 221..224,
    // and the sparkle loop kicks back in on the wizard pic.
    if ((obj->flags & AGI_OBJ_FLAG_DRAWN) == 0u) {
        return;
    }
    // stop.update freezes the obj's displayed cel until start.update.
    // Canonical AGI cycles cels internally regardless, but with no
    // display refresh the visual stays put. Since our host renderer
    // always paints from obj->cel, the easiest faithful approximation
    // is to also stop advancing cel while !UPDATING -- otherwise
    // poses cycle visibly (e.g. Manannan's body in room 46, which is
    // stop.update'd at init and only released to start.update at a
    // specific state, would otherwise wave its hand continuously).
    if ((obj->flags & AGI_OBJ_FLAG_UPDATING) == 0u) {
        return;
    }
    if (obj->cycleTime == 0u) {
        return;
    }
    obj->cycleTick++;
    if (obj->cycleTick < obj->cycleTime) {
        return;
    }
    obj->cycleTick = 0u;

    cels = celCount(vm, obj);
    if (cels <= 1u) {
        return;
    }

    switch (obj->cycleMode) {
        case AGI_CYCLE_NORMAL:
            next = (uint8_t)((obj->cel + 1u) % cels);
            obj->cel = next;
            break;
        case AGI_CYCLE_REVERSE:
            obj->cel = (obj->cel == 0u) ? (uint8_t)(cels - 1u) : (uint8_t)(obj->cel - 1u);
            break;
        case AGI_CYCLE_END_LOOP:
            if (obj->cel + 1u >= cels) {
                // reached last frame, fire end-of-loop
                obj->flags &= (uint8_t)~AGI_OBJ_FLAG_CYCLING;
                if (obj->endOfLoopFlag != 0u) {
                    vm->flags[obj->endOfLoopFlag] = 1u;
                }
            } else {
                obj->cel++;
            }
            break;
        case AGI_CYCLE_REV_LOOP:
            if (obj->cel == 0u) {
                obj->flags &= (uint8_t)~AGI_OBJ_FLAG_CYCLING;
                if (obj->endOfLoopFlag != 0u) {
                    vm->flags[obj->endOfLoopFlag] = 1u;
                }
            } else {
                obj->cel--;
            }
            break;
        default:
            break;
    }
}


static void advanceMotion(AgiVmT *vm, uint8_t objId) {
    AgiObjectT  *obj;
    int16_t      newX;
    int16_t      newY;
    uint8_t      dir;
    int8_t       dx;
    int8_t       dy;

    obj = &vm->objects[objId];
    if ((obj->flags & AGI_OBJ_FLAG_ANIMATED) == 0u) {
        return;
    }
    if ((obj->flags & AGI_OBJ_FLAG_DRAWN) == 0u) {
        return;
    }
    if (obj->stepSize == 0u) {
        return;
    }
    if (obj->stepTime == 0u) {
        return;
    }
    obj->stepTick++;
    if (obj->stepTick < obj->stepTime) {
        return;
    }
    obj->stepTick = 0u;

    switch (obj->motionType) {
        case AGI_MOTION_WANDER:
            if (obj->wanderTick == 0u) {
                obj->direction = (uint8_t)(jlRandomRange((uint16_t)AGI_DIR_COUNT));
                obj->wanderTick = (uint8_t)(WANDER_TICKS_MIN + jlRandomRange((uint16_t)(WANDER_TICKS_MAX - WANDER_TICKS_MIN)));
            } else {
                obj->wanderTick--;
            }
            break;
        case AGI_MOTION_FOLLOW_EGO:
            obj->direction = directionToward(obj->x, obj->y, vm->objects[0].x, vm->objects[0].y);
            break;
        case AGI_MOTION_MOVE_OBJ: {
            int16_t  reachDx;
            int16_t  reachDy;

            reachDx = (int16_t)(obj->targetX - obj->x);
            reachDy = (int16_t)(obj->targetY - obj->y);
            if (reachDx < 0) {
                reachDx = (int16_t)(-reachDx);
            }
            if (reachDy < 0) {
                reachDy = (int16_t)(-reachDy);
            }
            if (reachDx <= MOVE_REACH_PX && reachDy <= MOVE_REACH_PX) {
                obj->x         = obj->targetX;
                obj->y         = obj->targetY;
                obj->direction = AGI_DIR_NONE;
                obj->motionType = AGI_MOTION_NORMAL;
                obj->stepSize  = obj->moveStepBackup;
                if (obj->moveDoneFlag != 0u) {
                    vm->flags[obj->moveDoneFlag] = 1u;
                }
                return;
            }
            obj->direction = directionToward(obj->x, obj->y, obj->targetX, obj->targetY);
            break;
        }
        case AGI_MOTION_NORMAL:
        default:
            // Direction is whatever the script (or var 6 for ego) set.
            break;
    }

    dir = obj->direction;
    if (dir == AGI_DIR_NONE || dir >= AGI_DIR_COUNT) {
        return;
    }
    dx   = kDirDx[dir];
    dy   = kDirDy[dir];
    newX = (int16_t)(obj->x + (int16_t)((int16_t)dx * (int16_t)obj->stepSize));
    newY = (int16_t)(obj->y + (int16_t)((int16_t)dy * (int16_t)obj->stepSize));

    // Clamp to AGI's canonical 160x168 picture window. The horizon
    // applies only to objects observing it.
    if (newX < 0) {
        newX = 0;
    }
    if (newX > (int16_t)(AGI_PIC_WIDTH - 1)) {
        newX = (int16_t)(AGI_PIC_WIDTH - 1);
    }
    if (newY > (int16_t)(AGI_PIC_HEIGHT - 1)) {
        newY = (int16_t)(AGI_PIC_HEIGHT - 1);
    }
    if ((obj->flags & AGI_OBJ_FLAG_HORIZON_IGN) == 0u && newY < (int16_t)vm->horizon) {
        newY = (int16_t)vm->horizon;
    }
    if (newY < 0) {
        newY = 0;
    }
    obj->x = newX;
    obj->y = newY;
}


// Without a view-id-to-loop-cel-count map in the VM, fall back to an
// Asks the host for the actual cel count of obj's view+loop via
// the viewCelCount callback. Falls back to AGI_MAX_CELS_PER_LOOP
// only when no callback is wired (legacy host) -- but cycling
// is broken in that mode because end.of.loop won't fire at the
// correct cel. A 0 return from the callback means "view not
// loaded yet"; we report 0 and the caller's `cels <= 1u` guard
// pauses cycling until the load completes.
static uint8_t celCount(const AgiVmT *vm, const AgiObjectT *obj) {
    if (vm->callbacks.viewCelCount != NULL) {
        return vm->callbacks.viewCelCount(vm->callbacks.ctx, obj->viewId, obj->loop);
    }
    return AGI_MAX_CELS_PER_LOOP;
}


// 8-direction snap by quadrant + axis-dominance.
static uint8_t directionToward(int16_t fromX, int16_t fromY, int16_t toX, int16_t toY) {
    int16_t  dx;
    int16_t  dy;
    int16_t  ax;
    int16_t  ay;

    dx = (int16_t)(toX - fromX);
    dy = (int16_t)(toY - fromY);
    if (dx == 0 && dy == 0) {
        return AGI_DIR_NONE;
    }
    ax = (dx < 0) ? (int16_t)(-dx) : dx;
    ay = (dy < 0) ? (int16_t)(-dy) : dy;
    if (ax > ay * 2) {
        return (dx > 0) ? AGI_DIR_E : AGI_DIR_W;
    }
    if (ay > ax * 2) {
        return (dy > 0) ? AGI_DIR_S : AGI_DIR_N;
    }
    if (dx > 0) {
        return (dy > 0) ? AGI_DIR_SE : AGI_DIR_NE;
    }
    return (dy > 0) ? AGI_DIR_SW : AGI_DIR_NW;
}


// ----- Public API (alphabetical) -----

void agiObjReset(AgiObjectT *obj) {
    memset(obj, 0, sizeof(*obj));
    obj->stepSize  = 1u;
    obj->stepTime  = 1u;
    obj->cycleTime = 1u;
    obj->priority  = 0u;            // 0 = "auto-Y-band" (canonical AGI)
    obj->direction = AGI_DIR_NONE;
    obj->cycleMode = AGI_CYCLE_NORMAL;
    obj->motionType = AGI_MOTION_NORMAL;
    obj->endOfLoopFlag = 0u;
    obj->moveDoneFlag  = 0u;
    obj->followDoneFlag = 0u;
}


void agiObjResetAll(AgiVmT *vm) {
    uint8_t  i;

    for (i = 0u; i < (uint8_t)AGI_MAX_OBJECTS; i++) {
        agiObjReset(&vm->objects[i]);
    }
}


// Called by VM core after each VM cycle returns to the host (or by
// the host directly each game-loop cycle). Walks the object table
// and advances cycling + motion. Also polls the host for sound
// completion so the sound-done flag fires on the natural-end of
// playback rather than a pre-computed deadline.
void agiVmTickAnimation(AgiVmT *vm) {
    uint8_t  i;

    // Sound poll: fire done-flags on the host's playback->silent
    // edge. Runs every VM cycle so OP_IF wait loops see the flag
    // within one cycle of actual audio end.
    if (vm->soundPlaying && vm->callbacks.isPlayingSound != NULL) {
        if (!vm->callbacks.isPlayingSound(vm->callbacks.ctx)) {
            jlLogF("sound-done flag fires: flag=%u (v11=%u)",
                   (unsigned)vm->soundDoneFlag, (unsigned)vm->vars[11]);
            jlLogFlush();
            if (vm->soundDoneFlag != 0u) {
                vm->flags[vm->soundDoneFlag] = 1u;
            }
            vm->flags[9]     = 1u;
            vm->soundPlaying = false;
        }
    }

    for (i = 0u; i < (uint8_t)AGI_MAX_OBJECTS; i++) {
        AgiObjectT  *obj;

        obj = &vm->objects[i];
        if ((obj->flags & AGI_OBJ_FLAG_ANIMATED) == 0u) {
            continue;
        }
        // Snapshot current state so the host renderer can restore the
        // last frame's pixels under each obj before re-drawing this
        // frame. Done before the tick so prevX/prevY reflect what was
        // actually painted last frame.
        obj->prevX     = obj->x;
        obj->prevY     = obj->y;
        obj->prevLoop  = obj->loop;
        obj->prevCel   = obj->cel;
        obj->prevView  = obj->viewId;

        advanceCycle(vm, i);
        advanceMotion(vm, i);
    }
    // Mirror ego state into AGI's well-known engine vars so game
    // scripts that probe v6 (ego dir) / current.view / get.posn
    // see the right values without a per-opcode getter call.
    if ((vm->objects[0].flags & AGI_OBJ_FLAG_ANIMATED) != 0u) {
        vm->vars[6] = vm->objects[0].direction;
    }
}
