"""Power estimation from waveforms.

Toggle-based power estimation: computes activity factors (toggle rates)
per signal, detects glitches, and provides first-order power estimates.

Usage:
    from ewal.power import TogglePowerEstimator
    power = TogglePowerEstimator(clock=t.tb.clk, clock_freq_hz=100e6)
    activity = power.toggle_rates(t)
"""

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

if TYPE_CHECKING:
    from ewal.trace import Trace
    from ewal.signal import SignalRef


@dataclass
class Glitch:
    signal: str
    index: int
    timestamp: float  # in ns
    width_ns: float


@dataclass
class PowerEstimate:
    total_mw: float = 0.0
    per_signal: dict[str, float] = field(default_factory=dict)

    def top_consumers(self, n: int = 10) -> list[tuple[str, float]]:
        return sorted(self.per_signal.items(), key=lambda x: -x[1])[:n]


class TogglePowerEstimator:
    """Toggle-based power estimator."""

    def __init__(self, clock, clock_freq_hz: float = 100e6, liberty: str = None):
        self.clock = clock
        self.clock_freq_hz = clock_freq_hz
        self.liberty = liberty
        self.has_liberty = liberty is not None
        self._trace = None
        if hasattr(clock, "_trace"):
            self._trace = clock._trace

    def toggle_rates(self, trace: "Trace" = None) -> dict[str, float]:
        """Compute toggle rate (activity factor) for every signal.

        Toggle rate = number of transitions / (2 * number of clock cycles).
        A 50% toggle rate means the signal toggles every clock cycle.
        """
        t = trace or self._trace
        if t is None:
            return {}

        from ewal.signal import _compile_expr
        clk_rising = _compile_expr(self.clock.rising(), t)
        num_clk_edges = sum(1 for i in range(t.max_index + 1)
                            if t._core.eval_expr_bool(clk_rising, i))

        if num_clk_edges == 0:
            return {}

        rates = {}
        for sig_name in t._core.signal_names():
            toggles = 0
            prev_val = None
            for i in range(t.max_index + 1):
                val = t._get_signal_value(sig_name, i)
                if prev_val is not None and val != prev_val:
                    toggles += 1
                prev_val = val

            # Activity factor: toggles per clock edge, divided by 2
            # (one full toggle = 0->1->0 = 2 transitions)
            rates[sig_name] = toggles / (2 * num_clk_edges) if num_clk_edges > 0 else 0

        return rates

    def estimate(self, trace: "Trace" = None) -> PowerEstimate:
        """Estimate dynamic power per signal (requires liberty file).

        Without liberty, uses a default capacitance model.
        """
        t = trace or self._trace
        rates = self.toggle_rates(t)

        # Default capacitance per bit (fF) — rough estimate without liberty
        default_cap_ff = 5.0  # femtofarads
        vdd = 1.0  # Volts

        per_signal = {}
        total = 0.0

        for sig, rate in rates.items():
            width = t._core.signal_width(sig) if t else 1
            # P_dynamic = alpha * C * V^2 * f
            cap = default_cap_ff * 1e-15 * width  # Farads
            power_w = rate * cap * vdd * vdd * self.clock_freq_hz
            power_mw = power_w * 1000
            per_signal[sig] = power_mw
            total += power_mw

        return PowerEstimate(total_mw=total, per_signal=per_signal)

    def find_glitches(self, trace: "Trace" = None,
                      min_width_ns: float = 0.1) -> list[Glitch]:
        """Detect glitches (short pulses shorter than min_width_ns).

        A glitch is when a signal transitions and transitions back
        within min_width_ns.
        """
        t = trace or self._trace
        if t is None:
            return []

        glitches = []

        for sig_name in t._core.signal_names():
            width = t._core.signal_width(sig_name)
            if width > 1:
                continue  # Only check scalar signals for glitches

            prev_val = None
            prev_ts = 0
            transition_start = None
            transition_val = None

            for i in range(t.max_index + 1):
                val = t._get_signal_value(sig_name, i)
                ts = t._core.timestamp(i)

                if prev_val is not None and val != prev_val:
                    if transition_start is not None:
                        # We had a pending transition, and now there's another
                        width_ns = (ts - transition_start)
                        if width_ns > 0 and width_ns < min_width_ns * 1e9:
                            glitches.append(Glitch(
                                signal=sig_name,
                                index=i - 1,
                                timestamp=transition_start,
                                width_ns=width_ns / 1e9 if width_ns > 1e6 else width_ns,
                            ))
                    transition_start = ts
                    transition_val = val

                prev_val = val
                prev_ts = ts

        return glitches
