# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at

#   http://www.apache.org/licenses/LICENSE-2.0

# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.

"""Basic 2-dimensional geometric constructs: points, rectangles, etc. """
from __future__ import annotations

from dataclasses import dataclass
from typing import Iterator, Sequence, Tuple, Union


@dataclass(order=True, frozen=True)
class Point:
    """Represents a point in 2-dimensional plane (e.g. image)."""

    x: float
    y: float

    def __add__(self, other: Point) -> Point:
        return Point(self.x + other.x, self.y + other.y)

    def __sub__(self, other: Point) -> Point:
        return Point(self.x - other.x, self.y - other.y)

    def __mul__(self, scalar: float) -> Point:
        return Point(self.x * scalar, self.y * scalar)

    def __iter__(self) -> Iterator[float]:
        return (data for data in (self.x, self.y))

    @staticmethod
    def zero() -> Point:
        return Point(0, 0)


@dataclass(frozen=True)
class Rectangle:
    """Represents a rectangle in a 2-dimensional plane."""

    minima: Point
    maxima: Point

    def __iter__(self) -> Iterator[float]:
        return (data for data in (self.x, self.y, self.width, self.height))

    def __contains__(self, other: Union[Point, Rectangle]) -> bool:
        return self.contains(other)

    def contains(self, other: Union[Point, Rectangle]) -> bool:
        """
        Tests whether the rectangle contains ``other``.

        :return: ``True`` if other contained in the rectangle, ``False`` otherwise.
        """
        if isinstance(other, Point):
            return self.minima.x <= other.x <= self.maxima.x and self.minima.y <= other.y <= self.maxima.y
        if isinstance(other, Rectangle):
            return (
                self.maxima.x >= other.maxima.x
                and self.maxima.y >= other.maxima.y
                and self.minima.x <= other.minima.x
                and self.minima.y <= other.minima.y
            )

        raise TypeError(f"Expected a Rectangle or a Point, not {type(other)}")

    def clip(self, other: Rectangle) -> Rectangle:
        """
        Return a new rectangle generated by clipping this one by the bounds of ``other``.

        Similar to intersection, but clipping non-intersecting rectangles will result in a degenerate rectangle
        located on one of the edges of ``other``

        :return: New, clipped ``Rectangle`` (possibly degenerate)
        """
        return Rectangle.from_list(
            min(max(self.minima.x, other.minima.x), other.maxima.x),
            min(max(self.minima.y, other.minima.y), other.maxima.y),
            min(max(self.maxima.x, other.minima.x), other.maxima.x),
            min(max(self.maxima.y, other.minima.y), other.maxima.y),
        )

    @property
    def x(self) -> float:
        """The x-coordinate of the lower left vertex"""
        return self.minima.x

    @property
    def y(self) -> float:
        """The y-coordinate of the lower-left vertex"""
        return self.minima.y

    @property
    def bounds(self) -> Tuple[float, float, float, float]:
        """Returns min x, min y, max x, max y"""
        return self.minima.x, self.minima.y, self.maxima.x, self.maxima.y

    @property
    def center(self) -> Point:
        """Return the midpoint of the rectangle"""
        return (self.minima + self.maxima) * 0.5

    @property
    def width(self) -> float:
        return self.maxima.x - self.minima.x

    @property
    def height(self) -> float:
        return self.maxima.y - self.minima.y

    @property
    def area(self) -> float:
        return self.width * self.height

    @staticmethod
    def empty() -> Rectangle:
        """Returns a rectangle of zero area at origo."""
        return Rectangle.from_list(0, 0, 0, 0)

    def resized(self, delta: float) -> Rectangle:
        """
        Returns a resized rectangle, shrinked (inflated for ``delta<0``) by ``2*delta`` in width and in height.
        """
        assert 2 * delta >= -min(self.width, self.height), "Operation only defined for delta >= 2*min(width, height)"
        delta_point = Point(delta, delta)
        return Rectangle(self.minima - delta_point, self.maxima + delta_point)

    def __add__(self, vector: Point) -> Rectangle:
        """Returns a copy of the rectangle translated by ``vector`` """
        return Rectangle(self.minima + vector, self.maxima + vector)

    def __sub__(self, vector: Point) -> Rectangle:
        """Returns a copy of the rectangle inversly translated by ``vector`` """
        return Rectangle(self.minima - vector, self.maxima - vector)

    def __repr__(self):
        return "({}), ({})".format(self.minima, self.maxima)

    @staticmethod
    def bounding_box(rectangles: Sequence[Rectangle]) -> Rectangle:
        """Computes the bounding box of ``rectangles`` """
        if not rectangles:
            raise ValueError("Expected a non-empty sequence of rectangles")

        max_x = max(rect.maxima.x for rect in rectangles)
        max_y = max(rect.maxima.y for rect in rectangles)
        min_x = min(rect.minima.x for rect in rectangles)
        min_y = min(rect.minima.y for rect in rectangles)

        return Rectangle.from_list(max_x, max_y, min_x, min_y)

    @staticmethod
    def intersection(rectangles: Sequence[Rectangle]) -> Rectangle:
        """
        Computes the rectangle which is the intersection of a sequence of ``rectangles``. In case the
        intersection is empty, it returns an empty rectangle.
        """
        if not rectangles or len(rectangles) < 2:
            raise ValueError("Expected a sequence of at least two rectangles")

        if Rectangle.empty() in rectangles:
            return Rectangle.empty()

        max_x = min(rect.maxima.x for rect in rectangles)
        max_y = min(rect.maxima.y for rect in rectangles)
        min_x = max(rect.minima.x for rect in rectangles)
        min_y = max(rect.minima.y for rect in rectangles)

        if max_x < min_x or max_y < min_y:
            return Rectangle.empty()

        return Rectangle.from_list(max_x, max_y, min_x, min_y)

    @staticmethod
    def centered_at(center: Point, radius: float) -> Rectangle:
        """
        A new square centered at ``center`` with side length ``radius``. (The technically correct term here is
        `Apothem <https://en.wikipedia.org/wiki/Apothem>`_.)
        """
        if radius <= 0:
            raise ValueError(f"Cannot instantiate rectangle with non-positive side {2 * radius} centered at {center}")

        radius_point = Point(radius, radius)
        return Rectangle(center - radius_point, center + radius_point)

    @staticmethod
    def from_list(*args) -> Rectangle:
        """Converts tuple/list (x1, y1, x2, y2) to a Rectangle"""
        if len(args) == 1:
            args = args[0]
        if len(args) == 4 and all(isinstance(el, (int, float)) for el in args):
            first_x, first_y, second_x, second_y = args
        else:
            raise ValueError("Invalid argument(s) to create rectangle from!")

        minima = Point(min(first_x, second_x), min(first_y, second_y))
        maxima = Point(max(first_x, second_x), max(first_y, second_y))
        return Rectangle(minima, maxima)
