"""D3 Q4: who CONSUMES the state slots, and the transfer/quick-sell vocabulary.

The q3 publisher hunt had a control that came back empty for a reason I can state:
FUN_18013fe00 builds the record on the STACK (RSP-relative), and I had excluded
RSP/RBP bases, so the deser cannot appear. That control was therefore uninformative,
not passed. Here the fingerprint is rebuilt around offsets that are only meaningful on
a fully built item record and that a consumer must reach through a register:
    0x3c discardValue-computed, 0xb4 rating, 0x146 preferredPosition, 0x148 nation,
    0x154 leagueId, 0x94 teamid, 0x5c itemState, 0x49 tradeable-flag, 0x60 pile
CONTROL for this scan: it must rediscover functions that touch 0xb4 AND 0x146 AND
0x148 together, because the UI certainly draws rating, position and nation from the
same object. Zero such functions => the scan is broken and nothing below counts.

Also decompiled in full:
  FUN_1800515e0  touches all five of {0x38,0x3c,0x49,0x5c,0x60} -- candidate item
                 copy-constructor, which if true independently re-derives the layout
  FUN_180130d10  the untradeableCount deser that WRITES [reg+0x49]
  FUN_180128600 / FUN_180128e30   the only two deserializers that parse `pile` 0x226
  FUN_18013e410  tradeId 0x331 + tradeState 0x335
  FUN_180148b70  the second `untradeable` 0x361 arm
  FUN_180138e10  itemLoans 0x16f + duplicateItemLoans 0xed

And a literal hunt for the transfer-market / quick-sell vocabulary with xrefs.
"""
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, echo=True):
    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  (FULL)" % (tag, va, f.getName(), len(src)))
    print("=" * 78)
    if echo:
        print(src)
    with open(OUT + "q4_%s_%x.c" % (tag, va), "w") as fh:
        fh.write(src)
    return src


try:
    print("###### A. item-record consumer fingerprint over the whole .text")
    disp_of = defaultdict(set)
    it = listing.getInstructions(True)
    tot = 0
    while it.hasNext():
        ins = it.next()
        tot += 1
        f = None
        for i in range(ins.getNumOperands()):
            try:
                objs = ins.getOpObjects(i)
            except Exception:
                continue
            if len(objs) < 2:
                continue
            regs = [o for o in objs if hasattr(o, "getName")]
            if not regs or any(str(r) in ("RSP", "RBP", "ESP", "EBP") for r in regs):
                continue
            for o in objs:
                try:
                    v = int(o.getValue())
                except Exception:
                    continue
                if 0 <= v <= 0x200:
                    if f is None:
                        ff = fm.getFunctionContaining(ins.getAddress())
                        if ff is None:
                            break
                        f = int(ff.getEntryPoint().getOffset())
                    disp_of[f].add(v)
    print("  scanned %d instructions" % tot)
    CTRL = {0xb4, 0x146, 0x148}
    ctrl_fns = [e for e, d in disp_of.items() if CTRL <= d]
    print("  CONTROL {0xb4,0x146,0x148} all present in %d functions: %s"
          % (len(ctrl_fns), [("%s(%#x)" % (fname(e), e)) for e in sorted(ctrl_fns)][:20]))
    ITEM = {0x3c, 0xb4, 0x146, 0x148, 0x154, 0x94, 0x5c, 0x49, 0x60, 0x38, 0x50, 0x58}
    scored = sorted(((len(ITEM & d), e, sorted(ITEM & d)) for e, d in disp_of.items()),
                    reverse=True)
    print("  top item-record consumers by fingerprint overlap:")
    for k, e, s in scored[:40]:
        if k < 5:
            break
        print("    %2d/%d  %#x %-20s %s" % (k, len(ITEM), e, fname(e), [hex(x) for x in s]))
    print("  of those, the ones that ALSO touch 0x5c or 0x49:")
    for k, e, s in scored[:200]:
        if k < 4:
            break
        if 0x5c in s or 0x49 in s:
            print("    %2d/%d  %#x %-20s %s" % (k, len(ITEM), e, fname(e), [hex(x) for x in s]))

    print()
    print("###### B. full decompiles")
    for tag, va in (("copyctor_cand", 0x1800515e0),
                    ("untradeableCount_deser", 0x180130d10),
                    ("pile_deser_a", 0x180128600),
                    ("pile_deser_b", 0x180128e30),
                    ("tradeId_tradeState_deser", 0x18013e410),
                    ("untradeable_2nd", 0x180148b70),
                    ("itemLoans_deser", 0x180138e10)):
        dump(tag, va)

    print()
    print("###### C. transfer / sell / list vocabulary in .rdata, with xrefs")
    NEEDLES = [b"TRANSFER", b"Transfer", b"transfer", b"QUICK_SELL", b"QuickSell",
               b"quickSell", b"DISCARD", b"Discard", b"TRADEABLE", b"tradeable",
               b"Tradeable", b"UNTRADEABLE", b"LIST_ON", b"tradepile", b"TRADE_PILE",
               b"canBeSold", b"isTradeable", b"AUCTION", b"auctionhouse"]
    seen = set()
    for nd in NEEDLES:
        hits = find_all(nd, blocks=(".rdata", ".data"))
        print("  needle %-14s hits=%d" % (nd.decode(), len(hits)))
        for h in hits[:80]:
            # back up to the start of the C string
            st = h
            for _ in range(96):
                try:
                    if mem.getByte(addr(st - 1)) == 0:
                        break
                except Exception:
                    break
                st -= 1
            if st in seen:
                continue
            seen.add(st)
            s = rd_str(st, 120)
            xs = xrefs_to(st)
            fns = sorted({(fn, ent) for _, _, fn, ent in xs if ent})
            print("     %#x %-58r xrefs=%d %s" % (st, s, len(xs),
                                                  ["%s(%#x)" % (n, e) for n, e in fns][:6]))
except Exception:
    traceback.print_exc()
