"""Pure SVG waveform plot generation -- no matplotlib dependency.

Generates complete <svg> XML strings for time-domain, Bode, and spectrum plots
suitable for embedding in HTML or saving as standalone .svg files.
"""

from __future__ import annotations

import math
from html import escape as _html_escape

import numpy as np

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

_ENG_PREFIXES = [
    (1e18, "E"),
    (1e15, "P"),
    (1e12, "T"),
    (1e9, "G"),
    (1e6, "M"),
    (1e3, "k"),
    (1e0, ""),
    (1e-3, "m"),
    (1e-6, "\u00b5"),  # micro sign
    (1e-9, "n"),
    (1e-12, "p"),
    (1e-15, "f"),
    (1e-18, "a"),
]

_FREQ_PREFIXES = [
    (1e9, "G"),
    (1e6, "M"),
    (1e3, "k"),
    (1e0, ""),
    (1e-3, "m"),
]


def _svg_escape(text: str) -> str:
    """Escape special characters for embedding inside SVG XML."""
    return _html_escape(str(text), quote=True)


def _format_eng(value: float, unit: str = "") -> str:
    """Format *value* with an engineering prefix and optional *unit*."""
    if value == 0:
        return f"0{unit}"
    abs_val = abs(value)
    for threshold, prefix in _ENG_PREFIXES:
        if abs_val >= threshold * 0.9999:
            scaled = value / threshold
            # Trim trailing zeros but keep at least one digit
            txt = f"{scaled:.3g}"
            return f"{txt}{prefix}{unit}"
    # Extremely small -- fall back to scientific
    return f"{value:.2e}{unit}"


def _format_freq(hz: float) -> str:
    """Format a frequency value for axis labels (1, 10, 1k, 1M, etc.)."""
    if hz == 0:
        return "0"
    abs_hz = abs(hz)
    for threshold, prefix in _FREQ_PREFIXES:
        if abs_hz >= threshold * 0.9999:
            scaled = hz / threshold
            txt = f"{scaled:.4g}"
            # Strip unnecessary trailing zeros after decimal
            if "." in txt:
                txt = txt.rstrip("0").rstrip(".")
            return f"{txt}{prefix}"
    return f"{hz:.2e}"


def _nice_ticks(vmin: float, vmax: float, n_ticks: int = 5) -> list[float]:
    """Compute human-friendly tick values spanning [vmin, vmax].

    Returns a list of round numbers that cover the data range.
    """
    if vmin == vmax:
        return [vmin]
    if not math.isfinite(vmin) or not math.isfinite(vmax):
        return [0.0]

    raw_step = (vmax - vmin) / max(n_ticks - 1, 1)
    if raw_step == 0:
        return [vmin]

    magnitude = 10 ** math.floor(math.log10(abs(raw_step)))
    residual = raw_step / magnitude
    # Snap to a "nice" step: 1, 2, 2.5, 5, 10
    if residual <= 1.0:
        nice_step = magnitude
    elif residual <= 2.0:
        nice_step = 2 * magnitude
    elif residual <= 2.5:
        nice_step = 2.5 * magnitude
    elif residual <= 5.0:
        nice_step = 5 * magnitude
    else:
        nice_step = 10 * magnitude

    tick_min = math.floor(vmin / nice_step) * nice_step
    tick_max = math.ceil(vmax / nice_step) * nice_step
    ticks: list[float] = []
    t = tick_min
    while t <= tick_max + nice_step * 0.001:
        ticks.append(round(t, 12))
        t += nice_step
    return ticks


def _log_ticks(vmin: float, vmax: float) -> list[float]:
    """Generate tick values at powers of 10 spanning [vmin, vmax] (linear values)."""
    if vmin <= 0:
        vmin = 1.0
    if vmax <= vmin:
        vmax = vmin * 10
    low = math.floor(math.log10(vmin))
    high = math.ceil(math.log10(vmax))
    ticks = [10.0**i for i in range(low, high + 1)]
    # Filter to range
    return [t for t in ticks if vmin * 0.9999 <= t <= vmax * 1.0001]


def _data_extent(arr: np.ndarray, pad_frac: float = 0.05) -> tuple[float, float]:
    """Return (min, max) of *arr* with *pad_frac* padding on each side."""
    if len(arr) == 0:
        return (0.0, 1.0)
    lo, hi = float(np.nanmin(arr)), float(np.nanmax(arr))
    if lo == hi:
        lo -= 1.0
        hi += 1.0
    span = hi - lo
    return (lo - span * pad_frac, hi + span * pad_frac)


# ---------------------------------------------------------------------------
# Core SVG building blocks
# ---------------------------------------------------------------------------

_FONT = "system-ui, -apple-system, sans-serif"


def _build_path_d(
    xs: np.ndarray,
    ys: np.ndarray,
    x_min: float,
    x_max: float,
    y_min: float,
    y_max: float,
    plot_x: float,
    plot_y: float,
    plot_w: float,
    plot_h: float,
    log_x: bool = False,
) -> str:
    """Convert data arrays into an SVG path *d* attribute string."""
    if len(xs) == 0:
        return ""

    # Map data -> pixel coords
    if log_x:
        safe_xs = np.clip(xs, max(x_min, 1e-30), None)
        lx = np.log10(safe_xs)
        lx_min = math.log10(max(x_min, 1e-30))
        lx_max = math.log10(max(x_max, 1e-30))
        denom_x = lx_max - lx_min if lx_max != lx_min else 1.0
        px = plot_x + (lx - lx_min) / denom_x * plot_w
    else:
        denom_x = x_max - x_min if x_max != x_min else 1.0
        px = plot_x + (xs - x_min) / denom_x * plot_w

    denom_y = y_max - y_min if y_max != y_min else 1.0
    # Y axis is inverted in SVG (top = 0)
    py = plot_y + plot_h - (ys - y_min) / denom_y * plot_h

    parts = [f"M{px[0]:.2f},{py[0]:.2f}"]
    for i in range(1, len(px)):
        parts.append(f"L{px[i]:.2f},{py[i]:.2f}")
    return "".join(parts)


def _render_subplot(
    *,
    xs: np.ndarray,
    ys: np.ndarray,
    x_min: float,
    x_max: float,
    y_min: float,
    y_max: float,
    plot_x: float,
    plot_y: float,
    plot_w: float,
    plot_h: float,
    log_x: bool,
    xlabel: str,
    ylabel: str,
    title: str | None,
    stroke: str,
    x_ticks: list[float] | None = None,
    y_ticks: list[float] | None = None,
    show_x_labels: bool = True,
) -> str:
    """Render a single subplot region as a block of SVG elements."""
    lines: list[str] = []

    # Compute ticks
    if y_ticks is None:
        y_ticks = _nice_ticks(y_min, y_max, n_ticks=6)

    if x_ticks is None:
        if log_x:
            x_ticks = _log_ticks(x_min, x_max)
        else:
            x_ticks = _nice_ticks(x_min, x_max, n_ticks=6)

    # Background
    lines.append(
        f'<rect x="{plot_x}" y="{plot_y}" width="{plot_w}" height="{plot_h}" '
        f'fill="white" stroke="#ccc" stroke-width="1"/>'
    )

    # Grid + Y tick labels
    denom_y = y_max - y_min if y_max != y_min else 1.0
    for tv in y_ticks:
        if tv < y_min or tv > y_max:
            continue
        py = plot_y + plot_h - (tv - y_min) / denom_y * plot_h
        lines.append(
            f'<line x1="{plot_x}" y1="{py:.1f}" x2="{plot_x + plot_w}" y2="{py:.1f}" '
            f'stroke="#ddd" stroke-width="0.5" stroke-dasharray="4,3"/>'
        )
        label = _format_eng(tv)
        lines.append(
            f'<text x="{plot_x - 8}" y="{py:.1f}" text-anchor="end" '
            f'dominant-baseline="middle" font-size="11" font-family="{_FONT}" '
            f'fill="#444">{_svg_escape(label)}</text>'
        )

    # Grid + X tick labels
    if log_x:
        lx_min = math.log10(max(x_min, 1e-30))
        lx_max = math.log10(max(x_max, 1e-30))
        denom_x = lx_max - lx_min if lx_max != lx_min else 1.0
    else:
        denom_x = x_max - x_min if x_max != x_min else 1.0

    for tv in x_ticks:
        if log_x:
            if tv <= 0:
                continue
            frac = (math.log10(tv) - lx_min) / denom_x
        else:
            frac = (tv - x_min) / denom_x
        if frac < -0.001 or frac > 1.001:
            continue
        px = plot_x + frac * plot_w
        lines.append(
            f'<line x1="{px:.1f}" y1="{plot_y}" x2="{px:.1f}" y2="{plot_y + plot_h}" '
            f'stroke="#ddd" stroke-width="0.5" stroke-dasharray="4,3"/>'
        )
        if show_x_labels:
            if log_x:
                label = _format_freq(tv)
            else:
                label = _format_eng(tv)
            lines.append(
                f'<text x="{px:.1f}" y="{plot_y + plot_h + 16}" text-anchor="middle" '
                f'font-size="11" font-family="{_FONT}" fill="#444">'
                f'{_svg_escape(label)}</text>'
            )

    # Data path
    d = _build_path_d(xs, ys, x_min, x_max, y_min, y_max, plot_x, plot_y, plot_w, plot_h, log_x)
    if d:
        lines.append(
            f'<path d="{d}" fill="none" stroke="{stroke}" stroke-width="1.5" '
            f'stroke-linejoin="round" stroke-linecap="round"/>'
        )

    # Title
    if title:
        lines.append(
            f'<text x="{plot_x + plot_w / 2}" y="{plot_y - 12}" text-anchor="middle" '
            f'font-size="14" font-weight="600" font-family="{_FONT}" fill="#111">'
            f'{_svg_escape(title)}</text>'
        )

    # Axis labels
    if ylabel:
        mid_y = plot_y + plot_h / 2
        lines.append(
            f'<text x="{plot_x - 55}" y="{mid_y}" text-anchor="middle" '
            f'font-size="12" font-family="{_FONT}" fill="#333" '
            f'transform="rotate(-90, {plot_x - 55}, {mid_y})">'
            f'{_svg_escape(ylabel)}</text>'
        )

    if xlabel and show_x_labels:
        lines.append(
            f'<text x="{plot_x + plot_w / 2}" y="{plot_y + plot_h + 42}" '
            f'text-anchor="middle" font-size="12" font-family="{_FONT}" fill="#333">'
            f'{_svg_escape(xlabel)}</text>'
        )

    return "\n".join(lines)


def _wrap_svg(inner: str, width: int, height: int) -> str:
    """Wrap inner SVG elements in a root <svg> tag with white background."""
    return (
        f'<svg xmlns="http://www.w3.org/2000/svg" width="{width}" height="{height}" '
        f'viewBox="0 0 {width} {height}">\n'
        f'<rect width="{width}" height="{height}" fill="white"/>\n'
        f'{inner}\n'
        f'</svg>'
    )


# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------


def plot_timeseries(
    time: list[float] | np.ndarray,
    values: list[float] | np.ndarray,
    title: str = "Time Domain",
    ylabel: str = "Voltage (V)",
    width: int = 800,
    height: int = 400,
) -> str:
    """Plot a time-domain signal. Linear X axis (time), linear Y axis.

    Returns a complete ``<svg>`` XML string.
    """
    t = np.asarray(time, dtype=float).ravel()
    v = np.asarray(values, dtype=float).ravel()
    n = min(len(t), len(v))
    t, v = t[:n], v[:n]

    margin_l, margin_r, margin_t, margin_b = 80, 20, 40, 60
    plot_x = float(margin_l)
    plot_y = float(margin_t)
    plot_w = float(width - margin_l - margin_r)
    plot_h = float(height - margin_t - margin_b)

    x_min, x_max = _data_extent(t)
    y_min, y_max = _data_extent(v)

    inner = _render_subplot(
        xs=t,
        ys=v,
        x_min=x_min,
        x_max=x_max,
        y_min=y_min,
        y_max=y_max,
        plot_x=plot_x,
        plot_y=plot_y,
        plot_w=plot_w,
        plot_h=plot_h,
        log_x=False,
        xlabel="Time (s)",
        ylabel=ylabel,
        title=title,
        stroke="#2563eb",
    )
    return _wrap_svg(inner, width, height)


def plot_bode(
    freq: list[float] | np.ndarray,
    mag_db: list[float] | np.ndarray,
    phase_deg: list[float] | np.ndarray | None = None,
    title: str = "Bode Plot",
    width: int = 800,
    height: int = 500,
) -> str:
    """Plot frequency response (Bode plot). Log10 X, linear Y (dB).

    If *phase_deg* is provided, the SVG contains two stacked subplots:
    magnitude on top, phase on the bottom.

    Returns a complete ``<svg>`` XML string.
    """
    f = np.asarray(freq, dtype=float).ravel()
    m = np.asarray(mag_db, dtype=float).ravel()
    n = min(len(f), len(m))
    f, m = f[:n], m[:n]

    has_phase = phase_deg is not None
    if has_phase:
        p = np.asarray(phase_deg, dtype=float).ravel()
        n = min(len(f), len(p))
        f, m, p = f[:n], m[:n], p[:n]

    margin_l, margin_r, margin_t, margin_b = 80, 20, 40, 60
    plot_x = float(margin_l)
    plot_w = float(width - margin_l - margin_r)

    # Frequency range (shared)
    f_min, f_max = _data_extent(f[f > 0] if np.any(f > 0) else f, pad_frac=0.0)
    if f_min <= 0:
        f_min = 1.0
    freq_ticks = _log_ticks(f_min, f_max)

    if has_phase:
        # Split into two subplots with a gap
        gap = 30
        available_h = height - margin_t - margin_b - gap
        mag_h = available_h * 0.55
        phase_h = available_h * 0.45
        mag_y = float(margin_t)
        phase_y = float(margin_t + mag_h + gap)

        m_min, m_max = _data_extent(m)
        p_min, p_max = _data_extent(p)

        mag_svg = _render_subplot(
            xs=f,
            ys=m,
            x_min=f_min,
            x_max=f_max,
            y_min=m_min,
            y_max=m_max,
            plot_x=plot_x,
            plot_y=mag_y,
            plot_w=plot_w,
            plot_h=mag_h,
            log_x=True,
            xlabel="",
            ylabel="Magnitude (dB)",
            title=title,
            stroke="#2563eb",
            x_ticks=freq_ticks,
            show_x_labels=False,
        )
        phase_svg = _render_subplot(
            xs=f,
            ys=p,
            x_min=f_min,
            x_max=f_max,
            y_min=p_min,
            y_max=p_max,
            plot_x=plot_x,
            plot_y=phase_y,
            plot_w=plot_w,
            plot_h=phase_h,
            log_x=True,
            xlabel="Frequency (Hz)",
            ylabel="Phase (deg)",
            title=None,
            stroke="#dc2626",
            x_ticks=freq_ticks,
        )
        return _wrap_svg(mag_svg + "\n" + phase_svg, width, height)

    else:
        plot_y = float(margin_t)
        plot_h = float(height - margin_t - margin_b)
        m_min, m_max = _data_extent(m)

        inner = _render_subplot(
            xs=f,
            ys=m,
            x_min=f_min,
            x_max=f_max,
            y_min=m_min,
            y_max=m_max,
            plot_x=plot_x,
            plot_y=plot_y,
            plot_w=plot_w,
            plot_h=plot_h,
            log_x=True,
            xlabel="Frequency (Hz)",
            ylabel="Magnitude (dB)",
            title=title,
            stroke="#2563eb",
            x_ticks=freq_ticks,
        )
        return _wrap_svg(inner, width, height)


def plot_spectrum(
    freq: list[float] | np.ndarray,
    mag_db: list[float] | np.ndarray,
    title: str = "FFT Spectrum",
    width: int = 800,
    height: int = 400,
) -> str:
    """Plot an FFT spectrum. Log10 X axis, linear Y axis (dB).

    Returns a complete ``<svg>`` XML string.
    """
    f = np.asarray(freq, dtype=float).ravel()
    m = np.asarray(mag_db, dtype=float).ravel()
    n = min(len(f), len(m))
    f, m = f[:n], m[:n]

    margin_l, margin_r, margin_t, margin_b = 80, 20, 40, 60
    plot_x = float(margin_l)
    plot_y = float(margin_t)
    plot_w = float(width - margin_l - margin_r)
    plot_h = float(height - margin_t - margin_b)

    f_min, f_max = _data_extent(f[f > 0] if np.any(f > 0) else f, pad_frac=0.0)
    if f_min <= 0:
        f_min = 1.0
    m_min, m_max = _data_extent(m)

    inner = _render_subplot(
        xs=f,
        ys=m,
        x_min=f_min,
        x_max=f_max,
        y_min=m_min,
        y_max=m_max,
        plot_x=plot_x,
        plot_y=plot_y,
        plot_w=plot_w,
        plot_h=plot_h,
        log_x=True,
        xlabel="Frequency (Hz)",
        ylabel="Magnitude (dB)",
        title=title,
        stroke="#2563eb",
    )
    return _wrap_svg(inner, width, height)
