"""gdb pretty-printers for droidsaw IR types.

Load interactively:  (gdb) source /path/to/tools/gdb/droidsaw.py
Or set up auto-load:  see tools/gdb/README.md.

Covers the shared IR surface (droidsaw_common + droidsaw_dex + droidsaw_hermes).
Every printer is defensive: a bug here must never destroy the gdb session, so
to_string() and children() swallow exceptions and fall back to a stub.
"""

from __future__ import annotations

import re
import traceback

import gdb
import gdb.printing


def _safe_int(val, default=0):
    """Extract an integer from a gdb.Value without raising."""
    try:
        return int(val)
    except Exception:
        return default


def _safe_str(val, default="?"):
    """Best-effort string of a gdb.Value."""
    try:
        return str(val)
    except Exception:
        return default


def _stdlib_len(val):
    """Length of an alloc::vec::Vec or alloc::collections::btree::map::BTreeMap.

    Both have a private `length` field that Rust's own gdb support reads.
    Returns None if the field isn't reachable.
    """
    try:
        return int(val["length"])
    except Exception:
        pass
    try:
        return int(val["len"])
    except Exception:
        pass
    try:
        vis = gdb.default_visualizer(val)
        if vis is None:
            return None
        n = 0
        for _ in vis.children():
            n += 1
        return n
    except Exception:
        return None


def _enum_variant(val):
    """Best-effort extraction of the active Rust enum variant name.

    gdb's Rust support exposes enums via a synthetic field name; different
    gdb/rustc combinations shape this differently, so probe a few ways.
    """
    try:
        t = val.type.strip_typedefs()
        if t.code != gdb.TYPE_CODE_UNION and t.code != gdb.TYPE_CODE_STRUCT:
            return None
        for field in t.fields():
            fname = field.name or ""
            if fname.startswith("RUST$ENUM$DISR"):
                return _safe_str(val[fname])
        disc = val.type.tag
        if disc:
            return disc
    except Exception:
        pass
    return None


class _BasePrinter:
    """Defensive base. Subclasses override _to_string() and optionally _children()."""

    def __init__(self, val):
        self.val = val

    def to_string(self):
        try:
            return self._to_string()
        except Exception as exc:
            return f"<droidsaw printer error: {type(exc).__name__}: {exc}>"

    def children(self):
        try:
            yield from self._children()
        except Exception as exc:
            yield ("error", f"<droidsaw printer children error: {exc}>")

    def _to_string(self):
        return "<unimplemented droidsaw printer>"

    def _children(self):
        return iter(())


# -------------------------------------------------------------------- dominators


class PostDomPrinter(_BasePrinter):
    """droidsaw_common::graph::dominators::PostDom<N> — Node(N) | Exit."""

    def _to_string(self):
        variant = _enum_variant(self.val) or ""
        if "Exit" in variant:
            return "PostDom::Exit"
        if "Node" in variant:
            try:
                inner = self.val[variant]
                return f"PostDom::Node({_safe_str(inner)})"
            except Exception:
                return "PostDom::Node(?)"
        return f"PostDom::?({variant})"


# -------------------------------------------------------------------- SSA (common)


class VarPrinter(_BasePrinter):
    """droidsaw_common::ssa::Var<R> — {reg: R, ver: u32}. Displays as r{reg}_{ver}."""

    def _to_string(self):
        reg = _safe_str(self.val["reg"])
        ver = _safe_int(self.val["ver"])
        return f"Var(r{reg}_{ver})"


class PhiPrinter(_BasePrinter):
    """droidsaw_common::ssa::Phi<B,R> — {dst: Var<R>, args: Vec<(B, Var<R>)>}."""

    def _to_string(self):
        try:
            dst_reg = _safe_str(self.val["dst"]["reg"])
            dst_ver = _safe_int(self.val["dst"]["ver"])
        except Exception:
            dst_reg, dst_ver = "?", 0
        preds = _stdlib_len(self.val["args"])
        preds_s = str(preds) if preds is not None else "?"
        return f"Phi(dst=r{dst_reg}_{dst_ver}, preds={preds_s})"

    def _children(self):
        yield ("dst", self.val["dst"])
        yield ("args", self.val["args"])


class BuilderPrinter(_BasePrinter):
    """droidsaw_common::ssa::Builder<G,R> — {current_def, next_ver, phis}."""

    def _to_string(self):
        defs = _stdlib_len(self.val["current_def"])
        phi_blocks = _stdlib_len(self.val["phis"])
        next_ver = _safe_int(self.val["next_ver"])
        defs_s = str(defs) if defs is not None else "?"
        phi_s = str(phi_blocks) if phi_blocks is not None else "?"
        return f"Builder(defs={defs_s}, next_ver={next_ver}, phi-blocks={phi_s})"

    def _children(self):
        yield ("current_def", self.val["current_def"])
        yield ("next_ver", self.val["next_ver"])
        yield ("phis", self.val["phis"])


class SsaErrorPrinter(_BasePrinter):
    """droidsaw_common::ssa::SsaError — VarOverflow | IterationLimit{cap}."""

    def _to_string(self):
        variant = _enum_variant(self.val) or ""
        if "VarOverflow" in variant:
            return "SsaError::VarOverflow"
        if "IterationLimit" in variant:
            try:
                cap = _safe_int(self.val[variant]["cap"])
                return f"SsaError::IterationLimit(cap={cap})"
            except Exception:
                return "SsaError::IterationLimit(cap=?)"
        return f"SsaError::?({variant})"


# -------------------------------------------------------------------- findings


class FindingPrinter(_BasePrinter):
    """droidsaw_common::Finding — id + layer + severity, details on expand."""

    def _to_string(self):
        try:
            fid = _safe_str(self.val["id"])
            layer = _safe_str(self.val["layer"])
            sev = _safe_str(self.val["severity"])
            return f"Finding(id={fid}, layer={layer}, severity={sev})"
        except Exception:
            return "Finding(?)"

    def _children(self):
        for fname in (
            "id",
            "layer",
            "source",
            "severity",
            "gauge_class",
            "confidence",
            "dismiss_reason",
            "cwe",
            "detail",
            "extra",
            "func_id",
            "provenance",
        ):
            try:
                yield (fname, self.val[fname])
            except Exception:
                continue


# -------------------------------------------------------------------- dex


class DexErrorPrinter(_BasePrinter):
    """droidsaw_dex::error::DexError — 12 variants, each with discriminant data."""

    # Per-variant: the fields worth surfacing in the one-liner.
    _SHAPES = {
        "BadMagic": ("found",),
        "UnsupportedVersion": ("version",),
        "ChecksumMismatch": ("expected", "computed"),
        "OffsetOutOfBounds": ("offset", "file_size"),
        "ScrollRead": ("offset",),
        "InvalidMutf8": ("offset",),
        "InvalidUleb128": ("offset",),
        "IndexOob": ("pool", "index", "pool_size"),
        "Truncated": ("offset", "need", "have"),
        "UnknownOpcode": ("opcode", "offset"),
        "TruncatedInstruction": ("offset", "need", "have"),
        "InvalidInstruction": ("offset", "detail"),
    }

    def _to_string(self):
        variant = _enum_variant(self.val) or ""
        for name, fields in self._SHAPES.items():
            if name in variant:
                parts = []
                for f in fields:
                    try:
                        parts.append(f"{f}={_safe_str(self.val[variant][f])}")
                    except Exception:
                        parts.append(f"{f}=?")
                return f"DexError::{name}({', '.join(parts)})"
        return f"DexError::?({variant})"


class CfgDexPrinter(_BasePrinter):
    """droidsaw_dex::cfg::Cfg — Vec<BasicBlock> + entry + eh-regions + addr->block."""

    def _to_string(self):
        blocks = _stdlib_len(self.val["blocks"])
        eh = _stdlib_len(self.val["exception_regions"])
        try:
            entry = _safe_int(self.val["entry"])
        except Exception:
            entry = "?"
        blocks_s = str(blocks) if blocks is not None else "?"
        eh_s = str(eh) if eh is not None else "?"
        return f"dex::Cfg(blocks={blocks_s}, entry=#{entry}, eh-regions={eh_s})"

    def _children(self):
        for fname in ("blocks", "entry", "exception_regions", "addr_to_block"):
            try:
                yield (fname, self.val[fname])
            except Exception:
                continue


class SsaBodyPrinter(_BasePrinter):
    """droidsaw_dex::ssa::SsaBody — BTreeMap<BlockIdx, SsaBlock> + counters."""

    def _to_string(self):
        blocks = _stdlib_len(self.val["blocks"])
        params = _stdlib_len(self.val["param_vars"])
        vars_n = _safe_int(self.val["var_counter"])
        try:
            entry = _safe_int(self.val["entry"])
        except Exception:
            entry = "?"
        blocks_s = str(blocks) if blocks is not None else "?"
        params_s = str(params) if params is not None else "?"
        return f"SsaBody(blocks={blocks_s}, vars={vars_n}, entry=#{entry}, params={params_s})"

    def _children(self):
        for fname in ("blocks", "entry", "var_counter", "param_vars"):
            try:
                yield (fname, self.val[fname])
            except Exception:
                continue


# -------------------------------------------------------------------- hermes


class HermesErrorPrinter(_BasePrinter):
    """droidsaw_hermes::HermesError — UnsupportedVersion{version} | Ssa(SsaError)."""

    def _to_string(self):
        variant = _enum_variant(self.val) or ""
        if "UnsupportedVersion" in variant:
            try:
                v = _safe_int(self.val[variant]["version"])
                return f"HermesError::UnsupportedVersion(version={v})"
            except Exception:
                return "HermesError::UnsupportedVersion(version=?)"
        if "Ssa" in variant:
            try:
                return f"HermesError::Ssa({_safe_str(self.val[variant])})"
            except Exception:
                return "HermesError::Ssa(?)"
        return f"HermesError::?({variant})"


class CfgHermesPrinter(_BasePrinter):
    """droidsaw_hermes::decompile::cfg::Cfg — BTreeMap<BlockId, BasicBlock> + order + exc-preds."""

    def _to_string(self):
        blocks = _stdlib_len(self.val["blocks"])
        order = _stdlib_len(self.val["block_order"])
        exc = _stdlib_len(self.val["exc_predecessors"])
        try:
            entry = _safe_int(self.val["entry"])
        except Exception:
            entry = "?"
        blocks_s = str(blocks) if blocks is not None else "?"
        order_s = str(order) if order is not None else "?"
        exc_s = str(exc) if exc is not None else "?"
        return (
            f"hermes::Cfg(blocks={blocks_s}, entry=#{entry}, "
            f"order={order_s}, exc-preds={exc_s})"
        )

    def _children(self):
        for fname in ("blocks", "entry", "block_order", "exc_predecessors"):
            try:
                yield (fname, self.val[fname])
            except Exception:
                continue


# -------------------------------------------------------------------- dispatch


_GENERIC_RE = re.compile(r"<[^<>]*(?:<[^<>]*>[^<>]*)*>")


def _strip_generics(name):
    """Remove `<…>` (nested) from a Rust type tag so Var<u16> and Var<HermesReg>
    collide into the same lookup key `droidsaw_common::ssa::Var`."""
    prev = None
    while prev != name:
        prev = name
        name = _GENERIC_RE.sub("", name)
    return name


_TABLE = {
    "droidsaw_common::graph::dominators::PostDom": PostDomPrinter,
    "droidsaw_common::ssa::Var": VarPrinter,
    "droidsaw_common::ssa::Phi": PhiPrinter,
    "droidsaw_common::ssa::Builder": BuilderPrinter,
    "droidsaw_common::ssa::SsaError": SsaErrorPrinter,
    "droidsaw_common::finding::Finding": FindingPrinter,
    "droidsaw_common::Finding": FindingPrinter,  # in case of re-export in gdb's view
    "droidsaw_dex::error::DexError": DexErrorPrinter,
    "droidsaw_dex::cfg::Cfg": CfgDexPrinter,
    "droidsaw_dex::ssa::SsaBody": SsaBodyPrinter,
    "droidsaw_hermes::error::HermesError": HermesErrorPrinter,
    "droidsaw_hermes::HermesError": HermesErrorPrinter,
    "droidsaw_hermes::decompile::cfg::Cfg": CfgHermesPrinter,
}


def lookup(val):
    try:
        t = val.type
        # Dereference pointers and references.
        if t.code in (gdb.TYPE_CODE_PTR, gdb.TYPE_CODE_REF):
            try:
                val = val.dereference()
                t = val.type
            except Exception:
                return None
        tag = t.strip_typedefs().tag
        if not tag:
            return None
        key = _strip_generics(tag).strip()
        cls = _TABLE.get(key)
        if cls is None:
            return None
        return cls(val)
    except Exception:
        return None


def register(objfile=None):
    """Register the droidsaw printer with gdb. Idempotent."""
    try:
        pp = gdb.printing.RegexpCollectionPrettyPrinter("droidsaw")
        # We do our own dispatch, so register a single catch-all lookup instead.
        if objfile is None:
            gdb.pretty_printers.append(lookup)
        else:
            objfile.pretty_printers.append(lookup)
    except Exception:
        # Print once, don't propagate.
        traceback.print_exc()


# Auto-register when sourced.
register()
