# Copyright (c) 2010-2020 Manfred Moitzi
# License: MIT License
from typing import TYPE_CHECKING, Sequence
import math
from ezdxf.math import Vec2
from .line import ConstructionRay
from .bbox import BoundingBox2d

if TYPE_CHECKING:
    from ezdxf.eztypes import Vertex

HALF_PI = math.pi / 2.


class ConstructionCircle:
    """ Circle construction tool.

    Args:
        center: center point as :class:`Vec2` compatible object
        radius: circle radius > `0`

    """

    def __init__(self, center: 'Vertex', radius: float = 1.0):
        self.center = Vec2(center)
        self.radius = float(radius)
        if self.radius <= 0.:
            raise ValueError("Radius has to be > 0.")

    def __str__(self) -> str:
        """ Returns string representation of circle
        "ConstructionCircle(center, radius)".
        """
        return f'ConstructionCircle({self.center}, {self.radius})'

    @staticmethod
    def from_3p(p1: 'Vertex', p2: 'Vertex',
                p3: 'Vertex') -> 'ConstructionCircle':
        """ Creates a circle from three points, all points have to be compatible
        to :class:`Vec2` class.
        """
        p1 = Vec2(p1)
        p2 = Vec2(p2)
        p3 = Vec2(p3)
        ray1 = ConstructionRay(p1, p2)
        ray2 = ConstructionRay(p1, p3)
        center_ray1 = ray1.orthogonal(p1.lerp(p2))
        center_ray2 = ray2.orthogonal(p1.lerp(p3))
        center = center_ray1.intersect(center_ray2)
        return ConstructionCircle(center, center.distance(p1))

    @property
    def bounding_box(self) -> 'BoundingBox2d':
        """ 2D bounding box of circle as  :class:`BoundingBox2d` object. """
        rvec = Vec2((self.radius, self.radius))
        return BoundingBox2d((self.center - rvec, self.center + rvec))

    def translate(self, dx: float, dy: float) -> None:
        """ Move circle about `dx` in x-axis and about `dy` in y-axis.

        Args:
            dx: translation in x-axis
            dy: translation in y-axis

        """
        self.center += Vec2((dx, dy))

    def point_at(self, angle: float) -> Vec2:
        """ Returns point on circle at `angle` as :class:`Vec2` object.

        Args:
            angle: angle in radians

        """
        return self.center + Vec2.from_angle(angle, self.radius)

    def inside(self, point: 'Vertex') -> bool:
        """ Returns ``True`` if `point` is inside circle. """
        return self.radius >= self.center.distance(Vec2(point))

    def tangent(self, angle: float) -> ConstructionRay:
        """ Returns tangent to circle at `angle` as :class:`ConstructionRay`
        object.

        Args:
            angle: angle in radians

        """
        point_on_circle = self.point_at(angle)
        ray = ConstructionRay(self.center, point_on_circle)
        return ray.orthogonal(point_on_circle)

    def intersect_ray(self, ray: ConstructionRay,
                      abs_tol: float = 1e-10) -> Sequence[Vec2]:
        """ Returns intersection points of circle and `ray` as sequence of
        :class:`Vec2` objects.

        Args:
            ray: intersection ray
            abs_tol: absolute tolerance for tests (e.g. test for tangents)

        Returns:
            tuple of :class:`Vec2` objects

            =========== ==================================
            tuple size  Description
            =========== ==================================
            0           no intersection
            1           ray is a tangent to circle
            2           ray intersects with the circle
            =========== ==================================

        """
        ortho_ray = ray.orthogonal(self.center)
        intersection_point = ray.intersect(ortho_ray)
        dist = self.center.distance(intersection_point)
        result = []
        # Intersect in two points:
        if dist < self.radius:
            # Ray goes through center point:
            if math.isclose(dist, 0., abs_tol=abs_tol):
                angle = ortho_ray.angle
                alpha = HALF_PI
            else:
                # The exact direction of angle (all 4 quadrants Q1-Q4) is
                # important: ortho_ray.angle is only correct at the center point
                angle = (intersection_point - self.center).angle
                alpha = math.acos(intersection_point.distance(
                    self.center) / self.radius)
            result.append(self.point_at(angle + alpha))
            result.append(self.point_at(angle - alpha))
        # Ray is a tangent of the circle:
        elif math.isclose(dist, self.radius, abs_tol=abs_tol):
            result.append(intersection_point)
        # else: No intersection
        return tuple(result)

    def intersect_circle(self, other: 'ConstructionCircle',
                         abs_tol: float = 1e-10) -> Sequence[Vec2]:
        """ Returns intersection points of two circles as sequence of
        :class:`Vec2` objects.

        Args:
            other: intersection circle
            abs_tol: absolute tolerance for tests

        Returns:
            tuple of :class:`Vec2` objects

            =========== ==================================
            tuple size  Description
            =========== ==================================
            0           no intersection
            1           circle touches the `other` circle at one point
            2           circle intersects with the `other` circle
            =========== ==================================

        """
        r1 = self.radius
        r2 = other.radius
        d = self.center.distance(other.center)
        d_max = r1 + r2
        d_min = math.fabs(r1 - r2)
        result = []
        if d_min <= d <= d_max:
            angle = (other.center - self.center).angle
            # Circles touches at one point:
            if math.isclose(d, d_max, abs_tol=abs_tol) or math.isclose(
                    d, d_min, abs_tol=abs_tol):
                result.append(self.point_at(angle))
            else:  # Circles intersect in two points:
                # Law of Cosines:
                alpha = math.acos((r2 ** 2 - r1 ** 2 - d ** 2) / (-2. * r1 * d))
                result.append(self.point_at(angle + alpha))
                result.append(self.point_at(angle - alpha))
        return tuple(result)
