#!/usr/bin/env python3
"""Static helper for GDR-8050L E7/HL transaction-buffer analysis.

Searches a plaintext firmware image and optional ASM text for the literals and
helper targets involved in the 0x3CBB0 transaction path. This is intentionally
conservative: binary hits are candidate references, not proof of a CPU writer.
"""
import argparse
import csv
from pathlib import Path

DEFAULT_LITERALS = [
    0x0003CBB0,
    0x8003CBB0,
    0x0003CBBC,
    0x0003CC00,
    0x00000654,
    0x0000D954,
    0x0000D904,
    0x0000D950,
    0x0000DF00,
    0x0000DF28,
    0x0000DF37,
    0x0000DC54,
]

DEFAULT_TARGETS = [
    0x90030968,
    0x9003099E,
    0x90030B97,
    0x900210C4,
    0x90021BAD,
    0x90021304,
    0x9001BD2B,
    0x90022309,
    0x90022392,
    0x9001E292,
    0x90028580,
]


def parse_int(s):
    if isinstance(s, int):
        return s
    return int(str(s).replace("_", ""), 0)


def find_all(data, needle):
    off = data.find(needle)
    while off != -1:
        yield off
        off = data.find(needle, off + 1)


def binary_hits(path, base, literals):
    data = path.read_bytes()
    rows = []
    for lit in literals:
        patterns = []
        if lit <= 0xFFFF:
            patterns.append(("le16", lit.to_bytes(2, "little")))
            patterns.append(("be16", lit.to_bytes(2, "big")))
        patterns.append(("le32", lit.to_bytes(4, "little")))
        patterns.append(("be32", lit.to_bytes(4, "big")))
        for endian, needle in patterns:
            for off in find_all(data, needle):
                rows.append({
                    "source": str(path),
                    "kind": "binary_literal",
                    "target": f"0x{lit:08X}",
                    "encoding": endian,
                    "offset": f"0x{off:06X}",
                    "vma": f"0x{(base + off) & 0xFFFFFFFF:08X}",
                    "line": "",
                })
    return rows


def asm_hits(path, literals, targets):
    rows = []
    text = path.read_text(errors="replace").splitlines()
    needles = []
    for lit in literals:
        needles.extend([(f"0x{lit:08x}", lit), (f"{lit:08x}", lit)])
        if lit <= 0xFFFF:
            needles.extend([(f"0x{lit:04x}", lit), (f"{lit:04x}", lit)])
    for target in targets:
        needles.extend([(f"0x{target:08x}", target), (f"{target:08x}", target)])
    seen = set()
    for lineno, line in enumerate(text, 1):
        low = line.lower()
        for needle, target in needles:
            if needle in low:
                key = (lineno, target, needle)
                if key in seen:
                    continue
                seen.add(key)
                rows.append({
                    "source": str(path),
                    "kind": "asm_text_hit",
                    "target": f"0x{target:08X}",
                    "encoding": needle,
                    "offset": "",
                    "vma": "",
                    "line": f"{lineno}: {line.strip()}",
                })
    return rows


def write_outputs(rows, out_prefix):
    csv_path = Path(str(out_prefix) + ".csv")
    md_path = Path(str(out_prefix) + ".md")
    fields = ["source", "kind", "target", "encoding", "offset", "vma", "line"]
    with csv_path.open("w", newline="", encoding="utf-8") as f:
        w = csv.DictWriter(f, fieldnames=fields)
        w.writeheader()
        w.writerows(rows)
    with md_path.open("w", encoding="utf-8") as f:
        f.write("# GDR-8050L transaction reference scan\n\n")
        f.write(f"Rows: {len(rows)}\n\n")
        for r in rows[:200]:
            loc = r.get("vma") or r.get("line") or r.get("offset")
            f.write(f"- `{r['kind']}` target `{r['target']}` via `{r['encoding']}` at {loc}\n")
        if len(rows) > 200:
            f.write(f"\n_Truncated markdown preview at 200 rows; see `{csv_path.name}` for all rows._\n")
    return csv_path, md_path


def main():
    ap = argparse.ArgumentParser(description=__doc__)
    ap.add_argument("--bin", dest="bin_path", help="Plaintext firmware BIN")
    ap.add_argument("--base", default="0x90000000", help="Firmware VMA base for BIN offsets")
    ap.add_argument("--asm", dest="asm_path", help="Optional disassembly text/ASM")
    ap.add_argument("--literal", action="append", default=[], help="Extra literal to search, e.g. 0x3CBB0")
    ap.add_argument("--target", action="append", default=[], help="Extra function target to search, e.g. 0x90021BAD")
    ap.add_argument("--out-prefix", default="gdr8050l_transaction_ref_scan")
    args = ap.parse_args()

    literals = DEFAULT_LITERALS + [parse_int(x) for x in args.literal]
    targets = DEFAULT_TARGETS + [parse_int(x) for x in args.target]
    rows = []
    if args.bin_path:
        rows.extend(binary_hits(Path(args.bin_path), parse_int(args.base), literals))
    if args.asm_path:
        rows.extend(asm_hits(Path(args.asm_path), literals, targets))
    csv_path, md_path = write_outputs(rows, args.out_prefix)
    print(f"wrote {csv_path}")
    print(f"wrote {md_path}")


if __name__ == "__main__":
    main()
