// AGI v2 LOGIC disassembler.
//
// Walks one LOGIC resource and prints the bytecode in a human-readable
// form, with names for action opcodes, test commands, common variables
// and flags. Intended as a debugging aid while filling out the VM:
// dump KQ3's logic.0 + logic.45 to identify which stub opcodes are
// gating title-screen progression.
//
// Usage:
//   disasmAgi <game-directory> <logic-id> [end-pc]
//
// `end-pc` caps the walk at a given offset; useful for spelunking
// through huge logic.0 without dumping the whole thing.

#include "agi.h"

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


#define AGI_LOGIC_HDR_BYTES    2u
#define ACTION_UNKNOWN         0xFFu
#define MAX_TEST               0x12u


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

static const char *actionName(uint8_t op);
static uint16_t    disasmAction(const uint8_t *code, uint16_t pc, uint16_t length, uint8_t op);
static uint16_t    disasmIf(const uint8_t *code, uint16_t pc, uint16_t length);
static uint16_t    disasmTest(const uint8_t *code, uint16_t pc, uint16_t length);
static const char *testName(uint8_t op);
static int         walk(const uint8_t *code, uint16_t length, uint16_t endPc);


// AGI v2 action-opcode names. Indexed by opcode id; NULL means "no
// such opcode" (matches the VM's ACTION_UNKNOWN sentinel).
static const char *kActionNames[256] = {
    /* 0x00 */ "return", "increment", "decrement", "assignn",
    /* 0x04 */ "assignv", "addn", "addv", "subn",
    /* 0x08 */ "subv", "lindirectv", "rindirect", "lindirectn",
    /* 0x0C */ "set", "reset", "toggle", "setv",
    /* 0x10 */ "resetv", "togglev", "new.room", "new.room.v",
    /* 0x14 */ "load.logic", "load.logic.v", "call", "call.v",
    /* 0x18 */ "load.pic", "draw.pic", "show.pic", "discard.pic",
    /* 0x1C */ "overlay.pic", "show.pri.screen", "load.view", "load.view.v",
    /* 0x20 */ "discard.view", "animate.obj", "unanimate.all", "draw",
    /* 0x24 */ "erase", "position", "position.v", "get.posn",
    /* 0x28 */ "reposition", "set.view", "set.view.v", "set.loop",
    /* 0x2C */ "set.loop.v", "fix.loop", "release.loop", "set.cel",
    /* 0x30 */ "set.cel.v", "last.cel", "current.cel", "current.loop",
    /* 0x34 */ "current.view", "number.of.loops", "set.priority", "set.priority.v",
    /* 0x38 */ "release.priority", "get.priority", "stop.update", "start.update",
    /* 0x3C */ "force.update", "ignore.horizon", "observe.horizon", "set.horizon",
    /* 0x40 */ "object.on.water", "object.on.land", "object.on.anything", "ignore.objs",
    /* 0x44 */ "observe.objs", "distance", "stop.cycling", "start.cycling",
    /* 0x48 */ "normal.cycle", "end.of.loop", "reverse.cycle", "reverse.loop",
    /* 0x4C */ "cycle.time", "stop.motion", "start.motion", "step.size",
    /* 0x50 */ "step.time", "move.obj", "move.obj.v", "follow.ego",
    /* 0x54 */ "wander", "normal.motion", "set.dir", "get.dir",
    /* 0x58 */ "ignore.blocks", "observe.blocks", "block", "unblock",
    /* 0x5C */ "get", "get.v", "drop", "put",
    /* 0x60 */ "put.v", "get.room.v", "load.sound", "sound",
    /* 0x64 */ "stop.sound", "print", "print.v", "display",
    /* 0x68 */ "display.v", "clear.lines", "text.screen", "graphics",
    /* 0x6C */ "set.cursor.char", "set.text.attribute", "shake.screen", "configure.screen",
    /* 0x70 */ "status.line.on", "status.line.off", "set.string", "get.string",
    /* 0x74 */ "word.to.string", "parse", "get.num", "prevent.input",
    /* 0x78 */ "accept.input", "set.key", "add.to.pic", "add.to.pic.v",
    /* 0x7C */ "status", "save.game", "restore.game", "init.disk",
    /* 0x80 */ "restart.game", "show.obj", "random", "program.control",
    /* 0x84 */ "player.control", "obj.status.v", "quit", "show.mem",
    /* 0x88 */ "pause", "echo.line", "cancel.line", "init.joy",
    /* 0x8C */ "toggle.monitor", "version", "script.size", "set.game.id",
    /* 0x90 */ "log", "set.scan.start", "reset.scan.start", "reposition.to",
    /* 0x94 */ "reposition.to.v", "trace.on", "trace.info", "print.at",
    /* 0x98 */ "print.at.v", "discard.view.v", "clear.text.rect", "set.upper.left",
    /* 0x9C */ "set.menu", "set.menu.item", "submit.menu", "enable.item",
    /* 0xA0 */ "disable.item", "menu.input", "show.obj.v", "open.dialogue",
    /* 0xA4 */ "close.dialogue", "mul.n", "mul.v", "div.n",
    /* 0xA8 */ "div.v", "close.window", "set.simple", "push.script",
    /* 0xAC */ "pop.script", "hold.key", "set.pri.base", "discard.sound",
    /* 0xB0 */ "hide.mouse", "allow.menu", "show.mouse", "fence.mouse",
    /* 0xB4 */ "mouse.posn", "release.key", "adj.ego.move.to.x.y",
    /* 0xB7..0xFD */ NULL
    // 0xFE = goto, 0xFF = if -- handled separately
};


// AGI v2 action arg byte counts (matches kActionArgBytes in agiVm.c).
static const uint8_t kActionArgBytes[256] = {
    /* 0x00 */ 0u, 1u, 1u, 2u, 2u, 2u, 2u, 2u, 2u, 2u, 2u, 2u, 1u, 1u, 1u, 1u,
    /* 0x10 */ 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 0u, 1u, 1u, 0u, 1u, 1u,
    /* 0x20 */ 1u, 1u, 0u, 1u, 1u, 3u, 3u, 3u, 3u, 2u, 2u, 2u, 2u, 1u, 1u, 2u,
    /* 0x30 */ 2u, 2u, 2u, 2u, 2u, 2u, 2u, 2u, 1u, 2u, 1u, 1u, 1u, 1u, 1u, 1u,
    /* 0x40 */ 1u, 1u, 1u, 1u, 1u, 3u, 1u, 1u, 1u, 2u, 1u, 2u, 2u, 1u, 1u, 2u,
    /* 0x50 */ 2u, 5u, 5u, 3u, 1u, 1u, 2u, 2u, 1u, 1u, 4u, 0u, 1u, 1u, 1u, 2u,
    /* 0x60 */ 2u, 2u, 1u, 2u, 0u, 1u, 1u, 3u, 3u, 3u, 0u, 0u, 1u, 2u, 1u, 3u,
    /* 0x70 */ 0u, 0u, 2u, 5u, 2u, 1u, 2u, 0u, 0u, 3u, 7u, 7u, 0u, 0u, 0u, 0u,
    /* 0x80 */ 0u, 1u, 3u, 0u, 0u, 1u, 1u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 1u, 1u,
    /* 0x90 */ 1u, 0u, 0u, 3u, 3u, 0u, 3u, 4u, 4u, 1u, 4u, 2u, 1u, 2u, 0u, 1u,
    /* 0xA0 */ 1u, 0u, 1u, 0u, 0u, 2u, 2u, 2u, 2u, 0u, 1u, 0u, 0u, 0u, 1u, 1u,
    /* 0xB0 */ 0u, 1u, 0u, 4u, 2u, 0u, 0u,
                                          ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN,
    /* 0xBA */ ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN,
    /* 0xC0 */ ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN,
    /* 0xC8 */ ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN,
    /* 0xD0 */ ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN,
    /* 0xD8 */ ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN,
    /* 0xE0 */ ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN,
    /* 0xE8 */ ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN,
    /* 0xF0 */ ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN,
    /* 0xF8 */ ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN
};


static const char *kTestNames[MAX_TEST + 1u] = {
    /* 0x00 */ NULL,
    /* 0x01 */ "equaln",
    /* 0x02 */ "equalv",
    /* 0x03 */ "lessn",
    /* 0x04 */ "lessv",
    /* 0x05 */ "greatern",
    /* 0x06 */ "greaterv",
    /* 0x07 */ "isset",
    /* 0x08 */ "issetv",
    /* 0x09 */ "has",
    /* 0x0A */ "obj.in.room",
    /* 0x0B */ "posn",
    /* 0x0C */ "controller",
    /* 0x0D */ "have.key",
    /* 0x0E */ "said",
    /* 0x0F */ "compare.strings",
    /* 0x10 */ "obj.in.box",
    /* 0x11 */ "center.posn",
    /* 0x12 */ "right.posn"
};


static const uint8_t kTestArgBytes[MAX_TEST + 1u] = {
    0u, 2u, 2u, 2u, 2u, 2u, 2u, 1u, 1u, 1u, 2u, 5u, 1u, 0u, 0u /*said*/, 2u, 5u, 5u, 5u
};


// ----- Helpers (alphabetical) -----

static const char *actionName(uint8_t op) {
    if (op == 0xFFu) {
        return "if";
    }
    if (op == 0xFEu) {
        return "goto";
    }
    if (kActionNames[op] != NULL) {
        return kActionNames[op];
    }
    return "???";
}


static uint16_t disasmAction(const uint8_t *code, uint16_t pc, uint16_t length, uint8_t op) {
    uint8_t   args;
    uint8_t   i;
    uint16_t  here;

    here = (uint16_t)(pc - 1u);
    args = kActionArgBytes[op];
    if (args == ACTION_UNKNOWN) {
        printf("  %04X: %02X            ???\n", (unsigned)here, (unsigned)op);
        return length;
    }
    if ((uint32_t)pc + (uint32_t)args > (uint32_t)length) {
        printf("  %04X: %02X  truncated args\n", (unsigned)here, (unsigned)op);
        return length;
    }
    printf("  %04X: %s", (unsigned)here, actionName(op));
    if (args == 0u) {
        printf("()\n");
    } else {
        printf("(");
        for (i = 0u; i < args; i++) {
            if (i > 0u) {
                printf(", ");
            }
            printf("%u", (unsigned)code[pc + i]);
        }
        printf(")\n");
    }
    return (uint16_t)(pc + args);
}


static uint16_t disasmIf(const uint8_t *code, uint16_t pc, uint16_t length) {
    uint16_t  here;
    uint8_t   testOp;
    bool      first;
    bool      notNext;
    bool      inOr;
    uint16_t  skip;
    uint16_t  target;

    here = (uint16_t)(pc - 1u);
    printf("  %04X: if (", (unsigned)here);
    first   = true;
    notNext = false;
    inOr    = false;
    for (;;) {
        if (pc >= length) {
            printf(" <truncated>)\n");
            return length;
        }
        testOp = code[pc];
        pc = (uint16_t)(pc + 1u);
        if (testOp == 0xFFu) {
            // End of test list.
            break;
        }
        if (testOp == 0xFCu) {
            if (inOr) {
                printf(")");
                inOr = false;
            } else {
                if (!first) {
                    printf(" && ");
                }
                printf("(");
                inOr  = true;
                first = true;
            }
            continue;
        }
        if (testOp == 0xFDu) {
            notNext = true;
            continue;
        }
        if (!first) {
            printf(inOr ? " || " : " && ");
        }
        first = false;
        if (notNext) {
            printf("!");
            notNext = false;
        }
        pc = disasmTest(code, pc, length);
    }
    if (inOr) {
        printf(")");
    }
    if ((uint32_t)pc + 2u > (uint32_t)length) {
        printf(") <truncated skip>\n");
        return length;
    }
    skip   = (uint16_t)(code[pc] | ((uint16_t)code[pc + 1u] << 8));
    target = (uint16_t)(pc + 2u + skip);
    printf(") -> %04X\n", (unsigned)target);
    return (uint16_t)(pc + 2u);
}


static uint16_t disasmTest(const uint8_t *code, uint16_t pc, uint16_t length) {
    uint8_t   args;
    uint8_t   i;
    uint8_t   testOp;
    uint8_t   wordCount;
    uint16_t  word;

    testOp = code[pc - 1u];
    if (testOp == 0u || testOp > MAX_TEST) {
        printf("???_test_0x%02X(", (unsigned)testOp);
        return pc;
    }
    if (testOp == 0x0Eu) {
        // said(word, word, ...)
        if (pc >= length) {
            printf("said(<truncated>");
            return length;
        }
        wordCount = code[pc];
        pc = (uint16_t)(pc + 1u);
        printf("said(");
        for (i = 0u; i < wordCount; i++) {
            if (i > 0u) {
                printf(", ");
            }
            if ((uint32_t)pc + 2u > (uint32_t)length) {
                printf("<truncated>");
                return length;
            }
            word = (uint16_t)(code[pc] | ((uint16_t)code[pc + 1u] << 8));
            printf("w%u", (unsigned)word);
            pc = (uint16_t)(pc + 2u);
        }
        printf(")");
        return pc;
    }
    args = kTestArgBytes[testOp];
    printf("%s(", testName(testOp));
    if ((uint32_t)pc + (uint32_t)args > (uint32_t)length) {
        printf("<truncated>)");
        return length;
    }
    for (i = 0u; i < args; i++) {
        if (i > 0u) {
            printf(", ");
        }
        printf("%u", (unsigned)code[pc + i]);
    }
    printf(")");
    return (uint16_t)(pc + args);
}


static const char *testName(uint8_t op) {
    if (op > MAX_TEST || kTestNames[op] == NULL) {
        return "???";
    }
    return kTestNames[op];
}


static int walk(const uint8_t *code, uint16_t length, uint16_t endPc) {
    uint16_t  pc;
    uint8_t   op;
    int16_t   gotoOff;
    uint16_t  target;

    pc = 0u;
    while (pc < length && pc < endPc) {
        op = code[pc];
        pc = (uint16_t)(pc + 1u);
        if (op == 0xFFu) {
            pc = disasmIf(code, pc, length);
            continue;
        }
        if (op == 0xFEu) {
            if ((uint32_t)pc + 2u > (uint32_t)length) {
                printf("  %04X: goto <truncated>\n", (unsigned)(pc - 1u));
                return 1;
            }
            gotoOff = (int16_t)(code[pc] | ((uint16_t)code[pc + 1u] << 8));
            target  = (uint16_t)((int32_t)pc + 2 + (int32_t)gotoOff);
            printf("  %04X: goto %04X\n", (unsigned)(pc - 1u), (unsigned)target);
            pc = (uint16_t)(pc + 2u);
            continue;
        }
        pc = disasmAction(code, pc, length, op);
        if (op == 0x00u) {
            // return -- end of logic. Keep walking past it; some
            // logics have unreachable handlers below the top-level
            // return that callers jump into via goto.
        }
    }
    return 0;
}


// ----- main -----

int main(int argc, char **argv) {
    AgiGameT   game;
    uint16_t   logicId;
    uint8_t   *bytes;
    uint16_t   length;
    uint16_t   bytecodeLen;
    uint16_t   endPc;
    int        ret;

    if (argc < 3 || argc > 4) {
        fprintf(stderr, "usage: %s <game-directory> <logic-id> [end-pc]\n", argv[0]);
        return 2;
    }
    if (!agiResOpen(&game)) {
        fprintf(stderr, "FAIL: agiResOpen rejected '%s'\n", argv[1]);
        return 1;
    }
    logicId = (uint16_t)atoi(argv[2]);
    if (logicId >= game.resCount[AGI_RES_LOGIC]) {
        fprintf(stderr, "FAIL: no LOGIC at id %u\n", (unsigned)logicId);
        agiResClose(&game);
        return 1;
    }
    bytes = agiResLoad(&game, AGI_RES_LOGIC, logicId, &length);
    if (bytes == NULL) {
        fprintf(stderr, "FAIL: LOGIC %u failed to load\n", (unsigned)logicId);
        agiResClose(&game);
        return 1;
    }
    if (length < AGI_LOGIC_HDR_BYTES) {
        fprintf(stderr, "FAIL: LOGIC %u truncated header\n", (unsigned)logicId);
        free(bytes);
        agiResClose(&game);
        return 1;
    }
    bytecodeLen = (uint16_t)(bytes[0] | ((uint16_t)bytes[1] << 8));
    if ((uint32_t)bytecodeLen + AGI_LOGIC_HDR_BYTES > (uint32_t)length) {
        fprintf(stderr, "FAIL: LOGIC %u bytecode length exceeds resource\n", (unsigned)logicId);
        free(bytes);
        agiResClose(&game);
        return 1;
    }
    endPc = (argc == 4) ? (uint16_t)atoi(argv[3]) : bytecodeLen;
    if (endPc > bytecodeLen) {
        endPc = bytecodeLen;
    }

    printf("LOGIC %u: %u bytes resource, %u bytes bytecode\n",
           (unsigned)logicId, (unsigned)length, (unsigned)bytecodeLen);
    ret = walk(bytes + AGI_LOGIC_HDR_BYTES, bytecodeLen, endPc);
    free(bytes);
    agiResClose(&game);
    return ret;
}
