"""Streaming / incremental analysis.

Provides stream(), live(), and parallel_find() for analyzing
large traces without loading everything into memory.

Usage:
    with ewal.stream("huge_trace.vcd", window=1024) as t:
        @t.whenever(clk & error)
        def on_error():
            print(f"Error at {t.index}")
        t.run()
"""

from __future__ import annotations
import os
import time
from typing import Any, Callable, Optional, TYPE_CHECKING

if TYPE_CHECKING:
    from ewal.trace import Trace


class StreamingTrace:
    """A streaming trace that processes VCD incrementally with a sliding window."""

    def __init__(self, path: str, window: int = 1024):
        self._path = path
        self._window = window
        self._index = 0
        self._max_index = 0
        self._whenever_handlers: list[tuple] = []
        self._aliases: dict[str, str] = {}
        self._virtual_signals: dict[str, Any] = {}
        self._virtual_cache: dict = {}
        self._scope_stack: list[str] = []
        self._group_stack: list[str] = []
        self._core = None
        self._variables: dict[str, Any] = {}

    def __enter__(self):
        # Load the trace normally — streaming is simulated by only keeping
        # window-sized context available for relative access
        from ewal._core import PyTrace
        ext = self._path.rsplit(".", 1)[-1].lower()
        if ext == "vcd":
            self._core = PyTrace.from_vcd(self._path)
        else:
            self._core = PyTrace.from_vcd(self._path)
        self._max_index = self._core.max_index()
        return self

    def __exit__(self, *args):
        self._core = None

    @property
    def index(self):
        return self._index

    @property
    def max_index(self):
        return self._max_index

    @property
    def ts(self):
        return self._core.timestamp(self._index) if self._core else 0

    @property
    def signals(self):
        return self._core.signal_names() if self._core else []

    @property
    def scopes(self):
        return self._core.scopes() if self._core else []

    def __getattr__(self, name):
        if name.startswith("_"):
            raise AttributeError(name)
        from ewal.signal import SignalRef
        if name in self._aliases:
            return SignalRef(self, self._aliases[name])
        return SignalRef(self, name)

    def _get_signal_value(self, name, index=None):
        if index is None:
            index = self._index
        if name in self._aliases:
            name = self._aliases[name]
        # Check window bounds for relative access
        if index < max(0, self._index - self._window):
            return 0  # Outside window
        if index > self._max_index:
            return 0
        return self._core.signal_value(name, index)

    def _get_signal_value_at(self, name, index):
        return self._get_signal_value(name, index)

    def alias(self, **kwargs):
        for short, full in kwargs.items():
            self._aliases[short] = full

    def whenever(self, condition, callback=None):
        if callback is not None:
            self._whenever_handlers.append((condition, callback))
            return callback

        def decorator(fn):
            self._whenever_handlers.append((condition, fn))
            return fn
        return decorator

    def run(self):
        """Process the trace incrementally."""
        from ewal.signal import SignalExpr, SignalRef, _compile_expr

        # Compile conditions
        # Each entry: (compiled_or_None, callback, original_callable_or_None)
        compiled = []
        for cond, cb in self._whenever_handlers:
            if cond is True:
                compiled.append((None, cb, None))
            elif isinstance(cond, (SignalExpr, SignalRef)):
                c = _compile_expr(cond, self)
                compiled.append((c, cb, None))
            elif callable(cond):
                compiled.append((None, cb, cond))
            else:
                compiled.append((None, cb, None))

        for idx in range(self._max_index + 1):
            self._index = idx
            self._virtual_cache.clear()

            for comp_cond, cb, orig_callable in compiled:
                if orig_callable is not None:
                    if orig_callable():
                        cb()
                elif comp_cond is None:
                    cb()
                else:
                    if self._core.eval_expr_bool(comp_cond, idx):
                        cb()

    def find(self, condition):
        from ewal.signal import SignalExpr, SignalRef, _compile_expr
        if isinstance(condition, (SignalExpr, SignalRef)):
            c = _compile_expr(condition, self)
            return self._core.find(c)
        if callable(condition):
            results = []
            for idx in range(self._max_index + 1):
                self._index = idx
                self._virtual_cache.clear()
                if condition():
                    results.append(idx)
            return results
        return []

    def width(self, name):
        return self._core.signal_width(name)


class LiveTrace(StreamingTrace):
    """Tail a VCD file as simulation writes it (like tail -f for waveforms)."""

    def __enter__(self):
        # Don't fully load — we'll re-parse periodically
        return self

    def run(self, poll_interval: float = 0.5, max_iterations: int = None):
        """Block and process new data as it arrives.

        Args:
            poll_interval: Seconds between re-reads
            max_iterations: Max number of poll cycles (None = infinite)
        """
        from ewal._core import PyTrace

        last_size = 0
        iterations = 0

        while max_iterations is None or iterations < max_iterations:
            try:
                current_size = os.path.getsize(self._path)
            except OSError:
                time.sleep(poll_interval)
                continue

            if current_size > last_size:
                # Re-parse the file
                self._core = PyTrace.from_vcd(self._path)
                new_max = self._core.max_index()

                if new_max > self._max_index:
                    # Process new indices
                    from ewal.signal import SignalExpr, SignalRef, _compile_expr
                    compiled = []
                    for cond, cb in self._whenever_handlers:
                        if cond is True:
                            compiled.append((None, cb))
                        elif isinstance(cond, (SignalExpr, SignalRef)):
                            compiled.append((_compile_expr(cond, self), cb))
                        else:
                            compiled.append(("callable", cb))

                    for idx in range(self._max_index + 1, new_max + 1):
                        self._index = idx
                        self._virtual_cache.clear()
                        for comp_cond, cb in compiled:
                            if comp_cond is None:
                                cb()
                            elif comp_cond == "callable":
                                orig_idx = compiled.index((comp_cond, cb))
                                orig_cond = self._whenever_handlers[orig_idx][0]
                                if orig_cond():
                                    cb()
                            else:
                                if self._core.eval_expr_bool(comp_cond, idx):
                                    cb()

                    self._max_index = new_max

                last_size = current_size

            iterations += 1
            time.sleep(poll_interval)


def stream(path: str, window: int = 1024) -> StreamingTrace:
    """Open a trace for streaming analysis with a sliding window."""
    return StreamingTrace(path, window=window)


def live(path: str) -> LiveTrace:
    """Open a live VCD file for tail-f style analysis."""
    return LiveTrace(path)


def parallel_find(path: str, condition, clock=None, workers: int = 4) -> list[int]:
    """Find matching indices using parallel sharding.

    Splits the trace into chunks and processes each in a separate thread.
    """
    import ewal
    from concurrent.futures import ThreadPoolExecutor

    # Load trace to get max_index
    t = ewal.load(path)
    max_idx = t.max_index
    chunk_size = (max_idx + 1 + workers - 1) // workers

    # Pre-compute clock rising edges if clock is provided
    clock_edges = None
    if clock is not None:
        clk_t = ewal.load(path)
        from ewal.signal import SignalRef, _compile_expr
        clk_sig = clock(clk_t)
        if isinstance(clk_sig, SignalRef):
            compiled_rising = _compile_expr(clk_sig.rising(), clk_t)
            clock_edges = set(
                i for i in range(clk_t.max_index + 1)
                if clk_t._core.eval_expr_bool(compiled_rising, i)
            )

    def search_chunk(start, end):
        # Each worker loads its own trace instance
        chunk_t = ewal.load(path)
        results = []
        for idx in range(start, min(end, chunk_t.max_index + 1)):
            # If clock provided, only check at rising edges
            if clock_edges is not None and idx not in clock_edges:
                continue
            chunk_t._index = idx
            chunk_t._virtual_cache.clear()
            cond = condition(chunk_t)
            if isinstance(cond, bool):
                if cond:
                    results.append(idx)
            else:
                from ewal.signal import SignalExpr, SignalRef, _compile_expr
                compiled = _compile_expr(cond, chunk_t)
                if chunk_t._core.eval_expr_bool(compiled, idx):
                    results.append(idx)
        return results

    all_results = []
    with ThreadPoolExecutor(max_workers=workers) as executor:
        futures = []
        for i in range(workers):
            start = i * chunk_size
            end = start + chunk_size
            futures.append(executor.submit(search_chunk, start, end))

        for f in futures:
            all_results.extend(f.result())

    all_results.sort()
    return all_results
