# Copyright (c) 2010-2021, Manfred Moitzi
# License: MIT License
from typing import Dict, Iterable, List
from ezdxf.math import Vec3
from ezdxf.math.bspline import global_bspline_interpolation, BSpline

__all__ = ["EulerSpiral"]


def powers(base: float, count: int) -> List[float]:
    assert count > 2, "requires count > 2"
    values = [1.0, base]
    next_value = base
    for _ in range(count - 2):
        next_value *= base
        values.append(next_value)
    return values


def _params(length: float, segments: int) -> Iterable[float]:
    delta_l = float(length) / float(segments)
    for index in range(0, segments + 1):
        yield delta_l * index


class EulerSpiral:
    """
    This class represents an euler spiral (clothoid) for `curvature` (Radius of
    curvature).

    This is a parametric curve, which always starts at the origin = ``(0, 0)``.

    Args:
        curvature: radius of curvature

    """

    def __init__(self, curvature: float = 1.0):
        curvature = float(curvature)
        self.curvature = curvature  # Radius of curvature
        self.curvature_powers: List[float] = powers(curvature, 19)
        self._cache: Dict[float, Vec3] = {}  # coordinates cache

    def radius(self, t: float) -> float:
        """Get radius of circle at distance `t`."""
        if t > 0.0:
            return self.curvature_powers[2] / t
        else:
            return 0.0  # radius = infinite

    def tangent(self, t: float) -> Vec3:
        """Get tangent at distance `t` as :class:`Vec3` object."""
        angle = t ** 2 / (2.0 * self.curvature_powers[2])
        return Vec3.from_angle(angle)

    def distance(self, radius: float) -> float:
        """Get distance L from origin for `radius`."""
        return self.curvature_powers[2] / float(radius)

    def point(self, t: float) -> Vec3:
        """Get point at distance `t` as :class:`Vec3`."""

        def term(length_power, curvature_power, const):
            return t ** length_power / (
                const * self.curvature_powers[curvature_power]
            )

        if t not in self._cache:
            y = (
                term(3, 2, 6.0)
                - term(7, 6, 336.0)
                + term(11, 10, 42240.0)
                - term(15, 14, 9676800.0)
                + term(19, 18, 3530096640.0)
            )
            x = (
                t
                - term(5, 4, 40.0)
                + term(9, 8, 3456.0)
                - term(13, 12, 599040.0)
                + term(17, 16, 175472640.0)
            )
            self._cache[t] = Vec3(x, y)
        return self._cache[t]

    def approximate(self, length: float, segments: int) -> Iterable[Vec3]:
        """Approximate curve of length with line segments.
        Generates segments+1 vertices as :class:`Vec3` objects.

        """
        for t in _params(length, segments):
            yield self.point(t)

    def circle_center(self, t: float) -> Vec3:
        """Get circle center at distance `t`."""
        p = self.point(t)
        r = self.radius(t)
        return p + self.tangent(t).normalize(r).orthogonal()

    def bspline(
        self,
        length: float,
        segments: int = 10,
        degree: int = 3,
        method: str = "uniform",
    ) -> BSpline:
        """Approximate euler spiral as B-spline.

        Args:
            length: length of euler spiral
            segments: count of fit points for B-spline calculation
            degree: degree of BSpline
            method: calculation method for parameter vector t

        Returns:
            :class:`BSpline`

        """
        length = float(length)
        fit_points = list(self.approximate(length, segments=segments))
        derivatives = [
            # Scaling derivatives by chord length (< real length) is suggested
            # by Piegl & Tiller.
            self.tangent(t).normalize(length)
            for t in _params(length, segments)
        ]
        spline = global_bspline_interpolation(
            fit_points, degree, method=method, tangents=derivatives
        )
        return BSpline(
            spline.control_points,
            spline.order,
            # Scale knot values to length:
            [v * length for v in spline.knots()],
        )
