#!/usr/bin/env bash
# Run a 65816 binary in MAME after pre-loading a stub GS/OS
# dispatcher at $E100A8.  This lets the smoke test exercise the
# wrapper-to-dispatcher contract end-to-end without needing a
# real GS/OS-bootable disk image.
#
# The stub is the assembled bytes of /tmp/gsosStub.s (rebuilt
# from runtime/src/iigsGsosStub.s as part of this script — kept
# in-tree so it's reproducible).  It writes byte $42 to *parm
# and returns A=0 (success) for any call number.  That is enough
# to verify the wrapper:
#   1. pushes the parm-block low ptr (PHA),
#   2. pushes a zero bank (PEA 0),
#   3. JSLs $E100A8 with the right return convention,
#   4. returns A correctly to the caller.
#
# Usage: runInMameWithGsosStub.sh <binary> <addr> <expected>
#        runInMameWithGsosStub.sh <binary> --check <addr>=<exp>...

set -euo pipefail
source "$(dirname "$0")/common.sh"

BIN="$1"
shift
CHECK_FRAME=${MAME_CHECK_FRAME:-300}
SECS=${MAME_SECS:-6}

# 57-byte stub bytes (see runtime/src/iigsGsosStub.s for source).
# The new iigsGsos.s wrappers use INLINE form (callNum + pBlock LONG
# follow the JSL as 6 inline bytes), and the dispatcher bumps the
# return PC by +6 to skip them.  This stub mirrors that contract:
#   1.  PHP / PHA / PHY (5 bytes pushed total)
#   2.  Read return PC (16-bit) from S+6, PBR from S+8 into $E4..$E6
#   3.  Long-indirect-Y read pBlock LONG from inline data:
#         offset = [$E4+2], bank+pad = [$E4+4]  (callNum is at +0..+1)
#   4.  *(pBlock) = $42 via [$E8],y long-indirect
#   5.  Bump return PC by +6 so caller's RTL skips inline operands
#   6.  Restore Y/A/P; return A=0 (success)
# Regenerate on stub changes via:
#   llvm-mc -arch=w65816 -filetype=obj runtime/src/iigsGsosStub.s -o /tmp/s.o
#   llvm-objcopy --dump-section=.text=/tmp/s.bin /tmp/s.o
#   xxd -p /tmp/s.bin | head -1   (then trim trailing __gsosIsRealImpl word)
STUB_HEX="08c2 3048 5aa3 0685 e4e2 20a3 0885 e6c2 20a0 0300 b7e4 85e8 a005 00b7 e485 eaa0 0000 e220 a942 97e8 c220 a306 1869 0600 8306 7a68 28a9 0000 6b"

LUA_CHECKS=""
EXPECT_LIST=()
ADDR_LIST=()
if [ "$1" = "--check" ]; then
    shift
    for pair in "$@"; do
        ADDR="${pair%=*}"
        EXP="${pair#*=}"
        ADDR_LIST+=("$ADDR")
        EXPECT_LIST+=("$EXP")
        LUA_CHECKS="$LUA_CHECKS print(string.format('MAME-READ addr=0x%06x val=0x%04x', $ADDR, mem:read_u16($ADDR)))"$'\n'
    done
else
    ADDR="$1"
    EXP="$2"
    ADDR_LIST+=("$ADDR")
    EXPECT_LIST+=("$EXP")
    LUA_CHECKS="print(string.format('MAME-READ addr=0x%06x val=0x%04x', $ADDR, mem:read_u16($ADDR)))"
fi

[ -f "$BIN" ] || die "binary not found: $BIN"
LUA_PATH=$(mktemp --suffix=.lua)
trap 'rm -f "$LUA_PATH"' EXIT

# Build the stub-write Lua statement (one mem:write_u8 per byte).
STUB_BYTES=$(echo "$STUB_HEX" | tr -d ' ')
STUB_LUA=""
i=0
while [ $i -lt ${#STUB_BYTES} ]; do
    byte="${STUB_BYTES:$i:2}"
    addr=$(( 0xe100a8 + i / 2 ))
    STUB_LUA="${STUB_LUA}        mem:write_u8(${addr}, 0x${byte})"$'\n'
    i=$(( i + 2 ))
done

cat > "$LUA_PATH" <<EOF
local frame = 0
local loaded = false
emu.register_frame_done(function()
    frame = frame + 1
    if frame == 30 and not loaded then
        local cpu = manager.machine.devices[":maincpu"]
        local mem = cpu.spaces["program"]
        local f = io.open("$BIN", "rb")
        if not f then print("BIN-MISSING"); manager.machine:exit(); return end
        local data = f:read("*all"); f:close()
        for i = 1, #data do
            local addr = 0x001000 + i - 1
            if not (addr >= 0x00C000 and addr < 0x00D000) then
                mem:write_u8(addr, data:byte(i))
            end
        end
        -- Install GS/OS dispatcher stub at \$E100A8 (bank E1 RAM).
$STUB_LUA
        loaded = true
        cpu.state["PC"].value = 0x1000
        cpu.state["PB"].value = 0x00
        cpu.state["DB"].value = 0x00
        cpu.state["D"].value  = 0x00
        cpu.state["P"].value  = 0x34
        cpu.state["E"].value  = 0
        cpu.state["S"].value  = 0x01FF
        print("MAME-LOADED bytes=" .. #data .. " stub=$((${#STUB_BYTES}/2))")
    end
    if frame == $CHECK_FRAME then
        local cpu = manager.machine.devices[":maincpu"]
        local mem = cpu.spaces["program"]
$LUA_CHECKS
        manager.machine:exit()
    end
end)
EOF

OUT=$(SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy timeout 30 mame apple2gs \
    -rompath "$PROJECT_ROOT/tools/mame/roms" \
    -plugins -autoboot_script "$LUA_PATH" \
    -video none -sound none -nothrottle -seconds_to_run "$SECS" 2>&1 | grep "^MAME-")

echo "$OUT"
mapfile -t GOT_LIST < <(printf '%s\n' "$OUT" | grep -oE 'val=0x[0-9a-f]+' | sed 's/val=0x//')
ok=1
for i in "${!EXPECT_LIST[@]}"; do
    if [ "${GOT_LIST[$i]:-}" != "${EXPECT_LIST[$i]}" ]; then
        warn "MAME mismatch at ${ADDR_LIST[$i]}: got 0x${GOT_LIST[$i]:-MISSING} expected 0x${EXPECT_LIST[$i]}"
        ok=0
    fi
done
if [ $ok -eq 1 ]; then
    log "MAME (gsos-stub) OK: ${#EXPECT_LIST[@]} reads matched"
    exit 0
fi
exit 1
