"""Temporal Pattern Matching — "Regex for waveforms."

Provides seq(), within(), eventually(), repeat(), consecutive() for
multi-cycle pattern matching across waveform traces.

Usage:
    from ewal.temporal import seq, within, eventually, repeat, consecutive

    pattern = seq(
        req,                    # step 0: req is high
        within(5, gnt),         # within 5 cycles: gnt goes high
    )
    matches = t.match(pattern, clock=clk)
"""

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


@dataclass
class MatchResult:
    """Result of a temporal pattern match."""
    start: int
    end: int
    captures: dict[str, Any] = field(default_factory=dict)

    def __getattr__(self, name):
        if name in self.__dict__.get("captures", {}):
            return self.captures[name]
        raise AttributeError(name)


class TemporalStep:
    """Base class for a step in a temporal pattern."""
    pass


class ConditionStep(TemporalStep):
    """Match when a condition is true at the current cycle."""
    def __init__(self, condition):
        self.condition = condition


class WithinStep(TemporalStep):
    """Match when condition becomes true within N cycles."""
    def __init__(self, max_cycles: int, condition):
        self.max_cycles = max_cycles
        self.condition = condition


class EventuallyStep(TemporalStep):
    """Match when condition becomes true within max cycles (alias for within)."""
    def __init__(self, condition, max: int = 100):
        self.condition = condition
        self.max_cycles = max


class RepeatStep(TemporalStep):
    """Match when inner pattern repeats N times."""
    def __init__(self, count: int, inner: TemporalStep):
        self.count = count
        self.inner = inner


class ConsecutiveStep(TemporalStep):
    """Match when condition is true for min..max consecutive cycles."""
    def __init__(self, condition, min: int = 1, max: int = None):
        self.condition = condition
        self.min_cycles = min
        self.max_cycles = max


class SequencePattern:
    """A sequence of temporal steps to match against a trace."""

    def __init__(self, steps: list[TemporalStep]):
        self.steps = steps
        self._capture_fns: dict[str, tuple[int, Callable]] = {}

    def capture(self, **kwargs) -> "SequencePattern":
        """Add named captures to the pattern.

        Each kwarg maps a name to a callable that extracts a value.
        Captures are evaluated at the step index where the pattern matched.
        """
        for name, fn in kwargs.items():
            # By default capture at step 0; can be enhanced
            self._capture_fns[name] = fn
        return self


# ── Public API constructors ──

def seq(*steps) -> SequencePattern:
    """Create a temporal sequence pattern.

    Each argument is either:
      - A signal condition (evaluated as bool at a timestep)
      - A TemporalStep (within, eventually, repeat, consecutive)
    """
    parsed = []
    for s in steps:
        if isinstance(s, TemporalStep):
            parsed.append(s)
        else:
            # Treat as a simple condition
            parsed.append(ConditionStep(s))
    return SequencePattern(parsed)


def within(max_cycles: int, condition) -> WithinStep:
    """Condition must become true within max_cycles cycles."""
    return WithinStep(max_cycles, condition)


def eventually(condition, max: int = 100) -> EventuallyStep:
    """Condition must eventually become true (within max cycles)."""
    return EventuallyStep(condition, max=max)


def repeat(count: int, inner) -> RepeatStep:
    """Inner pattern must repeat exactly count times."""
    if not isinstance(inner, TemporalStep):
        inner = ConditionStep(inner)
    return RepeatStep(count, inner)


def consecutive(condition, min: int = 1, max: int = None) -> ConsecutiveStep:
    """Condition must be true for min..max consecutive cycles."""
    return ConsecutiveStep(condition, min=min, max=max)


def _eval_condition(trace, condition, index: int) -> bool:
    """Evaluate a condition at a given index."""
    from ewal.signal import SignalExpr, SignalRef, _compile_expr

    old_index = trace._index
    trace._index = index
    trace._virtual_cache.clear()

    try:
        if condition is True:
            return True
        if condition is False:
            return False
        if isinstance(condition, (SignalExpr, SignalRef)):
            compiled = _compile_expr(condition, trace)
            return trace._core.eval_expr_bool(compiled, index)
        if callable(condition):
            return bool(condition())
        return bool(condition)
    finally:
        trace._index = old_index


def match_pattern(trace, pattern, clock=None) -> list[MatchResult]:
    """Match a temporal pattern against a trace.

    Args:
        trace: The Trace object
        pattern: A SequencePattern or TemporalStep
        clock: Optional clock signal — if provided, only evaluates at rising edges

    Returns:
        List of MatchResult with start/end indices and captures.
    """
    from ewal.signal import SignalExpr, SignalRef, _compile_expr

    # Wrap bare TemporalStep into a SequencePattern
    if isinstance(pattern, TemporalStep):
        pattern = SequencePattern([pattern])

    results = []
    max_idx = trace.max_index

    # Determine which indices to evaluate at
    if clock is not None:
        # Only at rising edges of clock
        rising = _compile_expr(clock.rising() if isinstance(clock, SignalRef) else clock, trace)
        eval_indices = [i for i in range(max_idx + 1)
                        if trace._core.eval_expr_bool(rising, i)]
    else:
        eval_indices = list(range(max_idx + 1))

    # Try matching at each starting index
    for start_pos in range(len(eval_indices)):
        match_result = _try_match(trace, pattern.steps, eval_indices, start_pos)
        if match_result is not None:
            start_idx = eval_indices[start_pos]
            end_idx = eval_indices[match_result]

            # Evaluate captures
            captures = {}
            old_index = trace._index
            trace._index = start_idx
            trace._virtual_cache.clear()
            for name, fn in pattern._capture_fns.items():
                try:
                    captures[name] = fn()
                except Exception:
                    captures[name] = None
            trace._index = old_index

            results.append(MatchResult(start=start_idx, end=end_idx, captures=captures))

    return results


def _try_match(trace, steps, eval_indices, pos) -> Optional[int]:
    """Try to match steps starting at eval_indices[pos].

    Returns the final position index (into eval_indices) if matched, else None.
    """
    current_pos = pos

    for step in steps:
        if current_pos >= len(eval_indices):
            return None

        if isinstance(step, ConditionStep):
            idx = eval_indices[current_pos]
            if not _eval_condition(trace, step.condition, idx):
                return None
            current_pos += 1

        elif isinstance(step, (WithinStep, EventuallyStep)):
            max_c = step.max_cycles
            found = False
            for offset in range(max_c + 1):
                check_pos = current_pos + offset
                if check_pos >= len(eval_indices):
                    break
                idx = eval_indices[check_pos]
                if _eval_condition(trace, step.condition, idx):
                    current_pos = check_pos + 1
                    found = True
                    break
            if not found:
                return None

        elif isinstance(step, RepeatStep):
            for _ in range(step.count):
                if current_pos >= len(eval_indices):
                    return None
                inner_result = _try_match(trace, [step.inner], eval_indices, current_pos)
                if inner_result is None:
                    return None
                current_pos = inner_result

        elif isinstance(step, ConsecutiveStep):
            count = 0
            start_pos = current_pos
            while current_pos < len(eval_indices):
                idx = eval_indices[current_pos]
                if _eval_condition(trace, step.condition, idx):
                    count += 1
                    current_pos += 1
                    if step.max_cycles is not None and count >= step.max_cycles:
                        break
                else:
                    break
            if count < step.min_cycles:
                return None

    return current_pos - 1 if current_pos > pos else pos
