__version__ = '0.2.1'

from itertools import chain

import numpy as np
import pyglet
from Polygon import Polygon, setDataStyle, STYLE_NUMPY
from Polygon.Utils import pointList as point_list


setDataStyle(STYLE_NUMPY)


class Shape:
    """Graphical polygon primitive for use with `pyglet`_.

    Alternative constructor methods:

    - |Shape.circle|
    - |Shape.rectangle|
    - |Shape.regular_polygon|
    - |Shape.from_dict|

    Parameters
    ----------
    vertices : array-like or |Polygon|.
        If a |Polygon| is passed, its points will be used.
        Otherwise, `vertices` should be a sequence of `[x, y]` locations or an array with x and y columns.
    color : str or 3-tuple of int, optional
        Color, in R, G, B format.
        Alternatively, a key that refers to an element of `colors`.
    velocity : array-like
        Speed and direction of motion, in [dx_dt, dy_dt] format.
    angular_velocity : float
        Speed of angular motion, in counter-clockwise radians per second.
    colors : dict of tuple, optional
        Named colors, defined as R, G, B tuples.
        Useful for easily switching between a set of colors.

    Attributes
    ----------
    poly : |Polygon|
        Associated |Polygon| object.
    vertices : |array|
        An array of points, with x and y columns. Read-only.
    center : |array|
        The centroid of the shape.
        Setting center calls |Shape.translate|.
    position : |array|
        Alias for `center`.
    radius : |array|
        Mean distance from each point to the center.
        Setting radius calls |Shape.scale|.
    color : str or tuple of int
        The current color, in R, G, B format if `colors` was not passed.
        Otherwise, the current color is represented as a key in `colors`.
    colors : dict of tuple
        Named colors.
    velocity : |array|
        Speed and direction of linear motion.
    Angular_velocity : float
        Speed of angular motion, in counter-clockwise radians per second.
    enabled : bool
        If False, the shape will not be drawn.

    """
    def __init__(self, vertices, color=(255, 255, 255), velocity=(0, 0), angular_velocity=0, colors=None):
        if isinstance(vertices, Polygon):
            self.poly = vertices
        else:
            self.poly = Polygon(vertices)

        self.colors = colors
        self._color = 'primary'
        if colors:
            self.color = color

        else:
            self.colors = {'primary': color}

        self.velocity = np.asarray(velocity)
        self.angular_velocity = angular_velocity

        # Construct vertex_list.
        self._vertex_list = self._get_vertex_list()
        self.enabled = True

    @classmethod
    def regular_polygon(cls, center, radius, n_vertices, start_angle=0, **kwargs):
        """Construct a regular polygon.

        Parameters
        ----------
        center : array-like
        radius : float
        n_vertices : int
        start_angle : float, optional
            Where to put the first point, relative to `center`,
            in radians counter-clockwise starting from the horizontal axis.
        kwargs
            Other keyword arguments are passed to the |Shape| constructor.

        """
        angles = (np.arange(n_vertices) * 2 * np.pi / n_vertices) + start_angle
        return cls(center + radius * np.array([np.cos(angles), np.sin(angles)]).T, **kwargs)

    @classmethod
    def circle(cls, center, radius, n_vertices=50, **kwargs):
        """Construct a circle.

        Parameters
        ----------
        center : array-like
        radius : float
        n_vertices : int, optional
            Number of points to draw.
            Decrease for performance, increase for appearance.
        kwargs
            Other keyword arguments are passed to the |Shape| constructor.

        """
        return cls.regular_polygon(center, radius, n_vertices, **kwargs)

    @classmethod
    def rectangle(cls, vertices, **kwargs):
        """Shortcut for creating a rectangle aligned with the screen axes from only two corners.

        Parameters
        ----------
        vertices : array-like
            An array containing the ``[x, y]`` positions of two corners.
        kwargs
            Other keyword arguments are passed to the |Shape| constructor.

        """
        bottom_left, top_right = vertices
        top_left = [bottom_left[0], top_right[1]]
        bottom_right = [top_right[0], bottom_left[1]]
        return cls([bottom_left, bottom_right, top_right, top_left], **kwargs)

    @classmethod
    def from_dict(cls, spec):
        """Create a |Shape| from a dictionary specification.

        Parameters
        ----------
        spec : dict
            A dictionary with either the fields ``'center'`` and ``'radius'`` (for a circle),
            ``'center'``, ``'radius'``, and ``'n_vertices'`` (for a regular polygon),
            or ``'vertices'``.
            If only two vertices are given, they are assumed to be lower left and top right corners of a rectangle.
            Other fields are interpreted as keyword arguments.

        """
        spec = spec.copy()
        center = spec.pop('center', None)
        radius = spec.pop('radius', None)
        if center and radius:
            return cls.circle(center, radius, **spec)

        vertices = spec.pop('vertices')
        if len(vertices) == 2:
            return cls.rectangle(vertices, **spec)

        return cls(vertices, **spec)

    @property
    def vertices(self):
        return np.asarray(point_list(self.poly))

    @property
    def color(self):
        if len(self.colors) == 1:
            return self.colors[self._color]
        else:
            return self._color

    @color.setter
    def color(self, value):
        if value in self.colors:
            self._color = value
        else:
            self.colors[self._color] = value

    @property
    def _kwargs(self):
        """Keyword arguments for recreating the Shape from the vertices.

        """
        return dict(color=self.color, velocity=self.velocity, colors=self.colors)

    @property
    def center(self):
        return np.asarray(self.poly.center())

    @center.setter
    def center(self, value):
        self.translate(np.asarray(value) - self.center)

    @property
    def radius(self):
        return np.linalg.norm(self.vertices - self.center, axis=1).mean()

    @radius.setter
    def radius(self, value):
        self.scale(value / self.radius)

    @property
    def _gl_vertices(self):
        return list(chain(self.center, *point_list(self.poly)))

    @property
    def _gl_colors(self):
        return (len(self) + 1) * self.colors[self._color]

    def distance_to(self, point):
        """Distance from center to arbitrary point.

        Parameters
        ----------
        point : array-like

        Returns
        -------
        float

        """
        return np.linalg.norm(self.center - point)

    def scale(self, factor, center=None):
        """Resize the shape by a proportion (e.g., 1 is unchanged), in-place.

        Parameters
        ----------
        factor : float or array-like
            If a scalar, the same factor will be applied in the x and y dimensions.
        center : array-like, optional
            Point around which to perform the scaling.
            If not passed, the center of the shape is used.

        """
        factor = np.asarray(factor)

        if len(factor.shape):
            args = list(factor)
        else:
            args = [factor, factor]
        if center is not None:
            args.extend(center)

        self.poly.scale(*args)
        return self

    def translate(self, vector):
        """Translate the shape along a vector, in-place.

        Parameters
        ----------
        vector : array-like

        """
        self.poly.shift(*vector)

    def rotate(self, angle, center=None):
        """Rotate the shape, in-place.

        Parameters
        ----------
        angle : float
            Angle to rotate, in radians counter-clockwise.
        center : array-like, optional
            Point about which to rotate.
            If not passed, the center of the shape will be used.

        """
        args = [angle]
        if center is not None:
            args.extend(center)
        self.poly.rotate(*args)
        return self

    def flip_x(self, center=None):
        """Flip the shape in the x direction, in-place.

        Parameters
        ----------
        center : array-like, optional
            Point about which to flip.
            If not passed, the center of the shape will be used.

         """
        if center is None:
            self.poly.flip()
        else:
            self.poly.flip(center[0])

    def flip_y(self, center=None):
        """Flip the shape in the y direction, in-place.

        Parameters
        ----------
        center : array-like, optional
            Point about which to flip.
            If not passed, the center of the shape will be used.

         """
        if center is None:
            self.poly.flop()
        else:
            self.poly.flop(center[1])
        return self

    def flip(self, angle, center=None):
        """ Flip the shape in an arbitrary direction.

        Parameters
        ----------
        angle : array-like
            The angle, in radians counter-clockwise from the horizontal axis,
            defining the angle about which to flip the shape (of a line through `center`).
        center : array-like, optional
            The point about which to flip.
            If not passed, the center of the shape will be used.

        """
        return self.rotate(-angle, center=center).flip_y(center=center).rotate(angle, center=center)

    def _get_vertex_list(self):
        indices = []
        for i in range(1, len(self) + 1):
            indices.extend([0, i, i + 1])
        indices[-1] = 1
        return pyglet.graphics.vertex_list_indexed(
            len(self) + 1, indices, ('v2f', self._gl_vertices), ('c3B', self._gl_colors))

    def draw(self):
        """Draw the shape in the current OpenGL context.

        """
        if self.enabled:
            self._vertex_list.colors = self._gl_colors
            self._vertex_list.vertices = self._gl_vertices
            self._vertex_list.draw(pyglet.gl.GL_TRIANGLES)

    def update(self, dt):
        """Update the shape's position by moving it forward according to its velocity.

        Parameters
        ----------
        dt : float

        """
        self.translate(dt * self.velocity)
        self.rotate(dt * self.angular_velocity)

    def enable(self, enabled):
        """Set whether the shape should be drawn.

        Parameters
        ----------

        enabled : bool

        """
        self.enabled = enabled
        return self

    def overlaps(self, other):
        """Check if two shapes overlap.

        Parameters
        ----------
        other : |Shape|

        Returns
        -------
        bool

        """
        return bool(self.poly.overlaps(other.poly))

    def covers(self, other):
        """Check if the shape completely covers another shape.

        Parameters
        ----------
        other : |Shape|

        Returns
        -------
        bool

        """
        return bool(self.poly.covers(other.poly))

    def __repr__(self):
        kwarg_strs = []
        for arg, value in self._kwargs.items():
            if isinstance(value, str):
                value_str = "'{}'".format(value)
            elif isinstance(value, np.ndarray):
                value_str = '[{}, {}]'.format(*value)
            else:
                value_str = str(value)
            kwarg_strs.append(arg + '=' + value_str)
        kwargs = ',\n' + ', '.join(kwarg_strs)

        return '{cls}({points}{kwargs})'.format(
            cls=type(self).__name__,
            points='[{}]'.format(',\n'.join('[{}, {}]'.format(x, y) for x, y in self.vertices)),
            kwargs=kwargs,
        )

    def __eq__(self, other):
        if isinstance(other, Shape):
            if len(self) != len(other):
                return False
            return (np.all(np.isclose(np.sort(self.vertices, axis=0), np.sort(other.vertices, axis=0))) and
                    self.colors == other.colors and
                    self.color == other.color and
                    np.all(np.isclose(self.velocity, other.velocity)))
        else:
            return False

    def __bool__(self):
        return True

    def __getitem__(self, item):
        return self.vertices[item]

    def __len__(self):
        return self.poly.nPoints()

    def __add__(self, other):
        if isinstance(other, Shape):
            return type(self)(self.poly + other.poly)
        return type(self)(self.vertices + other, **self._kwargs)

    __radd__ = __add__

    def __sub__(self, other):
        if isinstance(other, Shape):
            return type(self)(self.poly - other.poly)
        return type(self)(self.vertices - other, **self._kwargs)

    def __mul__(self, other):
        return type(self)(self.vertices * other, **self._kwargs)

    def __rmul__(self, other):
        return type(self)(other * self.vertices, **self._kwargs)

    def __truediv__(self, other):
        return type(self)(self.vertices / other, **self._kwargs)

    __div__ = __truediv__

    def __xor__(self, other):
        return type(self)(self.poly ^ other.poly, **self._kwargs)

    def __and__(self, other):
        return type(self)(self.poly & other.poly, **self._kwargs)

    def __or__(self, other):
        return type(self)(self.poly | other.poly, **self._kwargs)

    def __iadd__(self, other):
        self.translate(other)
        return self

    def __isub__(self, other):
        self.translate(-np.asarray(other))
        return self

    def __imul__(self, other):
        if isinstance(other, int) or isinstance(other, float):
            self.poly.scale(other, other)
        elif len(other) == 2:
            self.poly.scale(*other)
        return self

    def __itruediv__(self, other):
        if isinstance(other, int) or isinstance(other, float):
            self.poly.scale(1/other, 1/other)
        elif len(other) == 2:
            self.poly.scale(1/other[0], 1/other[1])
        return self

    __idiv__ = __itruediv__

    position = center
