// Host-side AGI pipeline test (Phase 1 sub-D).
//
// Wires up a real logic-resource cache + trace callbacks and runs
// the VM in a simulated game-cycle loop:
//   * agiVmRun()
//   * NEW_ROOM   -> update var[0]/var[1], reset flags, restart logic.0
//   * RETURN     -> count one cycle, restart logic.0
//   * UNKNOWN_OP -> print and exit
// Capped at MAX_CYCLES so a logic that always returns without
// painting can't loop forever.
//
// What this lets us verify before touching any emulator:
//   - logic.0's prologue + IF branching against fresh game state
//   - room transition handling (new.room(N) loads logic N)
//   - room logic execution (load.pic/draw.pic/show.pic callbacks)
//   - call.logic / RETURN frame management
//
// Usage:
//   testAgiPipeline <game-directory> [max-cycles]

#include "agi.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>


#define MAX_CYCLES_DEFAULT  20u
#define AGI_LOGIC_HDR_BYTES 2u


typedef struct {
    uint8_t        *raw;
    uint16_t        rawLength;
    const uint8_t  *bytecode;
    uint16_t        bytecodeLength;
} CachedLogicT;


static AgiGameT        gGame;
static CachedLogicT    gLogicCache[AGI_MAX_RESOURCES];
static uint32_t        gTraceLoadPic;
static uint32_t        gTraceDrawPic;
static uint32_t        gTraceShowPic;
static uint32_t        gTraceDiscardPic;
static uint32_t        gTraceOverlayPic;
static uint32_t        gTraceFetchLogic;


static void           cbDiscardPic(void *ctx, uint8_t picId);
static void           cbDrawPic(void *ctx, uint8_t picId);
static const uint8_t *cbFetchLogic(void *ctx, uint8_t logicId, uint16_t *outLength);
static void           cbLoadPic(void *ctx, uint8_t picId);
static void           cbOverlayPic(void *ctx, uint8_t picId);
static void           cbShowPic(void *ctx);
static const char    *haltName(AgiVmHaltE r);
static void           releaseCache(void);


static void cbDiscardPic(void *ctx, uint8_t picId) {
    (void)ctx;
    gTraceDiscardPic++;
    printf("    [host] discardPic(%u)\n", (unsigned)picId);
}


static void cbDrawPic(void *ctx, uint8_t picId) {
    (void)ctx;
    gTraceDrawPic++;
    printf("    [host] drawPic(%u)\n", (unsigned)picId);
}


static const uint8_t *cbFetchLogic(void *ctx, uint8_t logicId, uint16_t *outLength) {
    CachedLogicT  *slot;
    uint8_t       *raw;
    uint16_t       rawLength;
    uint16_t       bcLen;

    (void)ctx;
    slot = &gLogicCache[logicId];
    if (slot->bytecode != NULL) {
        *outLength = slot->bytecodeLength;
        return slot->bytecode;
    }

    if (logicId >= gGame.resCount[AGI_RES_LOGIC]) {
        return NULL;
    }
    raw = agiResLoad(&gGame, AGI_RES_LOGIC, (uint16_t)logicId, &rawLength);
    if (raw == NULL || rawLength < AGI_LOGIC_HDR_BYTES) {
        if (raw != NULL) {
            free(raw);
        }
        return NULL;
    }
    bcLen = (uint16_t)(raw[0] | ((uint16_t)raw[1] << 8));
    if ((uint16_t)(AGI_LOGIC_HDR_BYTES + bcLen) > rawLength) {
        free(raw);
        return NULL;
    }
    slot->raw            = raw;
    slot->rawLength      = rawLength;
    slot->bytecode       = raw + AGI_LOGIC_HDR_BYTES;
    slot->bytecodeLength = bcLen;
    gTraceFetchLogic++;
    printf("    [host] fetchLogic(%u) -> %u bytes (first load)\n",
           (unsigned)logicId, (unsigned)bcLen);

    *outLength = bcLen;
    return slot->bytecode;
}


static void cbLoadPic(void *ctx, uint8_t picId) {
    (void)ctx;
    gTraceLoadPic++;
    printf("    [host] loadPic(%u)\n", (unsigned)picId);
}


static void cbOverlayPic(void *ctx, uint8_t picId) {
    (void)ctx;
    gTraceOverlayPic++;
    printf("    [host] overlayPic(%u)\n", (unsigned)picId);
}


static void cbShowPic(void *ctx) {
    (void)ctx;
    gTraceShowPic++;
    printf("    [host] showPic\n");
}


static const char *haltName(AgiVmHaltE r) {
    switch (r) {
        case AGI_VM_HALT_NONE:        return "NONE";
        case AGI_VM_HALT_RETURN:      return "RETURN";
        case AGI_VM_HALT_UNKNOWN_OP:  return "UNKNOWN_OP";
        case AGI_VM_HALT_TRUNCATED:   return "TRUNCATED";
        case AGI_VM_HALT_NEW_ROOM:    return "NEW_ROOM";
        case AGI_VM_HALT_NO_LOGIC:    return "NO_LOGIC";
        case AGI_VM_HALT_STACK_OVER:  return "STACK_OVER";
        default:                      return "?";
    }
}


static void releaseCache(void) {
    uint16_t  i;

    for (i = 0u; i < AGI_MAX_RESOURCES; i++) {
        if (gLogicCache[i].raw != NULL) {
            free(gLogicCache[i].raw);
            gLogicCache[i].raw = NULL;
        }
    }
}


int main(int argc, char **argv) {
    AgiVmT             vm;
    AgiVmCallbacksT    cb;
    uint16_t           length;
    const uint8_t     *logic0;
    uint32_t           maxCycles;
    uint32_t           cycles;
    AgiVmHaltE         reason;
    uint8_t            prevRoom;
    uint8_t            i;

    if (argc < 2 || argc > 3) {
        fprintf(stderr, "usage: %s <game-directory> [max-cycles]\n", argv[0]);
        return 2;
    }
    maxCycles = (argc == 3) ? (uint32_t)atoi(argv[2]) : (uint32_t)MAX_CYCLES_DEFAULT;
    if (maxCycles == 0u) {
        maxCycles = (uint32_t)MAX_CYCLES_DEFAULT;
    }

    if (!agiResOpen(&gGame)) {
        fprintf(stderr, "FAIL: agiResOpen rejected '%s'\n", argv[1]);
        return 1;
    }
    memset(gLogicCache, 0, sizeof(gLogicCache));

    cb.fetchLogic    = cbFetchLogic;
    cb.loadPic       = cbLoadPic;
    cb.drawPic       = cbDrawPic;
    cb.showPic       = cbShowPic;
    cb.discardPic    = cbDiscardPic;
    cb.overlayPic    = cbOverlayPic;
    cb.haveKey       = NULL;
    cb.fetchMessage  = NULL;
    cb.addToPic      = NULL;
    cb.soundDuration = NULL;
    cb.playSound     = NULL;
    cb.stopSound     = NULL;
    cb.ctx           = NULL;

    agiVmInit(&vm);
    agiVmSetCallbacks(&vm, &cb);

    length = 0u;
    logic0 = cbFetchLogic(NULL, 0u, &length);
    if (logic0 == NULL) {
        fprintf(stderr, "FAIL: could not load logic.0\n");
        releaseCache();
        agiResClose(&gGame);
        return 1;
    }
    agiVmResetToLogic(&vm, logic0, length, 0u);

    cycles = 0u;
    for (;;) {
        if (cycles >= maxCycles) {
            printf("  (capped at %u cycles)\n", (unsigned)maxCycles);
            break;
        }
        cycles++;
        // Simulate one wall-clock second per cycle so per-second
        // game logic (countdowns, state machines, animation cadence)
        // advances under the host test without waiting in real time.
        // KQ3's title state machine in particular is gated entirely
        // on var 11 changing; without this tick logic.45's countdowns
        // never fire and the title appears to stall at 20+ cycles.
        agiVmTickSeconds(&vm, 1u);
        printf("  cycle %u: pc=%u/%u logic=%u callDepth=%u\n",
               (unsigned)cycles, (unsigned)vm.pc, (unsigned)vm.codeLength,
               (unsigned)vm.currentLogicId, (unsigned)vm.callDepth);

        reason = agiVmRun(&vm);

        printf("    halt=%s at pc=%u logic=%u",
               haltName(reason), (unsigned)vm.pc, (unsigned)vm.currentLogicId);
        if (reason == AGI_VM_HALT_UNKNOWN_OP) {
            printf("  opcode=0x%02X", (unsigned)vm.lastUnknownOp);
        }
        if (reason == AGI_VM_HALT_NEW_ROOM) {
            printf("  newRoom=%u", (unsigned)vm.newRoomId);
        }
        printf("\n");

        if (reason == AGI_VM_HALT_NEW_ROOM) {
            prevRoom = vm.vars[0];
            vm.vars[0] = vm.newRoomId;
            vm.vars[1] = prevRoom;
            // Reset most flags, then set flag 5 ("new room") to
            // signal the room logic that its init branch should
            // fire. In the real interpreter the engine also resets
            // a handful of cycle vars (input, signal, etc.) but for
            // the sub-D pipeline test the new-room flag is the only
            // one room logic actually checks.
            for (i = 0u; i < 32u; i++) {
                vm.flags[i] = 0u;
            }
            vm.flags[5] = 1u;
            agiVmResetToLogic(&vm, logic0, length, 0u);
            continue;
        }
        if (reason == AGI_VM_HALT_RETURN) {
            // End-of-cycle housekeeping the real interpreter would
            // perform: clear flag 5 (new.room) so subsequent cycles
            // don't re-run the room-init branch.
            vm.flags[5] = 0u;
            agiVmResetToLogic(&vm, logic0, length, 0u);
            continue;
        }
        break;
    }

    printf("===== trace =====\n");
    printf("  fetchLogic:  %u (first-loads only)\n", (unsigned)gTraceFetchLogic);
    printf("  loadPic:     %u\n", (unsigned)gTraceLoadPic);
    printf("  drawPic:     %u\n", (unsigned)gTraceDrawPic);
    printf("  showPic:     %u\n", (unsigned)gTraceShowPic);
    printf("  discardPic:  %u\n", (unsigned)gTraceDiscardPic);
    printf("  overlayPic:  %u\n", (unsigned)gTraceOverlayPic);

    releaseCache();
    agiResClose(&gGame);
    return 0;
}
