"""D3 Q1: the itemState vocabulary and the lifecycle-field arms in the shared item deser.

HYPOTHESIS: FUN_18013fe00 (shared ITEM element deser) has an arm for atom 0x172
(itemState) that reads a STR and maps it through the enum table documented at
0x180229d20 (stride 0x10: WAITING_FOR_GAME, inGame, forSale, offered, activeBadge,
activeHomeKit, activeAwayKit, activeBall, activeStadium, active).  That documented
list omits "free", which we send on every card, so either the table is longer than
recorded or "free" is the default/no-match value.  Also locate the arms for
pile 0x226, pileType 0x228, owners 0x207, untradeable 0x361, untradeableCount 0x362,
tradeId 0x331, loans 0x19b, itemLoans 0x16f, duplicateItemLoans 0xed,
tradeState 0x335, contract 0xa9?(unknown, resolved from tsv below).

CONTROL (same syntactic form as the target): the item deser is known to store our
discardValue (atom 0xd7) at item+0x38.  If the immediate-scan below does not find
0xd7 in FUN_18013fe00 in the SAME form (case label / cmp / ladder) then the scan is
broken and every absence claim in this batch is void.  Second control: 0x172 itself
appears in docs as reaching the enum table, so xrefs_to(0x180229d20) must be
non-empty.

ABSENCE TRAP GUARD: we do not grep "== 0x".  We enumerate every immediate operand of
every instruction in the function (scalar operands of any size), which catches
`cmp ==`, `cmp !=`, jump-table `case` labels only indirectly, and running-sum
sub/dec ladders (the ladder deltas are computed and searched too).  Jump tables are
handled separately by walking every switch construct Ghidra knows about.

OUTPUT: full decompiles printed untruncated with len(src); full table dump; full
immediate census.
"""
import traceback, os

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

ATOMS = {
    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", 0xd7: "discardValue(CONTROL)", 0x19: "allowUntradeableForSquadBuildingSets",
}


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  fname=%s  len(src)=%d  (FULL, NOT TRUNCATED)" % (tag, va, f.getName(), len(src)))
    print("=" * 78)
    if echo:
        print(src)
    with open(OUT + "q1_%s_%x.c" % (tag, va), "w") as fh:
        fh.write(src)
    return src


def immediates(va):
    """Every scalar operand of every instruction in the function containing va."""
    f = func(va)
    out = {}
    if f is None:
        return out
    body = f.getBody()
    it = listing.getInstructions(body, True)
    n = 0
    while it.hasNext():
        ins = it.next()
        n += 1
        for i in range(ins.getNumOperands()):
            for o in ins.getOpObjects(i):
                try:
                    v = int(o.getValue())
                except Exception:
                    continue
                out.setdefault(v & 0xFFFFFFFFFFFFFFFF, []).append(
                    (int(ins.getAddress().getOffset()), str(ins)))
    print("[immediates] %s %#x: %d instructions, %d distinct scalars"
          % (f.getName(), va, n, len(out)))
    return out


def report_atoms(va, imms):
    """Direct hits + running-sum ladder reconstruction."""
    print("--- atom census for %#x (%s)" % (va, fname(va)))
    for a, name in sorted(ATOMS.items()):
        hits = imms.get(a, [])
        if hits:
            print("  DIRECT 0x%-4x %-38s x%d  first@%#x  %s"
                  % (a, name, len(hits), hits[0][0], hits[0][1]))
    # ladder reconstruction: walk instructions in address order, keep a running
    # sum of sub/dec/add immediates on the dispatch register, report any partial
    # sum that equals one of our atoms.
    f = func(va)
    if f is None:
        return
    it = listing.getInstructions(f.getBody(), True)
    running = {}
    while it.hasNext():
        ins = it.next()
        m = ins.getMnemonicString().lower()
        if m not in ("sub", "add", "dec", "inc", "cmp", "lea"):
            continue
        try:
            reg = str(ins.getOpObjects(0)[0])
        except Exception:
            continue
        val = None
        for o in ins.getOpObjects(1) if ins.getNumOperands() > 1 else []:
            try:
                val = int(o.getValue())
            except Exception:
                pass
        if m == "dec":
            val = 1
        if m == "inc":
            val = -1
        if val is None:
            continue
        if m in ("sub", "dec"):
            running[reg] = running.get(reg, 0) + val
        elif m in ("add", "inc"):
            running[reg] = running.get(reg, 0) - val
        cur = running.get(reg)
        if cur in ATOMS:
            print("  LADDER 0x%-4x %-38s @%#x  %s  (running sum on %s)"
                  % (cur, ATOMS[cur], int(ins.getAddress().getOffset()), ins, reg))


def switch_labels(va):
    """Every switch case label Ghidra resolved inside the function."""
    f = func(va)
    if f is None:
        return
    from ghidra.program.model.symbol import FlowType  # noqa
    it = listing.getInstructions(f.getBody(), True)
    tot = 0
    while it.hasNext():
        ins = it.next()
        ft = ins.getFlowType()
        if ft is not None and ft.isJump() and ft.isComputed():
            tgts = ins.getFlows()
            print("  SWITCH @%#x  %s  -> %d targets" % (int(ins.getAddress().getOffset()), ins, len(tgts)))
            tot += len(tgts)
    if tot:
        print("  (computed-jump targets total %d)" % tot)


try:
    print("###### PART A: the itemState enum table at 0x180229d20")
    # walk generously in both directions; entries are (char* name, ...) stride 0x10
    base = 0x180229d20
    for off in range(-0x200, 0x400, 0x10):
        a = base + off
        try:
            p = qword(a)
            q = qword(a + 8)
        except Exception:
            continue
        s = ""
        if 0x180000000 <= p < 0x181000000:
            try:
                s = rd_str(p, 64)
            except Exception:
                s = "<unreadable>"
        print("  %#x  (%+#5x)  p=%#018x  q=%#018x  str=%r" % (a, off, p, q, s))

    print()
    print("###### PART B: who references the table")
    for t in (0x180229d20,):
        for frm, typ, fn, ent in xrefs_to(t):
            print("  xref %#x %s in %s (%#x)" % (frm, typ, fn, ent))

    print()
    print("###### PART C: shared item deser FUN_18013fe00")
    src = dump("itemdeser", 0x18013fe00)
    imms = immediates(0x18013fe00)
    report_atoms(0x18013fe00, imms)
    switch_labels(0x18013fe00)

    print()
    print("###### PART D: full sorted immediate dump for 0x18013fe00 (values < 0x1000)")
    for v in sorted(k for k in imms if k < 0x1000):
        print("   0x%-4x n=%-3d %s" % (v, len(imms[v]), imms[v][0][1]))

    print()
    print("###### PART E: callees of the item deser")
    try:
        for c in callees(0x18013fe00):
            print("  callee", c)
    except Exception as e:
        print("  callees() failed:", e)

except Exception:
    traceback.print_exc()
