# Purpose: Bezier Curve optimized for 4 control points
# Created: 26.03.2010
# Copyright (c) 2010-2018 Manfred Moitzi
# License: MIT License
from typing import List, TYPE_CHECKING, Iterable

if TYPE_CHECKING:
    from ezdxf.eztypes import Vertex


def check_if_in_valid_range(t: float):
    if not (0 <= t <= 1.):
        raise ValueError("t not in range [0 to 1]")


class Bezier4P:
    """
    Implements an optimized Cubic Bezier Curve with 4 control points.

    Special behavior: 2d in -> 2d out and 3d in -> 3d out!

    """

    def __init__(self, control_points: List['Vertex']):
        if len(control_points) == 4:
            is3d = any(len(p) > 2 for p in control_points)
            self.math = D3D if is3d else D2D
            self._cpoints = [self.math.tovector(vector) for vector in control_points]
        else:
            raise ValueError("Four control points required.")

    @property
    def control_points(self) -> List['Vertex']:
        return self._cpoints

    def tangent(self, t: float) -> 'Vertex':
        """
        Calculate tangent at parameter t [0, 1].

        Args:
            t: curve position in the range [0, 1]

        Returns: (x, y, z) tuple, a vector which defines the direction of the tangent.

        """
        check_if_in_valid_range(t)
        return self._get_curve_tangent(t)

    def point(self, t: float) -> 'Vertex':
        """
        Calculate curve point at parameter t [0, 1].

        Args:
            t: curve position in the range [0, 1]

        Returns: (x, y, z) tuple

        """
        check_if_in_valid_range(t)
        return self._get_curve_point(t)

    def approximate(self, segments: int) -> Iterable['Vertex']:
        delta_t = 1. / segments
        yield self._cpoints[0]
        for segment in range(1, segments):
            yield self.point(delta_t * segment)
        yield self._cpoints[3]

    def _get_curve_point(self, t: float) -> 'Vertex':
        """
        Calculate curve point at parameter t [0, 1].

        Returns: (x, y, z) tuple

        """
        b1, b2, b3, b4 = self._cpoints
        one_minus_t = 1. - t
        m = self.math
        point = m.vmul_scalar(b1, one_minus_t ** 3)
        point = m.vadd(point, m.vmul_scalar(b2, 3. * one_minus_t ** 2 * t))
        point = m.vadd(point, m.vmul_scalar(b3, 3. * one_minus_t * t ** 2))
        point = m.vadd(point, m.vmul_scalar(b4, t ** 3))
        return tuple(point)

    def _get_curve_tangent(self, t: float) -> 'Vertex':
        """
        Calculate tangent at parameter t [0, 1]. Implementation optimized for 4 control points.

        Returns: (x, y, z) tuple, a vector which defines the direction of the tangent.

        """
        b1, b2, b3, b4 = self._cpoints
        m = self.math
        tangent = m.vmul_scalar(b1, -3. * (1. - t) ** 2)
        tangent = m.vadd(tangent, m.vmul_scalar(b2, 3. * (1. - 4. * t + 3. * t ** 2)))
        tangent = m.vadd(tangent, m.vmul_scalar(b3, 3. * t * (2. - 3. * t)))
        tangent = m.vadd(tangent, m.vmul_scalar(b4, 3. * t ** 2))
        return tuple(tangent)

    def approximated_length(self, segments: int = 100) -> float:
        length = 0.
        point_gen = self.approximate(segments)
        prev_point = next(point_gen)
        distance = self.math.distance
        for point in point_gen:
            length += distance(prev_point, point)
            prev_point = point
        return length


class D2D:
    @staticmethod
    def vadd(vector1: 'Vertex', vector2: 'Vertex') -> 'Vertex':
        """ Add vectors """
        return vector1[0] + vector2[0], vector1[1] + vector2[1]

    @staticmethod
    def vmul_scalar(vector: 'Vertex', scalar: float) -> 'Vertex':
        """ Mul vector with scalar """
        return vector[0] * scalar, vector[1] * scalar

    @staticmethod
    def tovector(vector: 'Vertex') -> 'Vertex':
        """ Return a 2d point """
        return float(vector[0]), float(vector[1])

    @staticmethod
    def distance(point1: 'Vertex', point2: 'Vertex') -> float:
        """ calc distance between two 2d points """
        return ((point1[0] - point2[0]) ** 2 +
                (point1[1] - point2[1]) ** 2) ** 0.5


class D3D:
    @staticmethod
    def vadd(vector1: 'Vertex', vector2: 'Vertex') -> 'Vertex':
        """ Add vectors """
        return vector1[0] + vector2[0], vector1[1] + vector2[1], vector1[2] + vector2[2]

    @staticmethod
    def vmul_scalar(vector: 'Vertex', scalar: float) -> 'Vertex':
        """ Mul vector with scalar """
        return vector[0] * scalar, vector[1] * scalar, vector[2] * scalar

    @staticmethod
    def tovector(vector: 'Vertex') -> 'Vertex':
        """ Return a 3d point """
        try:
            z = float(vector[2])
        except IndexError:
            z = 0.
        return float(vector[0]), float(vector[1]), z

    @staticmethod
    def distance(point1: 'Vertex', point2: 'Vertex') -> float:
        """Calc distance between two 3d points """
        return ((point1[0] - point2[0]) ** 2 +
                (point1[1] - point2[1]) ** 2 +
                (point1[2] - point2[2]) ** 2) ** 0.5
