"""
Simple data structures for working with points, sizes, and rects.
"""
from math import floor
from random import randint


class Point:
    """
    :param Real x:
    :param Real y:

    Represents a point in 2D space. Supports arithmetic operations with other
    points, and supports multiplication and division with numbers.

    BearLibTerminal requires ints for all coordinate values, so
    :py:attr:`~Point.floored` is your friend.

    .. py:attribute:: x

      X axis coordinate; may be any number

    .. py:attribute:: y

      Y coordinate; may be any number
    """
    __slots__ = ('x', 'y', '_hash')

    def __init__(self, x=0, y=0):
        super().__init__()
        self.x = x
        self.y = y
        self._hash = hash(str(self.x) + ',' + str(self.y))

    def __hash__(self):
        return self._hash

    def __repr__(self):
        return 'Point({}, {})'.format(self.x, self.y)

    @property
    def floored(self):
        """A copy of this point with ``math.floor()`` called on each coordinate."""
        return self.__class__(floor(self.x), floor(self.y))

    def path_L_to(self, target):
        """Generator of all points on an L-shaped path between ``self`` and
        ``target``."""
        p = self
        yield p
        while p.x < target.x:
            p = p + Point(1, 0)
            yield p
        while p.x > target.x:
            p = p - Point(1, 0)
            yield p
        while p.y < target.y:
            p = p + Point(0, 1)
            yield p
        while p.y > target.y:
            p = p - Point(0, 1)
            yield p

    def manhattan_distance_to(self, target):
        """:return: Manhattan distance between ``self`` and ``target``"""
        return abs(self.x - target.x) + abs(self.y - target.y)

    def get_closest_point(self, candidates):
        """
        :param [Point] candidates:
        :returns: :py:class:`Point`

        Of all the point in *candidates*, return the one closest to ``self``.
        """
        best_point = candidates[0]
        best_distance = best_point.manhattan_distance_to(self)
        for point in candidates:
            if point == self:
                return point
            distance = point.manhattan_distance_to(self)
            if distance < best_distance:
                best_distance = distance
                best_point = point
        return best_point

    def get_farthest_point(self, candidates):
        """
        :param [Point] candidates:
        :returns: :py:class:`Point`

        Of all the point in *candidates*, return the one farthest from ``self``.
        """
        best_point = candidates[0]
        best_distance = best_point.manhattan_distance_to(self)
        for point in candidates:
            distance = point.manhattan_distance_to(self)
            if distance > best_distance:
                best_distance = distance
                best_point = point
        return best_point

    @property
    def neighbors(self):
        """Generator of all points that share a horizontal or vertical edge with
        this one."""
        yield self - Point(-1, 0)
        yield self - Point(0, -1)
        yield self - Point(1, 0)
        yield self - Point(0, 1)

    @property
    def diagonal_neighbors(self):
        """Generator of all points that share *only* a corner with ``self``."""
        yield self - Point(-1, -1)
        yield self - Point(1, -1)
        yield self - Point(1, 1)
        yield self - Point(-1, 1)

    def points_bresenham_to(self, other):
        """
        :param Point other:

        Generator of all points on a Bresenham algorithm line between ``self``
        and ``other``.
        """
        delta = other - self
        xsign = 1 if delta.x > 0 else -1
        ysign = 1 if delta.y > 0 else -1

        delta.x = abs(delta.x)
        delta.y = abs(delta.y)

        if delta.x > delta.y:
            xx, xy, yx, yy = xsign, 0, 0, ysign
        else:
            delta.x, delta.y = delta.y, delta.x
            xx, xy, yx, yy = 0, ysign, xsign, 0

        D = 2 * delta.y - delta.x
        y = 0

        for x in range(delta.x + 1):
            yield Point(self.x + x * xx + y * yx, self.y + x * xy + y * yy)
            if D > 0:
                y += 1
                D -= delta.x
            D += delta.y

    ### operators ###

    def __eq__(self, other):
        if not isinstance(other, Point):
            return False
        return self.x == other.x and self.y == other.y

    def __add__(self, other):
        return self.__class__(self.x + other.x, self.y + other.y)

    def __mul__(self, other):
        if isinstance(other, Point):
            return self.__class__(self.x * other.x, self.y * other.y)
        else:
            return self.__class__(self.x * other, self.y * other)

    def __sub__(self, other):
        return self + (other * -1)

    def __truediv__(self, other):
        if isinstance(other, Point):
            return self.__class__(self.x / other.x, self.y / other.y)
        else:
            return self.__class__(self.x / other, self.y / other)


class Size(Point):
    """
    :param Real width:
    :param Real height:

    Thin wrapper over :py:class:`Point` that forwards :py:attr:`~Point.x` to
    :py:attr:`width` and :py:attr:`~Point.y` to :py:attr:`height`.

    .. py:attribute:: width

      alias for :py:attr:`~Point.x`

    .. py:attribute:: height

      alias for :py:attr:`~Point.y`
    """

    def __init__(self, width=0, height=0):
        super().__init__(x=width, y=height)

    @property
    def point(self):
        return Point(self.width, self.height)

    @property
    def width(self):
        return self.x

    @width.setter
    def width(self, value):
        self.x = value

    @property
    def height(self):
        return self.y

    @height.setter
    def height(self, value):
        self.y = value

    def __repr__(self):
        return 'Size({}, {})'.format(self.width, self.height)


class Rect:
    """
    :param Point origin:
    :param Size size:

    Represents an rectangle in 2D space.

    BearLibTerminal requires ints for all coordinate values, so
    :py:attr:`~Rect.floored` is your friend.

    .. py:attribute:: origin

      Origin of the rectangle

    .. py:attribute:: size

      Size of the rectangle
    """
    __slots__ = ('origin', 'size')

    def __init__(self, origin=None, size=None):
        super().__init__()
        self.origin = origin or Point()
        self.size = size or Size()

    def __eq__(self, other):
        if not isinstance(other, Rect):
            return False
        return self.origin == other.origin and self.size == other.size

    def __repr__(self):
        return 'Rect({!r}, {!r})'.format(self.origin, self.size)

    @property
    def x(self):
        """Forwarded from ``self.origin.x``"""
        return self.origin.x

    @x.setter
    def x(self, new_value):
        self.origin = Point(new_value, self.y)

    @property
    def y(self):
        """Forwarded from ``self.origin.y``"""
        return self.origin.y

    @y.setter
    def y(self, new_value):
        self.origin = Point(self.x, new_value)

    @property
    def width(self):
        """Forwarded from ``self.size.width``"""
        return self.size.width

    @width.setter
    def width(self, new_value):
        self.size = Size(new_value, self.height)

    @property
    def height(self):
        """Forwarded from ``self.size.height``"""
        return self.size.height

    @height.setter
    def height(self, new_value):
        self.size = Size(self.width, new_value)

    @property  # no setter; it's not clear if it would change origin or size
    def x2(self):
        """Max X value of this rect. Read-only."""
        return self.origin.x + self.size.width - 1

    @property  # no setter; it's not clear if it would change origin or sizey
    def y2(self):
        """Max Y value of this rect. Read-only."""
        return self.origin.y + self.size.height - 1

    @property
    def area(self):
        """:return: int (area of ``self``)"""
        return self.width * self.height

    ### handy iterators ###

    @property
    def points(self):
        """Iterator of all points in this rect"""
        for x in range(self.origin.x, self.origin.x + self.size.width):
            for y in range(self.origin.y, self.origin.y + self.size.height):
                yield Point(x, y)

    @property
    def points_top(self):
        """Iterator of all points along the top edge of this rect"""
        start = self.origin.x + 1
        end = self.origin.x + self.size.width - 1
        for x in range(start, end):
            yield Point(x, self.origin.y)

    @property
    def points_bottom(self):
        """Iterator of all points along the bottom edge of this rect"""
        start = self.origin.x + 1
        end = self.origin.x + self.size.width - 1
        for x in range(start, end):
            yield Point(x, self.origin.y + self.size.height - 1)

    @property
    def points_left(self):
        """Iterator of all points along the left edge of this rect"""
        start = self.origin.y + 1
        end = self.origin.y + self.size.height - 1
        for y in range(start, end):
            yield Point(self.origin.x, y)

    @property
    def points_right(self):
        """Iterator of all points along the right edge of this rect"""
        start = self.origin.y + 1
        end = self.origin.y + self.size.height - 1
        for y in range(start, end):
            yield Point(self.origin.x + self.size.width - 1, y)

    @property
    def points_corners(self):
        """Iterator of all four points in this rect's corners"""
        yield self.origin
        yield self.point_top_right
        yield self.point_bottom_right
        yield self.point_bottom_left

    ### individual points ###

    @property
    def center(self):
        """Point at the center of this rect"""
        return self.origin + self.size / 2

    @property
    def point_top_right(self):
        """Point at the top right corner of this rect"""
        return Point(self.origin.x + self.size.width - 1, self.origin.y)

    @property
    def point_bottom_left(self):
        """Point at the bottom left corner of this rect"""
        return Point(self.origin.x, self.origin.y + self.size.height - 1)

    @property
    def point_bottom_right(self):
        """Point at the bottom right corner of this rect"""
        return self.origin + self.size - Point(1, 1)

    ### copying transforms ###

    @property
    def floored(self):
        """A copy of this rect with the :py:attr:`Point.floored` origin/size
        copies"""
        return Rect(self.origin.floored, self.size.floored)

    def moved_by(self, delta):
        """
        :param Point delta:

        A copy of this rect with the origin moved by **delta**
        """
        return Rect(self.origin + delta, self.size)

    def with_origin(self, new_origin):
        """
        :param Point new_origin:

        A copy of this rect with the given origin
        """
        return Rect(new_origin, self.size)

    def with_size(self, new_size):
        """
        :param Point new_size:

        A copy of this rect with the given size
        """
        return Rect(self.origin, new_size)

    def with_inset(self, inset):
        """
        :param Point|Real inset:

        A copy of this rect inset by the given amount. If you pass a number, all
        sides will be inset by the same amount. If you pass a :py:class:`Point`
        or a :py:class:`Size`, the X and Y axes will be inset by the respective
        amounts specified in each coordinate.
        """
        if not isinstance(inset, Point):
            inset = Point(inset, inset)
        return Rect(self.origin + inset, self.size - inset * 2)

    ### tests ###

    def intersects(self, other):
        """Returns ``True`` iff ``self`` and ``other`` share any points"""
        if self.x > other.x2:
            return False
        if other.x2 > self.x:
            return False
        if self.y > other.y2:
            return False
        if other.y2 > self.y:
            return False
        return True

    def contains(self, point):
        """Returns ``True`` iff ``self`` contains all of ``other``'s points"""
        if self.x > point.x:
            return False
        if self.x2 < point.x:
            return False
        if self.y > point.y:
            return False
        if self.y2 < point.y:
            return False
        return True

    ### random stuff ###

    def get_random_point(self):
        """Returns a random point inside ``self``"""
        return Point(
            randint(self.origin.x, self.origin.x + self.size.width - 1),
            randint(self.origin.y, self.origin.y + self.size.height - 1))

    def get_random_rect(self, min_size=Size(1, 1)):
        """
        :param Size min_size:
        :returns: :py:class:`Rect`

        Returns a random rect inside ``self`` with the given minimum size. Returns
        ``self`` unmodified if size won't fit.
        """
        if self.width <= min_size.width:
            return self
        if self.height <= min_size.height:
            return self
        width = randint(min_size.width, self.width)
        height = randint(min_size.height, self.height)
        x = self.origin.x + randint(0, self.size.width - width)
        y = self.origin.y + randint(0, self.size.height - height)
        if width == 0 or height == 0:
            raise ValueError((x, y, width, height, self))
        return Rect(Point(x, y), Size(width, height))
