from __future__ import annotations

import re
from pathlib import Path

import numpy as np


MIN_IMPEDANCE_OHM = 1e-6


class ImpedanceCurve:
    """Measured loudspeaker impedance magnitude and phase."""

    def __init__(
        self,
        frequency_hz: np.ndarray,
        z_mag_ohm: np.ndarray,
        z_phase_deg: np.ndarray,
        source: Path | None = None,
    ):
        if frequency_hz.ndim != 1 or z_mag_ohm.ndim != 1 or z_phase_deg.ndim != 1:
            raise ValueError("impedance arrays must be one-dimensional")
        if not (len(frequency_hz) == len(z_mag_ohm) == len(z_phase_deg)):
            raise ValueError("impedance arrays must have the same length")
        if len(frequency_hz) < 2:
            raise ValueError("impedance curve needs at least two points")

        valid = np.isfinite(frequency_hz) & np.isfinite(z_mag_ohm) & np.isfinite(z_phase_deg)
        valid &= frequency_hz > 0.0
        valid &= z_mag_ohm > MIN_IMPEDANCE_OHM
        if np.count_nonzero(valid) < 2:
            raise ValueError("impedance curve has fewer than two valid positive-frequency points")

        frequency_hz = frequency_hz[valid].astype(np.float64)
        z_mag_ohm = z_mag_ohm[valid].astype(np.float64)
        z_phase_deg = z_phase_deg[valid].astype(np.float64)

        order = np.argsort(frequency_hz)
        frequency_hz = frequency_hz[order]
        z_mag_ohm = z_mag_ohm[order]
        z_phase_deg = z_phase_deg[order]

        unique_frequency, unique_indices = np.unique(frequency_hz, return_index=True)
        self.frequency_hz = unique_frequency
        self.z_mag_ohm = z_mag_ohm[unique_indices]
        self.z_phase_rad = np.unwrap(np.deg2rad(z_phase_deg[unique_indices]))
        self.source = source

    @classmethod
    def from_file(cls, path: str | Path) -> "ImpedanceCurve":
        path = Path(path)
        rows: list[list[float]] = []
        number_pattern = re.compile(r"[-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?")

        try:
            text = path.read_text(encoding="utf-8-sig", errors="replace")
        except OSError as exc:
            raise SystemExit(f"Could not read impedance file '{path}': {exc}") from exc

        for line in text.splitlines():
            stripped = line.strip()
            if not stripped or stripped.startswith(("#", "*", ";")):
                continue

            parts = re.split(r"[,;\t ]+", stripped)
            values: list[float] = []
            for part in parts:
                if not part:
                    continue
                if number_pattern.fullmatch(part) is None:
                    values = []
                    break
                values.append(float(part))

            if len(values) >= 3:
                rows.append(values[:3])

        if not rows:
            raise SystemExit(
                f"No numeric impedance rows found in '{path}'. Expected columns: "
                "frequency_Hz, Z_mag_ohm, Z_phase_deg."
            )

        data = np.asarray(rows, dtype=np.float64)
        return cls(data[:, 0], data[:, 1], data[:, 2], source=path)

    def interpolate(self, frequencies_hz: np.ndarray) -> np.ndarray:
        """Interpolate complex impedance to requested frequencies."""
        frequencies_hz = np.asarray(frequencies_hz, dtype=np.float64)
        interp_f = np.maximum(frequencies_hz, self.frequency_hz[0])
        log_f_src = np.log(self.frequency_hz)
        log_f_dst = np.log(interp_f)
        log_mag = np.interp(
            log_f_dst,
            log_f_src,
            np.log(self.z_mag_ohm),
            left=np.log(self.z_mag_ohm[0]),
            right=np.log(self.z_mag_ohm[-1]),
        )
        phase = np.interp(
            log_f_dst,
            log_f_src,
            self.z_phase_rad,
            left=self.z_phase_rad[0],
            right=self.z_phase_rad[-1],
        )
        return np.exp(log_mag) * np.exp(1j * phase)

