"""NACA 5-digit airfoil camber relations."""

import numpy as np
from scipy.optimize import root_scalar

from buffalo_wings.type_aliases import FloatArray, FloatScalar

from .camber_base import (
    CLASSIC_NACA_5DIGIT_IDEAL_LIFT_INDEX,
    CLASSIC_NACA_5DIGIT_LIFT_MAX,
    CLASSIC_NACA_5DIGIT_LIFT_MIN,
    CLASSIC_NACA_5DIGIT_LOCATION_MAX,
    CLASSIC_NACA_5DIGIT_LOCATION_MIN,
    CLASSIC_NACA_5DIGIT_PARAMETERS,
    REFLEXED_NACA_5DIGIT_PARAMETERS,
    Camber,
)


class Naca5DigitCamberClassic(Camber):
    """
    Class for the classic NACA 5-digit airfoil camber.

    Notes
    -----
    The only valid values for the relative chord location of maximum camber
    term are 1, 2, 3, 4, and 5. The only valid value for the ideal lift
    coefficient term is 2.
    """

    def __init__(self, lci: float, mci: float) -> None:
        """
        Initialize a classic NACA 5-digit camber line.

        Parameters
        ----------
        lci : float
            Ideal-lift-coefficient index from the designation.
        mci : float
            Maximum-camber-location index from the designation.

        Raises
        ------
        ValueError
            If either designation index is unsupported.
        """
        self._m = 0.2
        self._k1 = 0.0
        self._p_setter(mci)
        self._lci_setter(lci)

    @property
    def lift_coefficient_index(self) -> float:
        """
        Return the ideal-lift-coefficient index.

        Returns
        -------
        float
            Ideal lift coefficient expressed as a designation index.
        """
        return (20.0 / 3.0) * self._lci

    @lift_coefficient_index.setter
    def lift_coefficient_index(self, lci: float) -> None:
        """
        Update the ideal lift coefficient index.

        Parameters
        ----------
        lci : float
            Ideal-lift-coefficient index from the designation.
        """
        self._lci_setter(lci)

    @property
    def max_camber_index(self) -> float:
        """
        Return the maximum-camber-location index.

        Returns
        -------
        float
            Chordwise location of maximum camber as a designation index.
        """
        return 20.0 * self._p

    @max_camber_index.setter
    def max_camber_index(self, mci: float) -> None:
        """
        Update the maximum-camber-location index.

        Parameters
        ----------
        mci : float
            Maximum-camber-location index from the designation.
        """
        self._p_setter(mci)

    @property
    def m(self) -> float:
        """
        Return the camber transition location.

        Returns
        -------
        float
            Relative chord location of the camber transition point.
        """
        return self._m

    @property
    def k1(self) -> float:
        """
        Return the primary camber scale factor.

        Returns
        -------
        float
            Scale factor applied to the camber relation.
        """
        return self._k1

    def joints(self) -> list[float]:
        """
        Return the locations of any joints/discontinuities in the camber line.

        Returns
        -------
        List[float]
            Xi-coordinates of any discontinuities.
        """
        return [0.0, self.m, 1.0]

    def max_camber_parameter(self) -> FloatScalar:
        """
        Return parameter where the camber is maximum.

        Returns
        -------
        float
            Parameter where camber is maximum.
        """
        return self._p

    def _y(self, t: FloatArray) -> FloatArray:
        """
        Return the camber location at specified chord location.

        Parameters
        ----------
        t : numpy.ndarray
            Chord location of interest.

        Returns
        -------
        numpy.ndarray
            Camber at specified point.
        """
        t = np.asarray(t, dtype=np.float64)

        def fore(t: FloatArray) -> FloatArray:
            m = self.m
            return (self.k1 / 6) * (t**3 - 3 * m * t**2 + m**2 * (3 - m) * t)

        def aft(t: FloatArray) -> FloatArray:
            return (self.k1 * self.m**3 / 6) * (1 - t)

        return np.piecewise(t, [t <= self.m, t > self.m], [fore, aft])

    def _y_t(self, t: FloatArray) -> FloatArray:
        """
        Return first derivative of camber at specified chord location.

        Parameters
        ----------
        t : numpy.ndarray
            Chord location of interest.

        Returns
        -------
        numpy.ndarray
            First derivative of camber at specified point.
        """
        t = np.asarray(t, dtype=np.float64)
        m = self.m

        def fore(t: FloatArray) -> FloatArray:
            return (self.k1 / 6) * (3 * t**2 - 6 * m * t + m**2 * (3 - m))

        def aft(t: FloatArray) -> FloatArray:
            return -(self.k1 * m**3 / 6) * np.ones_like(t)

        return np.piecewise(t, [t <= m, t > m], [fore, aft])

    def _y_tt(self, t: FloatArray) -> FloatArray:
        """
        Return second derivative of camber at specified chord location.

        Parameters
        ----------
        t : numpy.ndarray
            Chord location of interest.

        Returns
        -------
        numpy.ndarray
            Second derivative of camber at specified point.
        """
        t = np.asarray(t, dtype=np.float64)

        def fore(t: FloatArray) -> FloatArray:
            return (self.k1) * (t - self.m)

        def aft(t: FloatArray) -> FloatArray:
            return np.zeros_like(t)

        return np.piecewise(t, [t <= self.m, t > self.m], [fore, aft])

    def _y_ttt(self, t: FloatArray) -> FloatArray:
        """
        Return third derivative of camber at specified chord location.

        Parameters
        ----------
        t : numpy.ndarray
            Chord location of interest.

        Returns
        -------
        numpy.ndarray
            Third derivative of camber at specified point.
        """
        t = np.asarray(t, dtype=np.float64)

        def fore(t: FloatArray) -> FloatArray:
            return self.k1 * np.ones_like(t)

        def aft(t: FloatArray) -> FloatArray:
            return np.zeros_like(t)

        return np.piecewise(t, [t <= self.m, t > self.m], [fore, aft])

    def _p_setter(self, mci: float) -> None:
        """
        Map a classic 5-digit camber index to tabulated parameters.

        Parameters
        ----------
        mci : float
            Maximum-camber-location index from the designation.

        Raises
        ------
        ValueError
            If ``mci`` is not a supported classic 5-digit index.
        """
        parameters = CLASSIC_NACA_5DIGIT_PARAMETERS.get(int(mci))
        if parameters is None:
            raise ValueError(
                f"Invalid NACA 5-digit maximum camber location: {mci}."
            )
        self._m, self._k1 = parameters

        self._p = mci / 20.0

    def _lci_setter(self, lci: float) -> None:
        """
        Validate and store the classic 5-digit lift index.

        Parameters
        ----------
        lci : float
            Ideal-lift-coefficient index from the designation.

        Raises
        ------
        ValueError
            If ``lci`` is not the supported classic 5-digit index.
        """
        if lci != CLASSIC_NACA_5DIGIT_IDEAL_LIFT_INDEX:
            raise ValueError(
                f"Invalid NACA 5-Digit ideal lift coefficient parameter: {lci}."
            )
        self._lci = (3.0 / 20.0) * lci


class Naca5DigitCamberParams(Naca5DigitCamberClassic):
    """
    Camber for the regular NACA 5-digit airfoils.

    The valid range of the relative chord location of maximum camber term is
    [1, 6). The valid range for the ideal lift coefficient term is [1, 4).
    """

    def __init__(self, lci: float, mci: float) -> None:
        """
        Initialize a continuous-index NACA 5-digit camber line.

        Parameters
        ----------
        lci : float
            Ideal-lift-coefficient index.
        mci : float
            Maximum-camber-location index.
        """
        # Need to bootstrap initialization
        self._lci = (3.0 / 20.0) * lci
        super().__init__(lci=lci, mci=mci)

    def _p_setter(self, mci: float) -> None:
        """
        Solve for continuous-index 5-digit camber parameters from ``mci``.

        Parameters
        ----------
        mci : float
            Maximum-camber-location index.

        Raises
        ------
        ValueError
            If ``mci`` is outside the supported continuous-index 5-digit
            range.
        """
        if (
            mci < CLASSIC_NACA_5DIGIT_LOCATION_MIN
            or mci >= CLASSIC_NACA_5DIGIT_LOCATION_MAX
        ):
            raise ValueError(
                "Invalid NACA 5-digit maximum camber location parameter: "
                f"{mci}."
            )

        self._p = mci / 20.0

        def camber_slope(m: float) -> float:
            return 3 * self._p**2 - 6 * m * self._p + m**2 * (3 - m)

        root = root_scalar(camber_slope, bracket=(self._p, 2 * self._p))
        self._m = float(root.root)
        self._determine_k1()

    def _lci_setter(self, lci: float) -> None:
        """
        Validate and store the continuous-index 5-digit lift index.

        Parameters
        ----------
        lci : float
            Ideal-lift-coefficient index.

        Raises
        ------
        ValueError
            If ``lci`` is outside the supported continuous-index 5-digit
            range.
        """
        if lci < CLASSIC_NACA_5DIGIT_LIFT_MIN or lci >= (
            CLASSIC_NACA_5DIGIT_LIFT_MAX
        ):
            raise ValueError(
                f"Invalid NACA 5-digit ideal lift coefficient parameter: {lci}."
            )

        self._lci = (3.0 / 20.0) * lci
        self._determine_k1()

    def _determine_k1(self) -> None:
        """
        Compute the continuous-index 5-digit camber scale factor.

        Returns
        -------
        None
            This method updates the stored scale factor in place.
        """
        self._k1 = (
            6
            * self._lci
            / (
                -3 / 2 * (1 - 2 * self._m) * np.arccos(1 - 2 * self._m)
                + (4 * self._m**2 - 4 * self._m + 3)
                * np.sqrt(self._m * (1 - self._m))
            )
        )


class Naca5DigitCamberReflexedClassic(Camber):
    """
    Class for the classic NACA 5-digit reflexed camber airfoil.

    Notes
    -----
    The only valid values for the relative chord location of maximum camber
    term are 1, 2, 3, 4, and 5. The only valid value for the ideal lift
    coefficient term is 2.
    """

    def __init__(
        self,
        lci: float,
        mci: float,
    ) -> None:
        """
        Initialize a classic reflexed NACA 5-digit camber line.

        Parameters
        ----------
        lci : float
            Ideal-lift-coefficient index from the designation.
        mci : float
            Maximum-camber-location index from the designation.

        Raises
        ------
        ValueError
            If either designation index is unsupported.
        """
        self._m = 0.2
        self._k1 = 0.0
        self._k2 = 0.0
        self._p_setter(mci)
        self._lci_setter(lci)

    @property
    def lift_coefficient_index(self) -> float:
        """
        Return the ideal-lift-coefficient index.

        Returns
        -------
        float
            Ideal lift coefficient expressed as a designation index.
        """
        return (20.0 / 3.0) * self._lci

    @lift_coefficient_index.setter
    def lift_coefficient_index(self, lci: float) -> None:
        """
        Update the ideal lift coefficient index.

        Parameters
        ----------
        lci : float
            Ideal-lift-coefficient index from the designation.
        """
        self._lci_setter(lci)

    @property
    def max_camber_index(self) -> float:
        """
        Return the maximum-camber-location index.

        Returns
        -------
        float
            Chordwise location of maximum camber as a designation index.
        """
        return 20.0 * self._p

    @max_camber_index.setter
    def max_camber_index(self, mci: float) -> None:
        """
        Update the maximum-camber-location index.

        Parameters
        ----------
        mci : float
            Maximum-camber-location index from the designation.
        """
        self._p_setter(mci)

    @property
    def m(self) -> float:
        """
        Return the camber transition location.

        Returns
        -------
        float
            Relative chord location of the camber transition point.
        """
        return self._m

    @property
    def k1(self) -> float:
        """
        Return the primary camber scale factor.

        Returns
        -------
        float
            First scale factor applied to the reflexed camber relation.
        """
        return self._k1

    @property
    def k2(self) -> float:
        """
        Return the secondary camber scale factor.

        Returns
        -------
        float
            Second scale factor applied to the reflexed camber relation.
        """
        return self._k2

    def joints(self) -> list[float]:
        """
        Return the locations of any joints/discontinuities in the camber line.

        Returns
        -------
        List[float]
            Xi-coordinates of any discontinuities.
        """
        return [0.0, self.m, 1.0]

    def max_camber_parameter(self) -> FloatScalar:
        """
        Return parameter where the camber is maximum.

        Returns
        -------
        float
            Parameter where camber is maximum.
        """
        return self._p

    def _y(self, t: FloatArray) -> FloatArray:
        """
        Return the camber location at specified chord location.

        Parameters
        ----------
        t : numpy.ndarray
            Chord location of interest.

        Returns
        -------
        numpy.ndarray
            Camber at specified point.
        """
        t = np.asarray(t, dtype=np.float64)
        m = self.m
        k1 = self.k1
        k2ok1 = self.k2 / k1

        def fore(t: FloatArray) -> FloatArray:
            return (k1 / 6) * (
                (t - m) ** 3 - k2ok1 * (1 - m) ** 3 * t + m**3 * (1 - t)
            )

        def aft(t: FloatArray) -> FloatArray:
            return (k1 / 6) * (
                k2ok1 * (t - m) ** 3 - k2ok1 * (1 - m) ** 3 * t + m**3 * (1 - t)
            )

        return np.piecewise(t, [t <= m, t > m], [fore, aft])

    def _y_t(self, t: FloatArray) -> FloatArray:
        """
        Return first derivative of camber at specified chord location.

        Parameters
        ----------
        t : numpy.ndarray
            Chord location of interest.

        Returns
        -------
        numpy.ndarray
            First derivative of camber at specified point.
        """
        t = np.asarray(t, dtype=np.float64)
        m = self.m
        k1 = self.k1
        k2ok1 = self.k2 / k1

        def fore(t: FloatArray) -> FloatArray:
            return (k1 / 6) * (3 * (t - m) ** 2 - k2ok1 * (1 - m) ** 3 - m**3)

        def aft(t: FloatArray) -> FloatArray:
            return (k1 / 6) * (
                3 * k2ok1 * (t - m) ** 2 - k2ok1 * (1 - m) ** 3 - m**3
            )

        return np.piecewise(t, [t <= m, t > m], [fore, aft])

    def _y_tt(self, t: FloatArray) -> FloatArray:
        """
        Return second derivative of camber at specified chord location.

        Parameters
        ----------
        t : numpy.ndarray
            Chord location of interest.

        Returns
        -------
        numpy.ndarray
            Second derivative of camber at specified point.
        """
        t = np.asarray(t, dtype=np.float64)

        def fore(t: FloatArray) -> FloatArray:
            return (self.k1) * (t - self.m)

        def aft(t: FloatArray) -> FloatArray:
            return (self.k2) * (t - self.m)

        return np.piecewise(t, [t <= self.m, t > self.m], [fore, aft])

    def _y_ttt(self, t: FloatArray) -> FloatArray:
        """
        Return third derivative of camber at specified chord location.

        Parameters
        ----------
        t : numpy.ndarray
            Chord location of interest.

        Returns
        -------
        numpy.ndarray
            Third derivative of camber at specified point.
        """
        t = np.asarray(t, dtype=np.float64)

        def fore(t: FloatArray) -> FloatArray:
            return self.k1 * np.ones_like(t)

        def aft(t: FloatArray) -> FloatArray:
            return self.k2 * np.ones_like(t)

        return np.piecewise(t, [t <= self.m, t > self.m], [fore, aft])

    def _p_setter(self, mci: float) -> None:
        """
        Map a reflexed 5-digit camber index to tabulated parameters.

        Parameters
        ----------
        mci : float
            Maximum-camber-location index from the designation.

        Raises
        ------
        ValueError
            If ``mci`` is not a supported reflexed 5-digit index.
        """
        parameters = REFLEXED_NACA_5DIGIT_PARAMETERS.get(int(mci))
        if parameters is None:
            raise ValueError(
                f"Invalid NACA 5-digit reflexed maximum camber location: {mci}."
            )
        self._m, self._k1, self._k2 = parameters

        self._p = mci / 20.0

    def _lci_setter(self, lci: float) -> None:
        """
        Validate and store the reflexed 5-digit lift index.

        Parameters
        ----------
        lci : float
            Ideal-lift-coefficient index from the designation.

        Raises
        ------
        ValueError
            If ``lci`` is not the supported reflexed 5-digit index.
        """
        if lci != CLASSIC_NACA_5DIGIT_IDEAL_LIFT_INDEX:
            raise ValueError(
                "Invalid NACA 5-Digit reflexed ideal lift "
                f"coefficient parameter: {lci}."
            )
        self._lci = (3.0 / 20.0) * lci


class Naca5DigitCamberReflexedParams(Naca5DigitCamberReflexedClassic):
    """
    Camber for the regular NACA 5-digit reflexed airfoils.

    The valid range of the relative chord location of maximum camber term is
    [1, 6). The valid range for the ideal lift coefficient term is [1, 4).
    """

    def __init__(self, lci: float, mci: float) -> None:
        """
        Initialize a continuous-index reflexed NACA 5-digit camber line.

        Parameters
        ----------
        lci : float
            Ideal-lift-coefficient index.
        mci : float
            Maximum-camber-location index.
        """
        # Need to bootstrap initialization
        self._lci = (3.0 / 20.0) * lci
        super().__init__(lci=lci, mci=mci)

    def _p_setter(self, mci: float) -> None:
        """
        Solve for continuous-index reflexed 5-digit parameters from ``mci``.

        Parameters
        ----------
        mci : float
            Maximum-camber-location index.

        Raises
        ------
        ValueError
            If ``mci`` is outside the supported continuous-index reflexed
            range.
        """
        if (
            mci < CLASSIC_NACA_5DIGIT_LOCATION_MIN
            or mci >= CLASSIC_NACA_5DIGIT_LOCATION_MAX
        ):
            raise ValueError(
                f"Invalid NACA 5-digit reflexed maximum camber location: {mci}"
            )

        self._p = mci / 20.0

        def m_fun(m: float) -> float:
            k1 = 1.0
            k2ok1 = self._k2ok1(m, self._p)
            cl_id = self._cl_id(m, k1, k2ok1)
            return -0.25 * cl_id + (k1 / 192) * (
                3 * k2ok1 * np.pi
                + 3 * (1 - k2ok1) * np.arccos(1 - 2 * m)
                + 2
                * (1 - k2ok1)
                * (1 - 2 * m)
                * (8 * m**2 - 8 * m - 3)
                * np.sqrt(m * (1 - m))
            )

        root = root_scalar(m_fun, bracket=(self._p, 3 * self._p))
        self._m = float(root.root)
        self._determine_k1k2()

    def _lci_setter(self, lci: float) -> None:
        """
        Validate and store the continuous-index reflexed lift index.

        Parameters
        ----------
        lci : float
            Ideal-lift-coefficient index.

        Raises
        ------
        ValueError
            If ``lci`` is outside the supported continuous-index reflexed
            range.
        """
        if lci < CLASSIC_NACA_5DIGIT_LIFT_MIN or lci >= (
            CLASSIC_NACA_5DIGIT_LIFT_MAX
        ):
            raise ValueError(
                "Invalid NACA 5-Digit reflexed ideal lift "
                f"coefficient parameter: {lci}."
            )

        self._lci = (3.0 / 20.0) * lci
        self._determine_k1k2()

    def _determine_k1k2(self) -> None:
        """
        Compute the continuous-index reflexed camber scale factors.

        Returns
        -------
        None
            This method updates the stored scale factors in place.
        """
        k2ok1 = self._k2ok1(self.m, self._p)
        self._k1 = self._lci / self._cl_id(self.m, 1, k2ok1)
        self._k2 = k2ok1 * self._k1

    @staticmethod
    def _cl_id(m: float, k1: float, k2ok1: float) -> float:
        """
        Return the ideal lift coefficient for a reflexed camber line.

        Parameters
        ----------
        m : float
            Camber transition location.
        k1 : float
            Primary camber scale factor.
        k2ok1 : float
            Ratio of the secondary and primary scale factors.

        Returns
        -------
        float
            Ideal lift coefficient associated with the reflexed camber line.
        """
        return (k1 / 12) * (
            3 * k2ok1 * (2 * m - 1) * np.pi
            + 3 * (1 - k2ok1) * (2 * m - 1) * np.arccos(1 - 2 * m)
            + 2 * (1 - k2ok1) * (4 * m**2 - 4 * m + 3) * np.sqrt(m * (1 - m))
        )

    @staticmethod
    def _k2ok1(m: float, p: float) -> float:
        """
        Return the reflex-parameter ratio ``k2 / k1``.

        Parameters
        ----------
        m : float
            Camber transition location.
        p : float
            Chordwise location of maximum camber.

        Returns
        -------
        float
            Ratio of the secondary and primary camber scale factors.
        """
        return (3 * (p - m) ** 2 - m**3) / (1 - m) ** 3
