"""Cross-Clock Domain Analysis.

Detects metastability-prone crossings, verifies synchronizer behavior,
and analyzes async FIFO CDC patterns.

Usage:
    from ewal.cdc import CDCAnalyzer
    cdc = CDCAnalyzer(clk_a=t.clk_100mhz, clk_b=t.clk_133mhz)
"""

from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any, Optional, TYPE_CHECKING

if TYPE_CHECKING:
    from ewal.signal import SignalRef


@dataclass
class TightCrossing:
    index: int
    signal: str
    setup_margin: float
    hold_margin: float


@dataclass
class SyncCheckResult:
    avg_latency_cycles: float
    max_latency_cycles: int
    glitch_count: int
    total_crossings: int


@dataclass
class FifoCheckResult:
    max_fill: int
    near_full_count: int
    near_empty_count: int
    overflow_count: int
    underflow_count: int
    gray_violations: int


class CDCAnalyzer:
    """Cross-clock domain analyzer."""

    def __init__(self, clk_a, clk_b):
        self.clk_a = clk_a
        self.clk_b = clk_b
        self._trace = None
        for sig in [clk_a, clk_b]:
            if hasattr(sig, "_trace"):
                self._trace = sig._trace
                break

    def _rising_edges(self, clk_sig) -> list[int]:
        """Get all rising edge indices for a clock."""
        t = self._trace
        if t is None:
            return []
        from ewal.signal import _compile_expr
        compiled = _compile_expr(clk_sig.rising(), t)
        return [i for i in range(t.max_index + 1)
                if t._core.eval_expr_bool(compiled, i)]

    def find_tight_crossings(self, signal, src_domain, dst_domain,
                              min_setup_ns: float = 0.5,
                              min_hold_ns: float = 0.3) -> list[TightCrossing]:
        """Find signals that change close to destination clock edges.

        A "tight crossing" is when a signal changes on a src_domain edge
        and the next dst_domain edge is dangerously close.
        """
        t = self._trace
        if t is None:
            return []

        src_edges = self._rising_edges(src_domain)
        dst_edges = self._rising_edges(dst_domain)

        if not src_edges or not dst_edges:
            return []

        results = []
        from ewal.signal import _compile_expr
        sig_changed = _compile_expr(signal.unstable(), t)

        dst_idx = 0
        for src_idx in src_edges:
            # Check if signal changed at this src edge
            if not t._core.eval_expr_bool(sig_changed, src_idx):
                continue

            # Find next dst edge after this src edge
            while dst_idx < len(dst_edges) and dst_edges[dst_idx] <= src_idx:
                dst_idx += 1

            if dst_idx >= len(dst_edges):
                break

            next_dst = dst_edges[dst_idx]
            src_ts = t._core.timestamp(src_idx)
            dst_ts = t._core.timestamp(next_dst)
            margin = dst_ts - src_ts

            if margin < min_setup_ns * 1e9 or margin < min_hold_ns * 1e9:
                sig_name = signal._path if hasattr(signal, "_path") else str(signal)
                results.append(TightCrossing(
                    index=src_idx,
                    signal=sig_name,
                    setup_margin=margin,
                    hold_margin=margin,
                ))

        return results

    def verify_synchronizer(self, input_signal, sync_stage_1, sync_stage_2,
                            clock) -> SyncCheckResult:
        """Verify a 2-FF synchronizer is working correctly.

        Checks that input transitions propagate through the sync stages
        and measures latency.
        """
        t = self._trace
        if t is None:
            return SyncCheckResult(0, 0, 0, 0)

        clk_edges = self._rising_edges(clock)
        latencies = []
        glitches = 0
        total_crossings = 0

        from ewal.signal import _compile_expr
        input_changed = _compile_expr(input_signal.unstable(), t)

        for i, edge_idx in enumerate(clk_edges):
            if not t._core.eval_expr_bool(input_changed, edge_idx):
                continue

            total_crossings += 1

            # Measure how many clock edges until sync_stage_2 reflects the change
            old = t._index
            t._index = edge_idx
            t._virtual_cache.clear()
            input_val = input_signal.value

            latency = 0
            for j in range(i + 1, min(i + 20, len(clk_edges))):
                latency += 1
                t._index = clk_edges[j]
                t._virtual_cache.clear()
                s2_val = sync_stage_2.value
                if s2_val == input_val:
                    break

            t._index = old
            latencies.append(latency)

            # Check for glitches: sync_stage_1 should never glitch
            # (simplified: check if it changes more than once per crossing)
            if i + 2 < len(clk_edges):
                t._index = clk_edges[i + 1]
                t._virtual_cache.clear()
                s1_a = sync_stage_1.value
                if i + 3 < len(clk_edges):
                    t._index = clk_edges[i + 2]
                    t._virtual_cache.clear()
                    s1_b = sync_stage_1.value
                    if s1_a != s1_b and s1_b != input_val:
                        glitches += 1
                t._index = old

        avg_lat = sum(latencies) / len(latencies) if latencies else 0
        max_lat = max(latencies) if latencies else 0

        return SyncCheckResult(
            avg_latency_cycles=avg_lat,
            max_latency_cycles=max_lat,
            glitch_count=glitches,
            total_crossings=total_crossings,
        )

    def analyze_async_fifo(self, wr_clk, rd_clk, wr_en, rd_en,
                           full, empty, wr_ptr=None, rd_ptr=None) -> FifoCheckResult:
        """Analyze an asynchronous FIFO for CDC issues."""
        t = self._trace
        if t is None:
            return FifoCheckResult(0, 0, 0, 0, 0, 0)

        wr_edges = self._rising_edges(wr_clk)
        rd_edges = self._rising_edges(rd_clk)

        max_fill = 0
        near_full_count = 0
        near_empty_count = 0
        overflow_count = 0
        underflow_count = 0
        gray_violations = 0

        fill_level = 0

        # Track fill level by counting writes and reads
        all_events = []
        for idx in wr_edges:
            old = t._index; t._index = idx; t._virtual_cache.clear()
            if wr_en.value:
                all_events.append((idx, "write", full.value, empty.value))
            t._index = old
        for idx in rd_edges:
            old = t._index; t._index = idx; t._virtual_cache.clear()
            if rd_en.value:
                all_events.append((idx, "read", full.value, empty.value))
            t._index = old

        all_events.sort(key=lambda x: x[0])

        for idx, op, is_full, is_empty in all_events:
            if op == "write":
                if is_full:
                    overflow_count += 1
                fill_level += 1
            elif op == "read":
                if is_empty:
                    underflow_count += 1
                fill_level = max(0, fill_level - 1)

            max_fill = max(max_fill, fill_level)

            # Near-full/empty heuristic
            if is_full:
                near_full_count += 1
            if is_empty:
                near_empty_count += 1

        # Gray code check on pointers
        if wr_ptr is not None:
            prev_val = None
            for idx in wr_edges:
                old = t._index; t._index = idx; t._virtual_cache.clear()
                val = wr_ptr.value
                t._index = old
                if val is not None and prev_val is not None:
                    # Gray code: only 1 bit should change at a time
                    xor = (val or 0) ^ (prev_val or 0)
                    if xor != 0 and (xor & (xor - 1)) != 0:
                        gray_violations += 1
                prev_val = val

        return FifoCheckResult(
            max_fill=max_fill,
            near_full_count=near_full_count,
            near_empty_count=near_empty_count,
            overflow_count=overflow_count,
            underflow_count=underflow_count,
            gray_violations=gray_violations,
        )
