#!/usr/bin/env bash
# Run a 65816 binary inside MAME's apple2gs simulation.
#
# Usage:
#   runInMame.sh <binary> <addr> <expected>
#       Read one 16-bit value at addr, compare to expected.
#   runInMame.sh <binary> --check <addr1>=<exp1> [<addr2>=<exp2> ...]
#       Read multiple 16-bit values, all must match.
#   runInMame.sh <binary> --check-u8 <addr1>=<exp1> [<addr2>=<exp2> ...]
#       Read multiple 8-bit (byte) values, all must match.  Required by
#       SHR pixel probes (sprite/desktop work) where the unit of truth is
#       a single $E1:9D00..$E1:9FFF byte, not a 16-bit word.
#
# Addresses can be 24-bit (e.g., "0x025000" for bank 2 offset $5000).
# Expected values are 4-hex (--check) or 2-hex (--check-u8), no 0x prefix.
#
# Code loads at $00:1000 in bank 0 RAM.  Code can switch DBR to bank
# 2+ for safe data writes (bank 0 zero page is scribbled by IIgs ROM
# during execution).
#
# Exit 0 if all reads match, 1 otherwise.

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

BIN="$1"
shift
# Frame budget: load at frame 30, check at CHECK_FRAME (default 300 = 4.5
# simulated seconds after load).  Override via env for heavy-compute tests.
# Earlier default was 60 frames (0.5 sec), which falsely flagged slow but
# correct math (e.g. 6-iter sqrt with chained soft-double libcalls) as
# runtime hangs — see feedback_sqrt_runtime_broken.md.
CHECK_FRAME=${MAME_CHECK_FRAME:-300}
# seconds_to_run is simulated time; MAME terminates at this point.  Sized
# to comfortably exceed CHECK_FRAME (300 frames = 5 sec at 60Hz).
SECS=${MAME_SECS:-6}

# Build address list as Lua table entries.  Two width modes: 16-bit
# (default --check) and 8-bit (--check-u8).  The width determines both
# the Lua read function (read_u16 vs read_u8) and the printf format
# (%04x vs %02x) so the post-run parser sees consistent widths.
LUA_CHECKS=""
EXPECT_LIST=()
ADDR_LIST=()
EXPECT_WIDTH=4   # hex digits per expected value
if [ "$1" = "--check" ] || [ "$1" = "--check-u8" ]; then
    MODE="$1"
    shift
    if [ "$MODE" = "--check-u8" ]; then
        LUA_READ="mem:read_u8"
        LUA_FMT="%02x"
        EXPECT_WIDTH=2
    else
        LUA_READ="mem:read_u16"
        LUA_FMT="%04x"
    fi
    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$LUA_FMT', $ADDR, $LUA_READ($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

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()
        -- Load at \$00:1000 (bank 0).  PB stays at \$00 — MAME's
        -- apple2gs CPU model doesn't honor a Lua-side PB!=0 set.
        -- The user's code can switch DBR to bank 2+ for safe data
        -- writes (bank 2 is clear of IIgs ROM IRQ scribbling).
        -- Skip writes that would land in the IIgs IO window
        -- (\$C000-\$CFFF).  link816 may pad this range with zeros
        -- when rodata auto-skips it, and writing zeros into soft
        -- switches could clobber IO state (e.g., the LC1 RAM enable
        -- that crt0 sets up).
        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
        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   -- M=1, X=1, I=1 (IRQ off)
        cpu.state["E"].value  = 0
        cpu.state["S"].value  = 0x01FF
        print("MAME-LOADED bytes=" .. #data)
    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"
# Parse all val=... and compare to expected list.  MAME's Lua prints
# zero-padded lowercase hex (%02x for u8, %04x for u16); normalize the
# user-supplied expected to the same width so callers can write "5"
# instead of "05" for u8 probes.
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
    want=$(printf "%0${EXPECT_WIDTH}x" "0x${EXPECT_LIST[$i]}" 2>/dev/null || printf '%s' "${EXPECT_LIST[$i]}")
    if [ "${GOT_LIST[$i]:-}" != "$want" ]; then
        warn "MAME mismatch at ${ADDR_LIST[$i]}: got 0x${GOT_LIST[$i]:-MISSING} expected 0x$want"
        ok=0
    fi
done
if [ $ok -eq 1 ]; then
    log "MAME OK: ${#EXPECT_LIST[@]} reads matched"
    exit 0
fi
exit 1
