"""Classes associated with general curves."""

from abc import ABC, abstractmethod

import numpy as np
from scipy.integrate import quad

from buffalo_wings.internal.numeric import as_float_array, as_float_scalar
from buffalo_wings.type_aliases import FloatArray, FloatInput, FloatScalar


class Curve(ABC):
    """
    Base class for 1-d curves.

    Curves can be interrogated based on their natural parameterization, using
    the parameter, t.
    """

    #
    # Parametric interface
    #
    @abstractmethod
    def xy(self, t: FloatInput) -> tuple[FloatArray, FloatArray]:
        """
        Calculate the coordinates of geometry at parameter location.

        Parameters
        ----------
        t : numpy.ndarray
            Parameter for desired locations.

        Returns
        -------
        numpy.ndarray
            X-coordinate of point.
        numpy.ndarray
            Y-coordinate of point.
        """

    @abstractmethod
    def xy_t(self, t: FloatInput) -> tuple[FloatArray, FloatArray]:
        """
        Calculate rates of change of the coordinates at parameter location.

        Parameters
        ----------
        t : numpy.ndarray
            Parameter for desired locations.

        Returns
        -------
        numpy.ndarray
            Parametric rate of change of the x-coordinate of point.
        numpy.ndarray
            Parametric rate of change of the y-coordinate of point.
        """

    @abstractmethod
    def xy_tt(self, t: FloatInput) -> tuple[FloatArray, FloatArray]:
        """
        Calculate second derivative of the coordinates at parameter location.

        Parameters
        ----------
        t : numpy.ndarray
            Parameter for desired locations.

        Returns
        -------
        numpy.ndarray
            Parametric second derivative of the x-coordinate of point.
        numpy.ndarray
            Parametric second derivative of the y-coordinate of point.
        """

    def normal(self, t: FloatInput) -> tuple[FloatArray, FloatArray]:
        """
        Calculate the unit normal at parameter location.

        Parameters
        ----------
        t : numpy.ndarray
            Parameter for desired locations.

        Returns
        -------
        numpy.ndarray, numpy.ndarray
            Unit normal at point.
        """
        sx, sy = self.tangent(t)
        nx = -sy
        ny = sx
        return nx, ny

    def tangent(self, t: FloatInput) -> tuple[FloatArray, FloatArray]:
        """
        Calculate the unit tangent at parameter location.

        Parameters
        ----------
        t : numpy.ndarray
            Parameter for desired locations.

        Returns
        -------
        numpy.ndarray, numpy.ndarray
            Unit tangent at point.
        """
        sx, sy = self.xy_t(t)
        temp = np.sqrt(sx**2 + sy**2)
        sx = as_float_array(np.divide(sx, temp))
        sy = as_float_array(np.divide(sy, temp))
        return sx, sy

    def k(self, t: FloatInput) -> FloatArray:
        """
        Calculate the curvature at parameter location.

        Parameters
        ----------
        t : numpy.ndarray
            Parameter for desired locations.

        Returns
        -------
        numpy.ndarray
            Curvature of surface at point.
        """
        xt, yt = self.xy_t(t)
        xtt, ytt = self.xy_tt(t)
        return (xt * ytt - yt * xtt) / (xt**2 + yt**2) ** (3 / 2)

    def arc_length(self, t_s: FloatScalar, t_e: FloatInput) -> FloatArray:
        """
        Calculate the arc-length distance between two points on surface.

        Parameters
        ----------
        t_s : float
            Start point of distance calculation.
        t_e : numpy.ndarray
            End point of distance calculation.

        Returns
        -------
        numpy.ndarray
            Distance from start point to end point.
        """

        def fun(t: float) -> float:
            xt, yt = self.xy_t(t)
            return as_float_scalar(np.sqrt(xt**2 + yt**2))

        t_begin = float(t_s)
        t_e = as_float_array(t_e)
        it = np.nditer([t_e, None])
        with it:
            for ti, alen in it:
                segment_ends = [x for x in self.joints() if t_begin < x < ti]
                segment_ends.append(float(ti))
                segment_start = t_begin
                alen[...] = 0.0
                for t_end in segment_ends:
                    alen[...] += quad(fun, segment_start, t_end)[0]
                    segment_start = t_end

            return it.operands[1]

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

        The resulting list needs to contain any parametric locations where
        some non-standard discontinuity (slope, curvature, etc.) occurs as
        well as the end points for the curve (if they exist).

        Returns
        -------
        List[float]
            Parametric coordinates of any discontinuities.
        """
