"""D3 Q8: close out the lifecycle table -- is forSale(5)/offered(6) ever tested, and
what is the item-record pile (+0x60) vocabulary?

ESTABLISHED SO FAR. itemState lands at item+0x5c; 0x64..0x68 publish the Flash boolean
IS_ACTIVE (three independent publishers agree, one of them as the range test
`state - 100 < 5`); the equip path FUN_180113870 writes 1 back into +0x5c when it
unequips and 0x67 when it equips; the squad code FUN_1801b3640 accepts state 1 and
state 2. The earlier precise scan reported the constants compared against [reg+0x5c]
anywhere in the DLL as {-1,0,1,2,3,100,101,102,103,104} -- 5 and 6 absent -- but that
population mixes several unrelated structs, so this query re-runs it printing EVERY hit
with its function, and adds the `SUB/DEC ladder` and `switch jump table` forms that a
plain compare scan cannot see.

CONTROL: the same three forms, pointed at [reg+0x60], must rediscover the values we
have already measured live in the item record (1 for club items, 6 for purchased) and
the value 4 we just read in FUN_1801c3480. If 1/4/6 do not come out of the scan it is
not seeing item-record pile tests and its silence proves nothing.
"""
import traceback, os
from collections import defaultdict

OUT = "/tmp/claude-1000/-home-alex-Documents-OpenFUT/8e521ca1-ca3e-4138-bb96-df1744dd1d30/scratchpad/cards/"
os.makedirs(OUT, exist_ok=True)


def dump(tag, va, full=7000):
    f = func(va)
    if f is None:
        print("%s %#x -> NO FUNCTION" % (tag, va))
        return ""
    src = dec(va)
    print("=" * 78)
    print("%s  %#x  %s  len(src)=%d" % (tag, va, f.getName(), len(src)))
    print("=" * 78)
    with open(OUT + "q8_%s_%x.c" % (tag, va), "w") as fh:
        fh.write(src)
    print(src if len(src) <= full else "  [long; written to file]")
    return src


try:
    for DISP in (0x5c, 0x60):
        print()
        print("###### displacement %#x : every compare / ladder / jump-table dispatch" % DISP)
        it = listing.getInstructions(True)
        window = []
        seen = []
        while it.hasNext():
            ins = it.next()
            window.append(ins)
            if len(window) > 16:
                window.pop(0)
            s0 = str(window[0])
            if ("+ %#x]" % DISP) not in s0 or "RSP" in s0 or "RBP" in s0:
                continue
            mn = window[0].getMnemonicString().upper()
            f = fm.getFunctionContaining(window[0].getAddress())
            fn = f.getName() if f else "?"
            a0 = int(window[0].getAddress().getOffset())
            if mn == "CMP":
                try:
                    v = int(window[0].getOpObjects(1)[0].getValue())
                except Exception:
                    v = None
                seen.append((a0, fn, "CMP", v, s0))
                continue
            if mn not in ("MOV", "MOVZX", "MOVSX", "MOVSXD"):
                continue
            try:
                dst = str(window[0].getOpObjects(0)[0])
            except Exception:
                continue
            key = dst.replace("R", "E")
            run = 0
            for nxt in window[1:]:
                sn = str(nxt)
                m2 = nxt.getMnemonicString().upper()
                if key not in sn.replace("R", "E"):
                    continue
                if m2 in ("CMP", "TEST"):
                    try:
                        v = int(nxt.getOpObjects(1)[0].getValue())
                    except Exception:
                        v = None
                    seen.append((a0, fn, m2, v, s0 + " ; " + sn))
                    break
                if m2 in ("SUB", "DEC", "ADD", "INC"):
                    try:
                        d = 1 if m2 == "DEC" else (-1 if m2 == "INC"
                                                   else int(nxt.getOpObjects(1)[0].getValue()))
                    except Exception:
                        break
                    run += d if m2 in ("SUB", "DEC") else -d
                    seen.append((a0, fn, "LADDER@%d" % run, run, s0 + " ; " + sn))
                    continue
                if m2 == "JMP":
                    seen.append((a0, fn, "JUMPTABLE", None, s0 + " ; " + sn))
                    break
        print("  %d hits" % len(seen))
        by_fn = defaultdict(list)
        for a0, fn, kind, v, txt in seen:
            by_fn[fn].append((a0, kind, v, txt))
        for fn in sorted(by_fn):
            vals = sorted({v for _, _, v, _ in by_fn[fn] if v is not None})
            print("  %-22s n=%-3d constants=%s" % (fn, len(by_fn[fn]), vals))
        allv = sorted({v for _, _, _, v, _ in seen if v is not None})
        print("  ALL CONSTANTS for disp %#x: %s" % (DISP, allv))
        if DISP == 0x60:
            print("  CONTROL -- are the live-measured pile values 1, 4 and 6 present? %s"
                  % {k: (k in allv) for k in (1, 4, 6)})
        if DISP == 0x5c:
            print("  are forSale(5) and offered(6) present? %s"
                  % {k: (k in allv) for k in (5, 6)})
            for a0, fn, kind, v, txt in seen:
                if v in (5, 6, 3, 0xff, -1):
                    print("    %#x %-22s %s %s | %s" % (a0, fn, kind, v, txt))

    print()
    print("###### the remaining itemState readers")
    for tag, va in (("s_18011dc50", 0x18011dc50), ("s_180094ae0", 0x180094ae0)):
        dump(tag, va)
except Exception:
    traceback.print_exc()
