"""D3 Q3: (a) where the lifecycle atoms are parsed AT ALL, (b) who reads the state
slots on the item record, (c) who writes the pile slot +0x60.

ESTABLISHED (q1/q2 + live, controls passed): record_off = 0x188 - N for the deser's
stack struct; itemState -> +0x5c (free==1 live), owners -> +0x48 (1 live),
!untradeable -> +0x49 (0 live), discardValue sent -> +0x38, computed -> +0x3c,
vtable -> +0x70, pile -> +0x60 (1 club / 6 purchased, NOT from the wire).

(a) THE ABSENCE QUESTION, done soundly. Every SAX deserializer in this DLL ends its
key loop with the value-SKIP FUN_180135ff0 as the default arm. So the set of callers of
FUN_180135ff0 IS the set of deserializers -- a bounded, enumerable population. We
decompile each and look for the lifecycle atoms in ALL dispatch forms at once by
matching the decompiler's own text: `case 0xNNN:`, `== 0xNNN`, `!= 0xNNN`, and
`< 0xNNN` / `- 0xNNN` ladder steps.
CONTROL: the same scan must find 0xd7 (discardValue) and 0x172 (itemState) inside
FUN_18013fe00, both of which we have already read with our own eyes as `case` labels.
If those two do not come back, the scan is broken and no absence below counts.

(b) publisher hunt: group every function by the set of register+displacement memory
operands it uses; a function that touches 0x38/0x3c/0x5c/0x49/0x60 together is reading
the item record.
CONTROL: the same grouping must rediscover FUN_18013fe00 itself as a heavy toucher.

(c) pile writers: instructions storing an immediate 1 or 6 into [reg+0x60].
"""
import traceback, os, re
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)

LIFE = {0x172: "itemState", 0x226: "pile", 0x227: "pileSizeClientData", 0x228: "pileType",
        0x207: "owners", 0x361: "untradeable", 0x362: "untradeableCount",
        0x331: "tradeId", 0x332: "tradepile", 0x333: "tradePile",
        0x19b: "loans", 0x16f: "itemLoans", 0xed: "duplicateItemLoans",
        0x335: "tradeState", 0x1c0: "maximumTradePileSize",
        0xd7: "discardValue(CONTROL)"}

PAT = {a: re.compile(r"(case %s:|== %s\b|!= %s\b|< %s\b|- %s\b|\+ %s\b)"
                     % tuple([hex(a)] * 6)) for a in LIFE}


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 + "q3_%s_%x.c" % (tag, va), "w") as fh:
        fh.write(src)
    return src


try:
    print("###### A. deserializer census (callers of the value-SKIP FUN_180135ff0)")
    ents = sorted({e for (_, _, _, e) in xrefs_to(0x180135ff0) if e})
    print("  deserializer population: %d functions" % len(ents))
    rows = []
    for i, e in enumerate(ents):
        try:
            src = dec(e, 120)
        except Exception as ex:
            print("  dec failed %#x %s" % (e, ex))
            continue
        hit = {}
        for a, nm in LIFE.items():
            m = PAT[a].findall(src)
            if m:
                hit[a] = len(m)
        if hit:
            rows.append((e, fname(e), len(src), hit))
    print("  functions mentioning at least one lifecycle atom: %d" % len(rows))
    for e, nm, ln, hit in rows:
        print("  %#x %-18s len=%-7d %s"
              % (e, nm, ln, ", ".join("%s(0x%x)x%d" % (LIFE[a], a, n)
                                      for a, n in sorted(hit.items()))))
    print()
    print("  --- per-atom summary over the whole deserializer population")
    for a, nm in sorted(LIFE.items()):
        fs = [(e, n) for e, n, _, h in rows if a in h]
        print("  0x%-4x %-24s parsed in %d deserializer(s): %s"
              % (a, nm, len(fs), ", ".join("%s(%#x)" % (n, e) for e, n in fs) or "NONE"))

    print()
    print("###### B. publisher hunt: displacement fingerprints over the whole .text")
    disp_of = defaultdict(set)
    count_of = defaultdict(lambda: defaultdict(int))
    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:
                continue
            if 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:
                        f = fm.getFunctionContaining(ins.getAddress())
                        if f is None:
                            break
                        f = int(f.getEntryPoint().getOffset())
                    disp_of[f].add(v)
                    count_of[f][v] += 1
    print("  scanned %d instructions, %d functions with reg+disp operands" % (tot, len(disp_of)))
    KEY = {0x38, 0x3c, 0x49, 0x5c, 0x60}
    cands = [(len(KEY & d), e, sorted(KEY & d)) for e, d in disp_of.items() if len(KEY & d) >= 3]
    cands.sort(reverse=True)
    print("  functions touching >=3 of {0x38,0x3c,0x49,0x5c,0x60}: %d" % len(cands))
    for k, e, s in cands[:60]:
        print("    %#x %-18s %d/%d  %s" % (e, fname(e), k, len(KEY), [hex(x) for x in s]))
    print("  CONTROL: is FUN_18013fe00 in the fingerprint map?",
          0x18013fe00 in disp_of,
          sorted(hex(x) for x in (KEY & disp_of.get(0x18013fe00, set()))))

    print()
    print("###### C. every function whose displacement set contains 0x5c AND 0x49")
    for e, d in sorted(disp_of.items()):
        if 0x5c in d and 0x49 in d:
            print("    %#x %-18s" % (e, fname(e)))

    print()
    print("###### D. small getters: functions <= 0x20 bytes that read [reg+0x5c] or [reg+0x49] or [reg+0x60]")
    for e, d in sorted(disp_of.items()):
        f = func(e)
        if f is None:
            continue
        sz = int(f.getBody().getNumAddresses())
        if sz <= 0x20 and d & {0x49, 0x5c, 0x60, 0x48}:
            print("    %#x %-18s size=%d disp=%s" % (e, fname(e), sz, sorted(hex(x) for x in d)))

    print()
    print("###### E. writers of the pile slot: STORE of imm into [reg+0x60]")
    it = listing.getInstructions(True)
    while it.hasNext():
        ins = it.next()
        s = str(ins)
        if "+ 0x60]" in s and ins.getMnemonicString().upper() == "MOV" and s.rstrip().endswith((",0x1", ",0x6", ",0x2", ",0x3", ",0x4", ",0x5", ",0x0")):
            if "RSP" in s or "RBP" in s:
                continue
            f = fm.getFunctionContaining(ins.getAddress())
            print("    %#x  %-46s in %s" % (int(ins.getAddress().getOffset()), s,
                                            f.getName() if f else "?"))

    print()
    print("###### F. the two byte-0x49 comparators found in q2")
    for tag, va in (("cmp49_a", 0x1801a7260), ("cmp49_b", 0x1801a8940), ("wr49", 0x180130d10)):
        dump(tag, va)

except Exception:
    traceback.print_exc()
