"""Parse Touchstone (.s1p, .s2p, .snp) S-parameter files for LTspice."""

import re
from dataclasses import dataclass, field
from pathlib import Path

import numpy as np

# Frequency unit multipliers to Hz
_FREQ_MULTIPLIERS: dict[str, float] = {
    "HZ": 1.0,
    "KHZ": 1e3,
    "MHZ": 1e6,
    "GHZ": 1e9,
}


@dataclass
class TouchstoneData:
    """Parsed contents of a Touchstone file.

    All frequencies are stored in Hz regardless of the original file's
    unit.  The ``data`` array holds complex-valued parameters with shape
    ``(n_freq, n_ports, n_ports)``.
    """

    filename: str
    n_ports: int
    freq_unit: str
    parameter_type: str  # S, Y, Z, H, G
    format_type: str  # MA, DB, RI
    reference_impedance: float
    frequencies: np.ndarray  # shape (n_freq,), always in Hz
    data: np.ndarray  # shape (n_freq, n_ports, n_ports), complex128
    comments: list[str] = field(default_factory=list)


def _to_complex(v1: float, v2: float, fmt: str) -> complex:
    """Convert a value pair to complex according to the format type."""
    if fmt == "RI":
        return complex(v1, v2)
    elif fmt == "MA":
        mag = v1
        angle_rad = np.deg2rad(v2)
        return complex(mag * np.cos(angle_rad), mag * np.sin(angle_rad))
    elif fmt == "DB":
        mag = 10.0 ** (v1 / 20.0)
        angle_rad = np.deg2rad(v2)
        return complex(mag * np.cos(angle_rad), mag * np.sin(angle_rad))
    else:
        raise ValueError(f"Unknown format type: {fmt!r}")


def _detect_ports(path: Path) -> int:
    """Detect port count from the file extension (.s<n>p)."""
    suffix = path.suffix.lower()
    m = re.match(r"\.s(\d+)p$", suffix)
    if not m:
        raise ValueError(
            f"Cannot determine port count from extension {suffix!r}. "
            "Expected .s1p, .s2p, .s3p, .s4p, etc."
        )
    return int(m.group(1))


def parse_touchstone(path: str | Path) -> TouchstoneData:
    """Parse a Touchstone file into a TouchstoneData object.

    Handles .s1p through .s4p (and beyond), all three format types
    (MA, DB, RI), all frequency units, and continuation lines used
    by files with more than two ports.

    Args:
        path: Path to the Touchstone file.

    Returns:
        A TouchstoneData with frequencies converted to Hz and
        parameters stored as complex128.
    """
    path = Path(path)
    n_ports = _detect_ports(path)

    # Defaults per Touchstone spec
    freq_unit = "GHZ"
    param_type = "S"
    fmt = "MA"
    ref_impedance = 50.0

    comments: list[str] = []
    data_lines: list[str] = []
    option_found = False

    with path.open() as fh:
        for raw_line in fh:
            line = raw_line.strip()

            # Comment lines
            if line.startswith("!"):
                comments.append(line[1:].strip())
                continue

            # Option line (only the first one is used)
            if line.startswith("#"):
                if not option_found:
                    option_found = True
                    tokens = line[1:].split()
                    # Parse tokens case-insensitively
                    i = 0
                    while i < len(tokens):
                        tok = tokens[i].upper()
                        if tok in _FREQ_MULTIPLIERS:
                            freq_unit = tok
                        elif tok in ("S", "Y", "Z", "H", "G"):
                            param_type = tok
                        elif tok in ("MA", "DB", "RI"):
                            fmt = tok
                        elif tok == "R" and i + 1 < len(tokens):
                            i += 1
                            ref_impedance = float(tokens[i])
                        i += 1
                continue

            # Skip blank lines
            if not line:
                continue

            # Inline comments after data (some files use !)
            if "!" in line:
                line = line[: line.index("!")]

            data_lines.append(line)

    # Compute the expected number of value pairs per frequency point.
    # Each frequency point has n_ports * n_ports parameters, each
    # consisting of two floats.
    values_per_freq = n_ports * n_ports * 2  # pairs * 2 values each

    # Flatten all data tokens
    all_tokens: list[float] = []
    for dl in data_lines:
        all_tokens.extend(float(t) for t in dl.split())

    # Each frequency row starts with the frequency value, followed by
    # values_per_freq data values.  For n_ports <= 2, everything fits
    # on one line.  For n_ports > 2, continuation lines are used and
    # don't repeat the frequency.
    stride = 1 + values_per_freq  # freq + data
    if len(all_tokens) % stride != 0:
        raise ValueError(
            f"Token count {len(all_tokens)} is not a multiple of "
            f"expected stride {stride} for a {n_ports}-port file."
        )

    n_freq = len(all_tokens) // stride
    freq_mult = _FREQ_MULTIPLIERS[freq_unit]

    frequencies = np.empty(n_freq, dtype=np.float64)
    data = np.empty((n_freq, n_ports, n_ports), dtype=np.complex128)

    for k in range(n_freq):
        offset = k * stride
        frequencies[k] = all_tokens[offset] * freq_mult

        # Data values come in pairs: (v1, v2) per parameter
        idx = offset + 1
        for row in range(n_ports):
            for col in range(n_ports):
                v1 = all_tokens[idx]
                v2 = all_tokens[idx + 1]
                data[k, row, col] = _to_complex(v1, v2, fmt)
                idx += 2

    return TouchstoneData(
        filename=path.name,
        n_ports=n_ports,
        freq_unit=freq_unit,
        parameter_type=param_type,
        format_type=fmt,
        reference_impedance=ref_impedance,
        frequencies=frequencies,
        data=data,
        comments=comments,
    )


def get_s_parameter(data: TouchstoneData, i: int, j: int) -> tuple[np.ndarray, np.ndarray]:
    """Extract a single S-parameter across all frequencies.

    Args:
        data: Parsed Touchstone data.
        i: Row index (1-based, as in S(i,j)).
        j: Column index (1-based).

    Returns:
        Tuple of (frequencies_hz, complex_values) where both are 1-D
        numpy arrays.
    """
    if i < 1 or i > data.n_ports:
        raise IndexError(f"Row index {i} out of range for {data.n_ports}-port data")
    if j < 1 or j > data.n_ports:
        raise IndexError(f"Column index {j} out of range for {data.n_ports}-port data")
    return data.frequencies.copy(), data.data[:, i - 1, j - 1].copy()


def s_param_to_db(complex_values: np.ndarray) -> np.ndarray:
    """Convert complex S-parameter values to decibels.

    Computes 20 * log10(|S|), flooring magnitudes at -300 dB to avoid
    log-of-zero warnings.

    Args:
        complex_values: Array of complex S-parameter values.

    Returns:
        Magnitude in dB as a real-valued numpy array.
    """
    magnitude = np.abs(complex_values)
    return 20.0 * np.log10(np.maximum(magnitude, 1e-15))


def generate_ltspice_subcircuit(touchstone_data: TouchstoneData, name: str) -> str:
    """Generate an LTspice-compatible subcircuit wrapping S-parameter data.

    LTspice can reference Touchstone files from within a subcircuit
    using the ``.net`` directive.  This function produces a ``.sub``
    file body that instantiates the S-parameter block.

    Args:
        touchstone_data: Parsed Touchstone data.
        name: Subcircuit name (used in .SUBCKT and filename references).

    Returns:
        A string containing the complete subcircuit definition.
    """
    td = touchstone_data
    n = td.n_ports

    # Build port list: port1, port2, ..., portN, plus a reference node
    port_names = [f"port{k}" for k in range(1, n + 1)]
    port_list = " ".join(port_names)

    lines: list[str] = []
    lines.append(f"* LTspice subcircuit for {td.filename}")
    lines.append(f"* {n}-port {td.parameter_type}-parameters, Z0={td.reference_impedance} Ohm")
    lines.append(
        f"* Frequency range: {td.frequencies[0]:.6g} Hz "
        f"to {td.frequencies[-1]:.6g} Hz "
        f"({len(td.frequencies)} points)"
    )
    lines.append(f".SUBCKT {name} {port_list} ref")
    lines.append(f".net {td.filename} {port_list} ref")
    lines.append(f".ends {name}")

    return "\n".join(lines) + "\n"
