from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

from math import cos
from math import pi
from math import sin

from compas.geometry import matrix_from_frame
from compas.geometry import transform_points
from compas.geometry import Frame
from compas.geometry import Plane

from ._shape import Shape


class Torus(Shape):
    """A torus is defined by a plane and two radii.

    Parameters
    ----------
    plane : [point, normal] | :class:`~compas.geometry.Plane`
        The plane of the torus.
    radius_axis: float
        The radius of the axis.
    radius_pipe: float
        The radius of the pipe.

    Attributes
    ----------
    plane : :class:`~compas.geometry.Plane`
        The torus' plane.
    radius_axis : float
        The radius of the axis.
    radius_pipe : float
        The radius of the pipe.
    center : :class:`~compas.geometry.Point`, read-only
        The centre of the torus.
    area : float, read-only
        The surface area of the torus.
    volume : float, read-only
        The volume of the torus.

    Examples
    --------
    >>> from compas.geometry import Plane
    >>> from compas.geometry import Torus
    >>> torus = Torus(Plane.worldXY(), 5., 2.)

    >>> from compas.geometry import Plane
    >>> from compas.geometry import Torus
    >>> torus = Torus(Plane.worldXY(), 5, 2)
    >>> sdict = {'plane': Plane.worldXY().data, 'radius_axis': 5., 'radius_pipe': 2.}
    >>> sdict == torus.data
    True

    """

    JSONSCHEMA = {
        "type": "object",
        "properties": {
            "plane": Plane.JSONSCHEMA,
            "radius_axis": {"type": "number", "exclusiveMinimum": 0},
            "radius_pipe": {"type": "number", "exclusiveMinimum": 0},
        },
        "required": ["plane", "radius_axis", "radius_pipe"],
    }

    __slots__ = ["_plane", "_radius_axis", "_radius_pipe"]

    def __init__(self, plane, radius_axis, radius_pipe, **kwargs):
        super(Torus, self).__init__(**kwargs)
        self._plane = None
        self._radius_axis = None
        self._radius_pipe = None
        self.plane = plane
        self.radius_axis = radius_axis
        self.radius_pipe = radius_pipe

    # ==========================================================================
    # data
    # ==========================================================================

    @property
    def data(self):
        """dict : Returns the data dictionary that represents the torus."""
        return {
            "plane": self.plane,
            "radius_axis": self.radius_axis,
            "radius_pipe": self.radius_pipe,
        }

    @data.setter
    def data(self, data):
        self.plane = data["plane"]
        self.radius_axis = data["radius_axis"]
        self.radius_pipe = data["radius_pipe"]

    @classmethod
    def from_data(cls, data):
        """Construct a torus from its data representation.

        Parameters
        ----------
        data : dict
            The data dictionary.

        Returns
        -------
        :class:`~compas.geometry.Torus`
            The constructed torus.

        Examples
        --------
        >>> from compas.geometry import Torus
        >>> data = {'plane': Plane.worldXY().data, 'radius_axis': 4., 'radius_pipe': 1.}
        >>> torus = Torus.from_data(data)

        """
        torus = cls(data["plane"], data["radius_axis"], data["radius_pipe"])
        return torus

    # ==========================================================================
    # properties
    # ==========================================================================

    @property
    def plane(self):
        return self._plane

    @plane.setter
    def plane(self, plane):
        self._plane = Plane(*plane)

    @property
    def radius_axis(self):
        return self._radius_axis

    @radius_axis.setter
    def radius_axis(self, radius):
        self._radius_axis = float(radius)

    @property
    def radius_pipe(self):
        return self._radius_pipe

    @radius_pipe.setter
    def radius_pipe(self, radius):
        self._radius_pipe = float(radius)

    @property
    def center(self):
        return self.plane.point

    @property
    def area(self):
        return (2 * pi * self.radius_pipe) * (2 * pi * self.radius_axis)

    @property
    def volume(self):
        return (pi * self.radius_pipe**2) * (2 * pi * self.radius_axis)

    # ==========================================================================
    # customisation
    # ==========================================================================

    def __repr__(self):
        return "Torus({0!r}, {1!r}, {2!r})".format(self.plane, self.radius_axis, self.radius_pipe)

    def __len__(self):
        return 3

    def __getitem__(self, key):
        if key == 0:
            return self.plane
        elif key == 1:
            return self.radius_axis
        elif key == 2:
            return self.radius_pipe
        else:
            raise KeyError

    def __setitem__(self, key, value):
        if key == 0:
            self.plane = value
        elif key == 1:
            self.radius_axis = value
        elif key == 2:
            self.radius_pipe = value
        else:
            raise KeyError

    def __iter__(self):
        return iter([self.plane, self.radius_axis, self.radius_pipe])

    # ==========================================================================
    # constructors
    # ==========================================================================

    # ==========================================================================
    # methods
    # ==========================================================================

    def to_vertices_and_faces(self, u=16, v=16, triangulated=False):
        """Returns a list of vertices and faces

        Parameters
        ----------
        u : int, optional
            Number of faces in the "u" direction.
        v : int, optional
            Number of faces in the "v" direction.
        triangulated: bool, optional
            If True, triangulate the faces.

        Returns
        -------
        list[list[float]]
            A list of vertex locations.
        list[list[int]]
            And a list of faces,
            with each face defined as a list of indices into the list of vertices.

        """
        if u < 3:
            raise ValueError("The value for u should be u > 3.")
        if v < 3:
            raise ValueError("The value for v should be v > 3.")

        theta = pi * 2 / u
        phi = pi * 2 / v
        vertices = []
        for i in range(u):
            for j in range(v):
                x = cos(i * theta) * (self.radius_axis + self.radius_pipe * cos(j * phi))
                y = sin(i * theta) * (self.radius_axis + self.radius_pipe * cos(j * phi))
                z = self.radius_pipe * sin(j * phi)
                vertices.append([x, y, z])

        # transform vertices to torus' plane
        frame = Frame.from_plane(self.plane)
        M = matrix_from_frame(frame)
        vertices = transform_points(vertices, M)

        faces = []
        for i in range(u):
            ii = (i + 1) % u
            for j in range(v):
                jj = (j + 1) % v
                a = i * v + j
                b = ii * v + j
                c = ii * v + jj
                d = i * v + jj
                faces.append([a, b, c, d])

        if triangulated:
            triangles = []
            for face in faces:
                if len(face) == 4:
                    triangles.append(face[0:3])
                    triangles.append([face[0], face[2], face[3]])
                else:
                    triangles.append(face)
            faces = triangles

        return vertices, faces

    def transform(self, transformation):
        """Transform the torus.

        Parameters
        ----------
        transformation : :class:`~compas.geometry.Transformation`
            The transformation used to transform the Torus.

        Returns
        -------
        None

        Examples
        --------
        >>> from compas.geometry import Frame
        >>> from compas.geometry import Plane
        >>> from compas.geometry import Transformation
        >>> from compas.geometry import Torus
        >>> torus = Torus(Plane.worldXY(), 5, 2)
        >>> frame = Frame([1, 1, 1], [0.68, 0.68, 0.27], [-0.67, 0.73, -0.15])
        >>> T = Transformation.from_frame(frame)
        >>> torus.transform(T)

        """
        self.plane.transform(transformation)
