from typing import List, Sequence, TYPE_CHECKING, Iterable, Tuple
import math
from .vector import Vec2
from .bbox import BoundingBox2d
from .line import ConstructionLine
from .construct2d import left_of_line, ConstructionTool

if TYPE_CHECKING:
    from ezdxf.eztypes import Vertex


class ConstructionBox(ConstructionTool):
    def __init__(self, center: 'Vertex' = (0, 0), width: float = 1, height: float = 1, angle: float = 0):
        self._center = Vec2(center)
        self._width = abs(width)  # type: float
        self._height = abs(height)  # type: float
        self._angle = angle  # type: float  # in degrees
        self._corners = None  # type: Tuple[Vec2, Vec2, Vec2, Vec2]
        self._tainted = True

    @classmethod
    def from_points(cls, p1: 'Vertex', p2: 'Vertex') -> 'ConstructionBox':
        p1 = Vec2(p1)
        p2 = Vec2(p2)
        width = abs(p2.x - p1.x)
        height = abs(p2.y - p1.y)
        center = p1.lerp(p2)
        return cls(center=center, width=width, height=height)

    def update(self) -> None:
        if not self._tainted:
            return
        center = self.center
        w2 = Vec2.from_deg_angle(self._angle, self._width / 2.)
        h2 = Vec2.from_deg_angle(self._angle + 90, self._height / 2.)
        self._corners = (
            center - w2 - h2,  # lower left
            center + w2 - h2,  # lower right
            center + w2 + h2,  # upper right
            center - w2 + h2,  # upper left
        )
        self._tainted = False

    @property
    def bounding_box(self) -> BoundingBox2d:
        return BoundingBox2d(self.corners)

    @property
    def center(self) -> Vec2:
        return self._center

    @center.setter
    def center(self, c: 'Vertex') -> None:
        self._center = Vec2(c)
        self._tainted = True

    @property
    def width(self) -> float:
        return self._width

    @width.setter
    def width(self, w: float) -> None:
        self._width = abs(w)
        self._tainted = True

    @property
    def height(self) -> float:
        return self._height

    @height.setter
    def height(self, h: float) -> None:
        self._height = abs(h)
        self._tainted = True

    @property
    def incircle_radius(self) -> float:
        return min(self._width, self._height) / 2.

    @property
    def circumcircle_radius(self) -> float:
        return math.hypot(self._width, self._height) / 2.

    @property
    def angle(self) -> float:
        return self._angle

    @angle.setter
    def angle(self, a: float) -> None:
        self._angle = a
        self._tainted = True

    @property
    def corners(self) -> Sequence[Vec2]:
        self.update()
        return self._corners

    def __iter__(self) -> Iterable[Vec2]:
        return iter(self.corners)

    def __getitem__(self, corner) -> Vec2:
        return self.corners[corner]

    def __repr__(self) -> str:
        return "ConstructionBox({0.center}, {0.width}, {0.height}, {0.angle})".format(self)

    def move(self, dx: float, dy: float) -> None:
        self.center += Vec2((dx, dy))

    def expand(self, dw: float, dh: float) -> None:
        self.width += dw
        self.height += dh

    def scale(self, sx: float, sy: float) -> None:
        self.width *= sx
        self.height *= sy

    def rotate(self, angle: float) -> None:
        self.angle += angle

    def is_inside(self, point: 'Vertex') -> bool:
        point = Vec2(point)
        delta = self.center - point
        if math.isclose(self.angle, 0.):  # fast path for horizontal rectangles
            return abs(delta.x) <= (self._width / 2.) and abs(delta.y) <= (self._height / 2.)
        else:
            distance = delta.magnitude
            if distance > self.circumcircle_radius:
                return False
            elif distance <= self.incircle_radius:
                return True
            else:
                # inside if point is "left of line" of all border lines.
                p1, p2, p3, p4 = self.corners
                return all(
                    (left_of_line(point, a, b, online=True) for a, b in [(p1, p2), (p2, p3), (p3, p4), (p4, p1)])
                )

    def is_any_corner_inside(self, other: 'ConstructionBox') -> bool:
        return any(self.is_inside(p) for p in other.corners)

    def is_overlapping(self, other: 'ConstructionBox') -> bool:
        distance = (self.center - other.center).magnitude
        max_distance = self.circumcircle_radius + other.circumcircle_radius
        if distance > max_distance:
            return False
        min_distance = self.incircle_radius + other.incircle_radius
        if distance <= min_distance:
            return True

        if self.is_any_corner_inside(other):
            return True
        if other.is_any_corner_inside(self):
            return True
        # no corner inside of any box, maybe crossing boxes?
        # check intersection of diagonals
        c1, c2, c3, c4 = self.corners
        diag1 = ConstructionLine(c1, c3)
        diag2 = ConstructionLine(c2, c4)

        t1, t2, t3, t4 = other.corners
        test_diag = ConstructionLine(t1, t3)
        if test_diag.has_intersection(diag1) or test_diag.has_intersection(diag2):
            return True
        test_diag = ConstructionLine(t2, t4)
        if test_diag.has_intersection(diag1) or test_diag.has_intersection(diag2):
            return True

        return False

    def border_lines(self) -> Sequence[ConstructionLine]:
        p1, p2, p3, p4 = self.corners
        return (
            ConstructionLine(p1, p2),
            ConstructionLine(p2, p3),
            ConstructionLine(p3, p4),
            ConstructionLine(p4, p1),
        )

    def intersect(self, line: ConstructionLine) -> List[Vec2]:
        """
        Returns 0, 1 or 2 intersection points between `line` and `TextBox` border lines.

        Args:
            line: line to intersect with border lines

        Returns: list of intersection points

        """
        result = set()
        for border_line in self.border_lines():
            p = line.intersect(border_line)
            if p is not None:
                result.add(p)
        return sorted(result)
