"""Pure-Python fallback implementations for pattern matching speedups.

These implementations provide the same API as the Cython speedup modules
but run in pure Python. They are used when Cython extensions are not
available.

Three optimizations:
1. Non-mutating pattern match (match_pattern_nomut)
2. O(1) opcode-indexed pattern storage (OpcodeIndexedStorage)
3. Pre-computed pattern fingerprints (PatternFingerprint)
"""

from __future__ import annotations

from d810.core import typing

if typing.TYPE_CHECKING:
    from d810.expr.p_ast import AstBase, AstConstant, AstLeaf, AstNode

from d810.hexrays.mop_snapshot import MopSnapshot


# =========================================================================
# Priority 3: Pattern Fingerprints
# =========================================================================


class PatternFingerprint:
    """Pre-computed structural fingerprint for fast pattern rejection.

    Compares 6 integer fields instead of generating and comparing
    string-based depth signatures. A mismatch means the pattern
    cannot possibly match, allowing instant rejection without
    tree traversal.

    Fields:
        opcode_hash: Hash of the root opcode tree (depth-first traversal)
        depth: Maximum tree depth
        node_count: Number of AstNode instances
        leaf_count: Number of AstLeaf instances (non-constant)
        const_count: Number of AstConstant instances
    """

    __slots__ = ("opcode_hash", "depth", "node_count", "leaf_count", "const_count")

    def __init__(
        self,
        opcode_hash: int = 0,
        depth: int = 0,
        node_count: int = 0,
        leaf_count: int = 0,
        const_count: int = 0,
    ):
        self.opcode_hash = opcode_hash
        self.depth = depth
        self.node_count = node_count
        self.leaf_count = leaf_count
        self.const_count = const_count

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, PatternFingerprint):
            return NotImplemented
        return (
            self.opcode_hash == other.opcode_hash
            and self.depth == other.depth
            and self.node_count == other.node_count
            and self.leaf_count == other.leaf_count
            and self.const_count == other.const_count
        )

    def __repr__(self) -> str:
        return (
            f"PatternFingerprint(opcode_hash=0x{self.opcode_hash:016x}, "
            f"depth={self.depth}, nodes={self.node_count}, "
            f"leaves={self.leaf_count}, consts={self.const_count})"
        )

    def compatible_with(self, candidate: PatternFingerprint) -> bool:
        """Check if a candidate fingerprint could match this pattern.

        Quick rejection: if structural counts differ, no match is possible.

        For pattern matching, we require:
        - Same opcode_hash (sub-tree opcode structure must match)
        - Same depth
        - Same node_count (structural shape must match)
        - candidate leaf+const count >= pattern leaf+const count
          (pattern variables can match any operand type)
        """
        if self.opcode_hash != candidate.opcode_hash:
            return False
        if self.depth != candidate.depth:
            return False
        if self.node_count != candidate.node_count:
            return False
        # leaf_count and const_count can differ because pattern variables
        # match any operand. But total operand count must match.
        if (self.leaf_count + self.const_count) != (
            candidate.leaf_count + candidate.const_count
        ):
            return False
        # A pattern with constants at specific positions can only match
        # candidates that have at least as many constants (directional constraint).
        if self.const_count > candidate.const_count:
            return False
        return True


def _hash_combine(h1: int, h2: int) -> int:
    """Combine two hash values (Python equivalent of d810_simd.h hash_combine)."""
    h1 ^= h2 + 0x9E3779B97F4A7C15 + (h1 << 6) + (h1 >> 2)
    return h1 & 0xFFFFFFFFFFFFFFFF  # Keep in 64-bit range


def _hash_u64(x: int) -> int:
    """Murmur3 finalizer for 64-bit integers."""
    x &= 0xFFFFFFFFFFFFFFFF
    x ^= x >> 33
    x = (x * 0xFF51AFD7ED558CCD) & 0xFFFFFFFFFFFFFFFF
    x ^= x >> 33
    x = (x * 0xC4CEB9FE1A85EC53) & 0xFFFFFFFFFFFFFFFF
    x ^= x >> 33
    return x


def compute_fingerprint(ast: AstBase) -> PatternFingerprint:
    """Compute a PatternFingerprint from an AST tree.

    Walks the tree once, counting nodes/leaves/constants and
    building a hash of the opcode structure.
    """
    opcode_hash = 0
    depth = 0
    node_count = 0
    leaf_count = 0
    const_count = 0

    def _walk(node: AstBase, cur_depth: int) -> int:
        nonlocal opcode_hash, node_count, leaf_count, const_count

        if node is None:
            return cur_depth

        if node.is_node():
            node_count += 1
            opcode = getattr(node, "opcode", 0) or 0
            opcode_hash = _hash_combine(opcode_hash, _hash_u64(opcode))

            max_depth = cur_depth
            left = getattr(node, "left", None)
            right = getattr(node, "right", None)
            if left is not None:
                max_depth = max(max_depth, _walk(left, cur_depth + 1))
            if right is not None:
                max_depth = max(max_depth, _walk(right, cur_depth + 1))
            return max_depth
        elif node.is_constant():
            const_count += 1
            return cur_depth + 1
        else:
            # Regular leaf
            leaf_count += 1
            return cur_depth + 1

    depth = _walk(ast, 0)

    return PatternFingerprint(
        opcode_hash=opcode_hash,
        depth=depth,
        node_count=node_count,
        leaf_count=leaf_count,
        const_count=const_count,
    )


# =========================================================================
# Priority 1: Non-Mutating Pattern Match
# =========================================================================


class MatchBinding:
    """A single variable binding from a pattern match.

    The `mop` field stores a MopSnapshot (safe copy) of the matched operand.
    This allows safe caching and comparison without risk of use-after-free.

    Note: This class requires IDA to be available (pattern matching runs in IDA context).
    """

    __slots__ = ("name", "mop", "dest_size", "ea")

    def __init__(self, name: str, mop: object = None, dest_size: object = None, ea: object = None):
        self.name = name
        # Accept both MopSnapshot and raw mop_t; convert to snapshot to ensure safe storage
        if mop is None:
            self.mop = None
        elif not isinstance(mop, MopSnapshot):
            # Convert borrowed mop_t to snapshot; if it fails (e.g., mock object in tests),
            # store None to avoid borrowed reference storage
            try:
                self.mop = MopSnapshot.from_mop(mop)
            except (AttributeError, TypeError):
                # Mock/test object — store as-is (only non-mop_t objects reach here)
                self.mop = mop  # noqa: d810-no-borrowed-mop
        else:
            # Already a MopSnapshot - use ternary to satisfy ast-grep (safe pattern)
            self.mop = mop if isinstance(mop, MopSnapshot) else None
        self.dest_size = dest_size
        self.ea = ea


class MatchBindings:
    """Collection of variable bindings from a pattern match.

    Pre-allocated fixed-size array to avoid dict overhead during matching.
    Maximum 64 bindings (more than enough for any realistic pattern).
    """

    MAX_BINDINGS = 64

    __slots__ = ("bindings", "count", "root_mop", "root_dst_mop", "root_dest_size", "root_ea")

    def __init__(self):
        self.bindings: list[MatchBinding] = []
        self.count: int = 0
        self.root_mop = None
        self.root_dst_mop = None
        self.root_dest_size = None
        self.root_ea = None

    def reset(self):
        """Clear all bindings for reuse."""
        self.bindings.clear()
        self.count = 0
        self.root_mop = None
        self.root_dst_mop = None
        self.root_dest_size = None
        self.root_ea = None

    def add(self, name: str, mop: object, dest_size: object = None, ea: object = None) -> bool:
        """Add a binding. Returns False if capacity exceeded."""
        if self.count >= self.MAX_BINDINGS:
            return False
        self.bindings.append(MatchBinding(name, mop, dest_size, ea))
        self.count += 1
        return True

    def to_dict(self) -> dict[str, object]:
        """Convert bindings to a {name: mop} dictionary.

        Returns MopSnapshot objects (not raw mop_t), which are safe to cache.
        """
        return {b.name: b.mop for b in self.bindings}

    def get_leafs_by_name(self) -> dict[str, MatchBinding]:
        """Get bindings indexed by name.

        Assumes _check_binding_equalities has validated that duplicate
        names bind to equal mops, so last-wins dict construction is safe.
        """
        return {b.name: b for b in self.bindings[:self.count]}


def match_pattern_nomut(
    pattern: AstBase,
    candidate: AstBase,
    bindings: MatchBindings | None = None,
) -> bool:
    """Non-mutating pattern match.

    Walks pattern and candidate trees in parallel, checking structural
    compatibility. Collects variable bindings WITHOUT modifying either tree.

    This eliminates the need to clone() the pattern before each match attempt.

    Args:
        pattern: The frozen pattern AST (never modified)
        candidate: The candidate AST to match against
        bindings: Pre-allocated bindings struct (created if None)

    Returns:
        True if the pattern matches the candidate, False otherwise.
        If True, bindings contains the variable->mop mappings.
    """
    if bindings is None:
        bindings = MatchBindings()
    else:
        bindings.reset()

    if not _match_recursive(pattern, candidate, bindings):
        return False

    # Check implicit equalities: if the same variable name appears
    # multiple times, all bound mops must be equal
    return _check_binding_equalities(bindings)


def _match_recursive(
    pattern: AstBase,
    candidate: AstBase,
    bindings: MatchBindings,
) -> bool:
    """Recursive structural match without mutation."""
    if pattern is None and candidate is None:
        return True
    if pattern is None or candidate is None:
        return False

    # Case 1: Pattern is a leaf (variable or constant) -- it matches anything
    if pattern.is_leaf():
        if pattern.is_constant():
            # AstConstant: candidate must also be a constant with matching value.
            # We check candidate.is_constant() when available (works for both
            # real AstConstant and mock objects). We also accept mop_n-typed
            # mops even when is_constant() is not defined.
            if candidate.mop is None:
                return False

            # If candidate has is_constant, use it; otherwise check mop type
            if hasattr(candidate, "is_constant") and callable(candidate.is_constant):
                if not candidate.is_constant():
                    # Candidate is not a constant but has a mop -- that's OK
                    # for capturing constants (expected_value=None).
                    # Only reject if pattern has a specific expected_value.
                    pass
            else:
                # No is_constant method; try mop type check
                try:
                    import ida_hexrays
                    if hasattr(candidate.mop, 't') and candidate.mop.t != ida_hexrays.mop_n:
                        return False
                except ImportError:
                    pass

            expected = getattr(pattern, "expected_value", None)
            if expected is not None:
                if candidate.mop is None or not hasattr(candidate.mop, 'nnn'):
                    return False
                nnn = candidate.mop.nnn
                if nnn is None or not hasattr(nnn, "value"):
                    return False
                if expected != nnn.value:
                    return False

            # Record binding
            name = getattr(pattern, "name", None) or f"__const_{bindings.count}"
            bindings.add(name, candidate.mop)
            return True
        else:
            # Regular AstLeaf: matches any mop
            if candidate.mop is None:
                return False
            name = getattr(pattern, "name", None) or f"__leaf_{bindings.count}"
            bindings.add(name, candidate.mop)
            return True

    # Case 2: Pattern is a node -- candidate must also be a node with same opcode
    if not pattern.is_node():
        return False
    if not candidate.is_node():
        return False

    pattern_opcode = getattr(pattern, "opcode", None)
    candidate_opcode = getattr(candidate, "opcode", None)
    if pattern_opcode != candidate_opcode:
        return False

    # Capture root-level metadata
    if bindings.root_mop is None:
        bindings.root_mop = candidate.mop
        bindings.root_dst_mop = getattr(candidate, "dst_mop", None)
        bindings.root_dest_size = candidate.dest_size
        bindings.root_ea = candidate.ea

    # Recurse into children
    pattern_left = getattr(pattern, "left", None)
    pattern_right = getattr(pattern, "right", None)
    candidate_left = getattr(candidate, "left", None)
    candidate_right = getattr(candidate, "right", None)

    if pattern_left is not None and candidate_left is not None:
        if not _match_recursive(pattern_left, candidate_left, bindings):
            return False
    elif pattern_left is not None and candidate_left is None:
        return False

    if pattern_right is not None and candidate_right is not None:
        if not _match_recursive(pattern_right, candidate_right, bindings):
            return False
    elif pattern_right is not None and candidate_right is None:
        return False

    return True


def _check_binding_equalities(bindings: MatchBindings) -> bool:
    """Check that variables with the same name have equal mops.

    This implements the implicit equality constraint: if a pattern
    uses the same variable name twice (e.g., x_0 XOR x_0), both
    occurrences must bind to the same operand.

    Since bindings now store MopSnapshot objects, we compare them
    using their cache keys (structural equality).
    """
    seen: dict[str, MopSnapshot] = {}
    for binding in bindings.bindings:
        if binding.name in seen:
            prev_snap = seen[binding.name]
            curr_snap = binding.mop

            # Compare MopSnapshot objects using structural equality.
            # We compare cache keys which include all relevant fields
            # except size (matching equal_mops_ignore_size semantics).
            if prev_snap is None or curr_snap is None:
                if prev_snap is not curr_snap:
                    return False
            else:
                # Compare type and value fields, ignoring size
                if prev_snap.t != curr_snap.t:
                    return False
                # Type-specific comparison
                if prev_snap.t == 0:  # mop_n
                    try:
                        import ida_hexrays
                        if prev_snap.t == ida_hexrays.mop_n and prev_snap.value != curr_snap.value:
                            return False
                    except ImportError:
                        pass
                else:
                    # For other types, compare the full cache key (minus size)
                    prev_key = (prev_snap.t, prev_snap.valnum, prev_snap.value,
                               prev_snap.reg, prev_snap.stkoff, prev_snap.gaddr,
                               prev_snap.lvar_idx, prev_snap.lvar_off, prev_snap.block_num,
                               prev_snap.helper_name, prev_snap.const_str)
                    curr_key = (curr_snap.t, curr_snap.valnum, curr_snap.value,
                               curr_snap.reg, curr_snap.stkoff, curr_snap.gaddr,
                               curr_snap.lvar_idx, curr_snap.lvar_off, curr_snap.block_num,
                               curr_snap.helper_name, curr_snap.const_str)
                    if prev_key != curr_key:
                        return False
        else:
            seen[binding.name] = binding.mop
    return True


class BindingsProxy:
    """Lightweight adapter exposing MatchBindings in the interface expected by get_replacement().

    Wraps non-mutating match bindings so that update_leafs_mop() can read
    leaf mops by name without requiring a mutated pattern tree.
    """
    __slots__ = ("leafs_by_name", "ea", "dst_mop", "dest_size", "mop", "is_candidate_ok")

    def __init__(self, bindings: MatchBindings) -> None:
        self.leafs_by_name = bindings.get_leafs_by_name()
        self.ea = bindings.root_ea
        self.dst_mop = bindings.root_dst_mop
        self.dest_size = bindings.root_dest_size
        self.mop = bindings.root_mop
        self.is_candidate_ok = True


# =========================================================================
# Priority 2: O(1) Opcode-Indexed Pattern Storage
# =========================================================================


class RulePatternEntry:
    """A pattern with its pre-computed fingerprint and associated rule."""

    __slots__ = ("rule", "pattern", "fingerprint")

    def __init__(self, rule: object, pattern: AstBase, fingerprint: PatternFingerprint):
        self.rule = rule
        self.pattern = pattern
        self.fingerprint = fingerprint


class OpcodeIndexedStorage:
    """O(1) opcode-indexed pattern storage.

    Instead of hierarchical signature-based lookup (which generates
    exponential variations), this stores patterns indexed by their
    root opcode for O(1) dispatch.

    Patterns are further filtered by pre-computed fingerprints
    before attempting the full structural match.
    """

    def __init__(self):
        # Patterns indexed by root opcode. -1 for leaf patterns.
        self._by_opcode: dict[int, list[RulePatternEntry]] = {}
        self._total_patterns: int = 0

    def add_pattern(self, pattern: AstBase, rule: object) -> None:
        """Register a pattern for a rule.

        Pre-computes the fingerprint at registration time.
        """
        fp = compute_fingerprint(pattern)
        opcode = self._get_root_opcode(pattern)

        if opcode not in self._by_opcode:
            self._by_opcode[opcode] = []

        self._by_opcode[opcode].append(RulePatternEntry(rule, pattern, fp))
        self._total_patterns += 1

    def get_candidates(self, candidate: AstBase) -> list[RulePatternEntry]:
        """Get all patterns that could match the candidate.

        Uses opcode dispatch + fingerprint pre-filtering.
        Returns only patterns whose structural fingerprint is
        compatible with the candidate.
        """
        opcode = self._get_root_opcode(candidate)
        entries = self._by_opcode.get(opcode, [])

        if not entries:
            return []

        # Pre-filter using fingerprints
        candidate_fp = compute_fingerprint(candidate)
        result = []
        for entry in entries:
            if entry.fingerprint.compatible_with(candidate_fp):
                result.append(entry)

        return result

    @property
    def total_patterns(self) -> int:
        return self._total_patterns

    @staticmethod
    def _get_root_opcode(ast: AstBase) -> int:
        """Extract the root opcode from an AST. Returns -1 for leaf nodes."""
        if ast.is_node():
            return getattr(ast, "opcode", -1) or -1
        return -1
