# Copyright (c) 2010-2019 Manfred Moitzi
# License: MIT License
from typing import TYPE_CHECKING, Sequence
import math
from .line import ConstructionRay
from .vector import Vec2
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 'ConstructionCircle({0.center}, {0.radius})'.format(self)

    @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 = []
        if dist < self.radius:  # intersect in two points
            if math.isclose(dist, 0., abs_tol=abs_tol):  # if ray goes through center point
                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 at the center point correct
                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))
        elif math.isclose(dist, self.radius, abs_tol=abs_tol):  # ray is a tangent of circle
            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 (e.g. test for circle touch point)

        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
            # if circles touches in 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
                alpha = math.acos((r2 ** 2 - r1 ** 2 - d ** 2) / (-2. * r1 * d))  # 'Cosinus-Satz'
                result.append(self.point_at(angle + alpha))
                result.append(self.point_at(angle - alpha))
        return tuple(result)
