"""Spec-backed runtime classes for modified NACA 4-digit airfoils."""

from __future__ import annotations

from copy import deepcopy
from typing import Literal, Self

from .airfoil import OrthogonalAirfoil
from .airfoil_schema import (
    Naca4ModifiedAirfoilParamsSpec,
    Naca4ModifiedAirfoilSpec,
)
from .naca4_airfoil import (
    NACA4_MAX_CAMBER_LIMIT,
    NACA4_MAX_CAMBER_LOCATION_LIMIT,
    NACA4_MAX_THICKNESS_LIMIT,
)
from .naca4_camber import Naca4DigitCamber
from .naca45_modified_thickness import (
    LEADING_EDGE_INDEX_MAX,
    LEADING_EDGE_INDEX_MIN,
    MAX_THICKNESS_LOCATION_MAX,
    MAX_THICKNESS_LOCATION_MIN,
    Naca45DigitModifiedThicknessClassic,
    Naca45DigitModifiedThicknessParams,
)

NACA4_MODIFIED_DESIGNATION_LENGTH = 7
NACA4_MODIFIED_DESIGNATION_DASH_INDEX = 4
NACA4_MODIFIED_MIN_THICKNESS_LOCATION = 0.1


def _validate_naca4_modified_designation(designation: str) -> None:
    """
    Validate a modified NACA 4-digit designation string.

    Parameters
    ----------
    designation : str
        Modified NACA 4-digit designation string in ``####-##`` form.

    Raises
    ------
    ValueError
        If ``designation`` does not match the supported modified-series
        format.
    """
    if len(designation) != NACA4_MODIFIED_DESIGNATION_LENGTH:
        raise ValueError(
            "Modified NACA 4-digit designations must be 7 characters."
        )
    if designation[NACA4_MODIFIED_DESIGNATION_DASH_INDEX] != "-":
        raise ValueError(
            "Modified NACA 4-digit designations must use the form ####-##."
        )
    digits = designation.replace("-", "")
    if not digits.isdigit():
        raise ValueError(
            "Modified NACA 4-digit designations must contain only digits."
        )


def _validate_naca4_modified_params(
    params: Naca4ModifiedAirfoilParamsSpec,
) -> None:
    """
    Validate the explicit modified NACA 4-digit parameters.

    Parameters
    ----------
    params : Naca4ModifiedAirfoilParamsSpec
        Explicit modified NACA 4-digit parameter specification to validate.

    Raises
    ------
    ValueError
        If any parameter violates the supported modified-series bounds or
        consistency rules.
    """
    if not 0.0 <= params.m < NACA4_MAX_CAMBER_LIMIT:
        raise ValueError(
            "Modified NACA 4-digit parameter m must satisfy 0 <= m < 0.1."
        )
    if not 0.0 <= params.p <= NACA4_MAX_CAMBER_LOCATION_LIMIT:
        raise ValueError(
            "Modified NACA 4-digit parameter p must satisfy 0 <= p <= 0.9."
        )
    if not 0.0 <= params.t <= NACA4_MAX_THICKNESS_LIMIT:
        raise ValueError(
            "Modified NACA 4-digit parameter t must satisfy 0 <= t <= 0.4."
        )
    if params.m == 0.0 and params.p != 0.0:
        raise ValueError(
            "Modified NACA 4-digit parameter p must be 0 when m is 0."
        )
    if params.m > 0.0 and params.p <= 0.0:
        raise ValueError(
            "Modified NACA 4-digit parameter p must be positive when m > 0."
        )
    if (
        not LEADING_EDGE_INDEX_MIN
        <= params.leading_edge_index
        < (LEADING_EDGE_INDEX_MAX)
    ):
        raise ValueError(
            "Modified NACA 4-digit leading_edge_index must satisfy "
            "1 <= leading_edge_index < 10."
        )
    if not (
        NACA4_MODIFIED_MIN_THICKNESS_LOCATION
        <= params.max_thickness_location
        < 1.0
    ):
        raise ValueError(
            "Modified NACA 4-digit max_thickness_location must satisfy "
            "0.1 <= max_thickness_location < 1.0."
        )


def _copy_naca4_modified_spec(
    spec: Naca4ModifiedAirfoilSpec,
) -> Naca4ModifiedAirfoilSpec:
    """
    Return a defensive copy of a modified NACA 4-digit spec.

    Parameters
    ----------
    spec : Naca4ModifiedAirfoilSpec
        Source specification to copy.

    Returns
    -------
    Naca4ModifiedAirfoilSpec
        Deep copy of ``spec``.
    """
    return deepcopy(spec)


def _designation_to_lmti(designation: str) -> float:
    """
    Return the max-thickness-location index from a designation.

    Parameters
    ----------
    designation : str
        Modified NACA 4-digit designation string.

    Returns
    -------
    float
        Max-thickness-location index extracted from ``designation``.

    Raises
    ------
    ValueError
        If the designation encodes an unsupported thickness-location index.
    """
    lmti = float(designation[6])
    if not MAX_THICKNESS_LOCATION_MIN <= lmti < MAX_THICKNESS_LOCATION_MAX:
        raise ValueError(
            "Modified NACA 4-digit max thickness location must be 1-9."
        )
    return lmti


class Naca4ModifiedAirfoilClassic(OrthogonalAirfoil):
    """
    Classic modified NACA 4-digit airfoil built from a designation.

    Parameters
    ----------
    spec : Naca4ModifiedAirfoilSpec
        Serialized specification containing a modified-series designation.

    Raises
    ------
    ValueError
        If ``spec`` does not contain a valid designation-only definition.
    """

    def __init__(self, spec: Naca4ModifiedAirfoilSpec) -> None:
        """
        Build a classic modified NACA 4-digit airfoil from its spec.

        Parameters
        ----------
        spec : Naca4ModifiedAirfoilSpec
            Serialized specification containing a modified-series
            designation.

        Raises
        ------
        ValueError
            If ``spec`` does not contain a valid designation-only
            definition.
        """
        if spec.designation is None or spec.params is not None:
            raise ValueError(
                "Classic modified NACA 4-digit airfoils require "
                "designation-only specs."
            )
        _validate_naca4_modified_designation(spec.designation)
        lmti = _designation_to_lmti(spec.designation)
        camber = Naca4DigitCamber(
            mci=float(spec.designation[0]),
            lci=float(spec.designation[1]),
        )
        thickness = Naca45DigitModifiedThicknessClassic(
            mti=float(spec.designation[2:4]),
            lei=float(spec.designation[5]),
            lmti=lmti,
        )
        super().__init__(camber=camber, thickness=thickness)
        self._spec = _copy_naca4_modified_spec(spec)

    @classmethod
    def from_designation(cls, designation: str) -> Self:
        """
        Build a classic modified NACA 4-digit airfoil from a designation.

        Parameters
        ----------
        designation : str
            Modified NACA 4-digit designation such as ``"0003-64"``.

        Returns
        -------
        Self
            Runtime airfoil built from ``designation``.

        Raises
        ------
        ValueError
            If ``designation`` is not a valid modified NACA 4-digit code.
        """
        return cls(Naca4ModifiedAirfoilSpec(designation=designation))

    @property
    def spec(self) -> Naca4ModifiedAirfoilSpec:
        """
        Return the source spec for this airfoil.

        Returns
        -------
        Naca4ModifiedAirfoilSpec
            Defensive copy of the serialized source spec.
        """
        return _copy_naca4_modified_spec(self._spec)

    def to_spec(self) -> Naca4ModifiedAirfoilSpec:
        """
        Return the schema definition needed to recreate this airfoil.

        Returns
        -------
        Naca4ModifiedAirfoilSpec
            Serialized source spec for this airfoil.
        """
        return self.spec


class Naca4ModifiedAirfoilParams(OrthogonalAirfoil):
    """
    Parametric modified NACA 4-digit airfoil built from explicit params.

    Parameters
    ----------
    spec : Naca4ModifiedAirfoilSpec
        Serialized specification containing explicit modified-series
        parameters.

    Raises
    ------
    ValueError
        If ``spec`` does not contain a valid params-only definition.
    """

    def __init__(self, spec: Naca4ModifiedAirfoilSpec) -> None:
        """
        Build a parametric modified NACA 4-digit airfoil from its spec.

        Parameters
        ----------
        spec : Naca4ModifiedAirfoilSpec
            Serialized specification containing explicit modified-series
            parameters.

        Raises
        ------
        ValueError
            If ``spec`` does not contain a valid params-only definition.
        """
        if spec.designation is not None or spec.params is None:
            raise ValueError(
                "Parametric modified NACA 4-digit airfoils require "
                "params-only specs."
            )
        _validate_naca4_modified_params(spec.params)
        camber = Naca4DigitCamber(
            mci=100.0 * spec.params.m,
            lci=10.0 * spec.params.p,
        )
        thickness = Naca45DigitModifiedThicknessParams(
            mti=100.0 * spec.params.t,
            lei=spec.params.leading_edge_index,
            lmti=10.0 * spec.params.max_thickness_location,
            closed_te=spec.params.trailing_edge == "sharp",
        )
        super().__init__(camber=camber, thickness=thickness)
        self._spec = _copy_naca4_modified_spec(spec)

    @classmethod
    def from_params(
        cls,
        *,
        m: float,
        p: float,
        t: float,
        leading_edge_index: float,
        max_thickness_location: float,
        trailing_edge: Literal["standard", "sharp"] = "standard",
    ) -> Self:
        """
        Build a parametric modified NACA 4-digit airfoil from params.

        Parameters
        ----------
        m : float
            Maximum camber as a fraction of chord.
        p : float
            Chordwise location of maximum camber as a fraction of chord.
        t : float
            Maximum thickness as a fraction of chord.
        leading_edge_index : float
            Modified-series leading-edge shape index.
        max_thickness_location : float
            Chordwise location of maximum thickness as a fraction of chord.
        trailing_edge : {"standard", "sharp"}, default="standard"
            Trailing-edge closure model for the thickness distribution.

        Returns
        -------
        Self
            Runtime airfoil built from the explicit parameters.

        Raises
        ------
        ValueError
            If the parameter set falls outside the supported modified-series
            range.
        """
        params = Naca4ModifiedAirfoilParamsSpec(
            m=m,
            p=p,
            t=t,
            leading_edge_index=leading_edge_index,
            max_thickness_location=max_thickness_location,
            trailing_edge=trailing_edge,
        )
        return cls(Naca4ModifiedAirfoilSpec(params=params))

    @property
    def spec(self) -> Naca4ModifiedAirfoilSpec:
        """
        Return the source spec for this airfoil.

        Returns
        -------
        Naca4ModifiedAirfoilSpec
            Defensive copy of the serialized source spec.
        """
        return _copy_naca4_modified_spec(self._spec)

    def to_spec(self) -> Naca4ModifiedAirfoilSpec:
        """
        Return the schema definition needed to recreate this airfoil.

        Returns
        -------
        Naca4ModifiedAirfoilSpec
            Serialized source spec for this airfoil.
        """
        return self.spec


__all__ = [
    "Naca4ModifiedAirfoilClassic",
    "Naca4ModifiedAirfoilParams",
]
