#!/usr/bin/env bash
# probeLocals.sh - end-to-end validation harness for pc2line.py --locals.
#
# Phase 3.2 slice 2 smoke probe:
#   1. Compile a probe C file (4 i16 locals + a sentinel store at $025000)
#      with -O0 -g.
#   2. Link with crt0+libgcc, produce DWARF sidecar + map.
#   3. Load the .bin into MAME, run until the sentinel store fires
#      (poll bank-2 $5000 for 0xC0DE).
#   4. Snapshot the S register at that point and the stack memory
#      around it.
#   5. Call pc2line.py --locals with the captured S; verify each
#      reported ADDR= holds the expected constant (0xABCD/0x1234/0x5678).
#
# Exit 0 if at least the first variable's resolved ADDR yields the
# expected value (matches the smoke gate the plan asks for: "asserts
# at least one of x/y/z resolves correctly").  Exit non-zero on any
# build/link failure or MAME read mismatch.
#
# Usage: probeLocals.sh [--verbose]
set -euo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
ROOT="$(cd "$HERE/.." && pwd)"
VERBOSE=0
if [ "${1:-}" = "--verbose" ]; then
    VERBOSE=1
fi

CLANG="$ROOT/tools/llvm-mos-build/bin/clang"
LLVMMC="$ROOT/tools/llvm-mos-build/bin/llvm-mc"
LINK="$ROOT/tools/link816"

if [ ! -x "$CLANG" ] || [ ! -x "$LLVMMC" ] || [ ! -x "$LINK" ]; then
    echo "probeLocals: missing toolchain (clang/llvm-mc/link816)" >&2
    exit 2
fi
if ! command -v mame >/dev/null 2>&1; then
    echo "probeLocals: mame not on PATH; skipping" >&2
    exit 77   # autotools-style "skip"
fi

WORK="$(mktemp -d)"
trap 'rm -rf "$WORK"' EXIT
CFILE="$WORK/loctest.c"
OFILE="$WORK/loctest.o"
OCRT0="$WORK/crt0.o"
OLIBGCC="$WORK/libgcc.o"
BIN="$WORK/loctest.bin"
MAP="$WORK/loctest.map"
DWARF="$WORK/loctest.dwarf"
LUA="$WORK/loctest.lua"
OUT="$WORK/loctest.out"

cat > "$CFILE" <<'EOF'
int main(void) {
    int x = 0xABCD;
    int y = 0x1234;
    int z = 0x5678;
    *(volatile unsigned short *)0x025000 = 0xC0DE;
    while (1) { }
    return 0;
}
EOF

"$CLANG" --target=w65816 -O0 -g -ffunction-sections \
    -c "$CFILE" -o "$OFILE" 2>/dev/null
"$LLVMMC" -arch=w65816 -filetype=obj \
    "$ROOT/runtime/src/crt0.s" -o "$OCRT0" 2>/dev/null
"$LLVMMC" -arch=w65816 -filetype=obj \
    "$ROOT/runtime/src/libgcc.s" -o "$OLIBGCC" 2>/dev/null
"$LINK" -o "$BIN" --text-base 0x1000 \
    --map "$MAP" --debug-out "$DWARF" \
    "$OCRT0" "$OFILE" "$OLIBGCC" >/dev/null 2>&1 || true

[ -s "$BIN" ]   || { echo "probeLocals: link produced empty .bin"; exit 1; }
[ -s "$DWARF" ] || { echo "probeLocals: link produced empty DWARF sidecar"; exit 1; }

MAIN_PC=$(awk '$2 == "main" { print $1; exit }' "$MAP")
[ -n "$MAIN_PC" ] || { echo "probeLocals: no 'main' symbol in map"; exit 1; }

# Lua: load .bin at $001000, kick PC, then poll bank-2 $5000 for the
# sentinel value 0xC0DE.  When the sentinel fires, snapshot S + PC and
# the stack memory in the surrounding 64-byte window.  Print everything
# on MAME- prefixed lines so the host script can grep them.
cat > "$LUA" <<EOF
local frame = 0
local loaded = false
local captured = 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"); 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
        cpu.state["PC"].value = 0x1000
        cpu.state["PB"].value = 0
        cpu.state["DB"].value = 0
        cpu.state["D"].value  = 0
        cpu.state["P"].value  = 0x34
        cpu.state["E"].value  = 0
        cpu.state["S"].value  = 0x01FF
        loaded = true
        print("MAME-LOADED bytes=" .. #data)
    end
    if loaded and not captured and frame > 35 then
        local cpu = manager.machine.devices[":maincpu"]
        local mem = cpu.spaces["program"]
        local sentinel = mem:read_u16(0x025000)
        if sentinel == 0xC0DE then
            local sp = cpu.state["S"].value
            local pc = cpu.state["PC"].value
            print(string.format("MAME-SENTINEL val=0x%04x", sentinel))
            print(string.format("MAME-S val=0x%04x", sp))
            print(string.format("MAME-PC val=0x%06x", pc))
            -- Dump 64 bytes of stack around S (sp+0 .. sp+63) as u16
            -- words.  pc2line.py addresses we'll evaluate land in this
            -- window (fbreg offsets are at most 24 for this probe).
            for ofs = 0, 32 do
                local addr = sp + ofs
                local v = mem:read_u16(addr)
                print(string.format("MAME-STACK addr=0x%06x val=0x%04x",
                                    addr, v))
            end
            captured = true
            manager.machine:exit()
        end
    end
    if frame >= 240 then
        print("MAME-TIMEOUT")
        manager.machine:exit()
    end
end)
EOF

SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy timeout 30 \
    mame apple2gs -rompath "$ROOT/tools/mame/roms" \
    -plugins -autoboot_script "$LUA" \
    -video none -sound none -nothrottle -seconds_to_run 6 2>&1 \
    | grep "^MAME-" > "$OUT" || true

if [ "$VERBOSE" -eq 1 ]; then
    cat "$OUT" >&2
fi

if grep -q "^MAME-TIMEOUT$" "$OUT"; then
    echo "probeLocals: timed out before sentinel fired" >&2
    exit 1
fi

SP_HEX=$(awk -F= '/^MAME-S val=/ {print $NF; exit}' "$OUT")
PC_HEX=$(awk -F= '/^MAME-PC val=/ {print $NF; exit}' "$OUT")
SENTINEL=$(awk -F= '/^MAME-SENTINEL val=/ {print $NF; exit}' "$OUT")

if [ -z "$SP_HEX" ] || [ -z "$PC_HEX" ]; then
    echo "probeLocals: no S/PC snapshot captured" >&2
    exit 1
fi
if [ "$SENTINEL" != "0xc0de" ]; then
    echo "probeLocals: sentinel mismatch ($SENTINEL)" >&2
    exit 1
fi

echo "probeLocals: sentinel fired, S=$SP_HEX PC=$PC_HEX"

# Call pc2line.py --locals with the captured S.  Expect three variables
# (the DW_TAG_variable DIEs for x, y, z) each with an ADDR= field.
LOCALS=$(python3 "$HERE/pc2line.py" --sidecar "$DWARF" --map "$MAP" \
                 --locals --sp "$SP_HEX" "$PC_HEX")
echo "$LOCALS"

# For each ADDR= line, read the stored value from the snapshot and
# compare against the set of expected constants.  At least one must
# match (the slice gate).
EXPECTED=(abcd 1234 5678)
hits=0
while IFS= read -r line; do
    addr_hex=$(echo "$line" | sed -nE 's/.* ADDR=0x([0-9a-fA-F]+).*/\1/p')
    if [ -z "$addr_hex" ]; then
        continue
    fi
    addr_norm=$(printf "0x%06x" "0x$addr_hex" 2>/dev/null || echo "")
    # Find that addr in MAME-STACK lines.
    snap=$(awk -F= -v want="$addr_norm" '
        /^MAME-STACK addr=/ {
            split($2, parts, " ")
            a = parts[1]
            v = $NF
            if (a == want) { print v; exit }
        }' "$OUT")
    if [ -z "$snap" ]; then
        continue
    fi
    snap_lc=$(echo "$snap" | tr 'A-Z' 'a-z' | sed 's/^0x//')
    for exp in "${EXPECTED[@]}"; do
        if [ "$snap_lc" = "$exp" ]; then
            echo "probeLocals: HIT addr=$addr_norm value=0x$snap_lc"
            hits=$((hits + 1))
            break
        fi
    done
done <<< "$LOCALS"

if [ "$hits" -lt 1 ]; then
    echo "probeLocals: FAIL: no variable's --locals ADDR resolved to a known constant" >&2
    exit 1
fi
echo "probeLocals: OK ($hits/3 variables resolved correctly)"
exit 0
