"""
Maestro 3D hemispheres.
The hemisphere module allows creation of hemispheres.  Clients add hemispheres
to Group instances, and Maesto draws them.
Control over the center, radius, color, resolution and opacity of a hemisphere
are provided. See the Hemisphere class for more info.
To add any number of hemispheres, create the Hemisphere instance and add it to
a Group instance. Hemispheres can be shown or hidden through the Group instance.
Copyright Schrodinger, LLC. All rights reserved.
"""
import numpy
import schrodinger
from . import common
from .common import OPACITY_MAX
from .common import OPACITY_MIN
from .common import TRANSPARENCY_MAX  # noqa: F401
from .common import TRANSPARENCY_MIN  # noqa: F401
from .common import Group  # noqa: F401
maestro = schrodinger.get_maestro()
RESOLUTION_MIN = 4
RESOLUTION_MAX = 50
RESOLUTION_DEFAULT = 16
TRANSPARENCY_DEFAULT = 0.0  # for backwards-compatibility
OPACITY_DEFAULT = 1.0
# Constants used to calculate bounding box:
BOUNDING_BOX_INIT_VALUE = 100000000.0
#############################################################################
#                              CLASSES                                      #
#############################################################################
[docs]class MaestroHemisphere(common._MaestroPrimitiveMixin, common.Primitive):
    """
    Class to create a 3D hemisphere in Maestro.  This hemisphere will be drawn
    at the appropriate time in Maestro to interact properly with transparent
    objects.
    Hemispheres should be added to a Group and drawing done via the Group. See
    Group documentation.
    API Example::
        import schrodinger.maestro.maestro as maestro
        import schrodinger.graphics3d.common as common
        import schrodinger.graphics3d.hemisphere as hemisphere
        hemisphere_grp = common.Group()
        st = maestro.workspace_get()
        for at in st.atom:
            hemisph = hemisphere.MaestroHemisphere(
                center=(at.x, at.y, at.z)
                radius=1.0,
                resolution=20,
                opacity=0.8,
                color='red'
            )
            # Add the primative to the container.
            hemisphere_grp.add(hemisph)
        # Unlike Hemisphere MaestroHemisphere simply needs to be shown to be
        #  drawn.
        # No special callback like there is for Hemispheres is needed.
        hemisphere_grp.show()
        # Hide the markers.
        hemisphere_grp.hide()
        # Remove the markers
        hemisphere_grp.clear()
    """
[docs]    def __init__(self,
                 center=None,
                 color=None,
                 direction=(0.0, 0.0, 1.0),
                 radius=None,
                 opacity=OPACITY_DEFAULT,
                 resolution=RESOLUTION_DEFAULT,
                 angle_dep_transparency=False):
        """
        Constructor requires:
        :param center: List of 3 Angstrom values indicating the center
                        coordinate of the icosahedron.
        :type center: list(float, float, float)
        color:    One of:
                    Color object
                    Color name (string)
                    Tuple of (R, G, B) (each 0.0-1.0)
        radius:   radius of the hemisphere in Angstroms
        Optional arguments:
        opacity:  0.0 (transparent) through 1.0 (opaque)
                       Defaults to 1.0
        resolution:    4 to 50
                       Defaults to 16
        angle_dep_transparency: if true, then we modify the color of the
                                hemisphere based on the angle from the center
        """
        # Need to set this first because it is used by helper functions when
        # we set properties.
        self.hemisphere = None
        # These variables are all accessed when assign to .x, .y, or .z, so
        # they need to be initialized here.
        self._x = 0
        self._y = 0
        self._z = 0
        if center is None:
            raise ValueError(
                "Must specify center value to define the hemisphere center")
        elif radius is None:
            raise ValueError("Must specify a value for radius")
        else:
            self.x, self.y, self.z = center
            self.radius = radius
            self.direction = direction
        if color is not None:
            self.r, self.g, self.b = common.color_arg_to_rgb(color)
        else:
            raise ValueError("Must specify a color")
        # Clamp to range of 0.0 and 1.0, inclusive
        if opacity < OPACITY_MIN:
            self.opacity = OPACITY_MIN
        elif opacity > OPACITY_MAX:
            self.opacity = OPACITY_MAX
        else:
            self.opacity = opacity
        # Clamp the resolution
        if resolution < RESOLUTION_MIN:
            self.resolution = RESOLUTION_MIN
        elif resolution > RESOLUTION_MAX:
            self.resolution = RESOLUTION_MAX
        else:
            self.resolution = resolution
        self.angle_dep_transparency = angle_dep_transparency
        self.hemisphere = maestro.create_hemisphere(
            self.x, self.y, self.z, self.direction[0], self.direction[1],
            self.direction[2], self.r, self.g, self.b, self.radius,
            self.opacity, self.resolution, self.angle_dep_transparency)
        maestro_objects = [self.hemisphere]
        common.Primitive.__init__(self, maestro_objects) 
    def _calculateBoundingBox(self, mat):
        xyzmin = []
        xyzmax = []
        for k in range(6):
            xyzmin.append(BOUNDING_BOX_INIT_VALUE)
            xyzmax.append(-BOUNDING_BOX_INIT_VALUE)
        x = self.x
        y = self.y
        z = self.z
        r = self.radius
        vertex = [[x - r, y - r, z + r],
                  [x + r, y - r, z + r],
                  [x + r, y + r, z + r],
                  [x - r, y + r, z + r],
                  [x - r, y - r, z - r],
                  [x + r, y - r, z - r],
                  [x + r, y + r, z - r],
                  [x - r, y + r, z - r]] # yapf: disable
        transform = numpy.matrix(mat)
        tmp = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
        for i in range(8):
            for k in range(3):
                tmp[k + 3] = vertex[i][k]
            vec = numpy.array([tmp[3], tmp[4], tmp[5], 1])
            out = transform.dot(vec)
            for k in range(3):
                tmp[k] = out[0, k]
            xyzmin = numpy.minimum(xyzmin, tmp).tolist()
            xyzmax = numpy.maximum(xyzmax, tmp).tolist()
        return (xyzmin, xyzmax)
    # Helper functions
[docs]    def setCoords(self):
        if self.hemisphere:
            maestro.set_coords(self.hemisphere, self._x, self._y, self._z) 
[docs]    def setXYZCoords(self, x, y, z):
        self._x = x
        self._y = y
        self._z = z
        self.setCoords() 
    # Properties
    @property
    def x(self):
        """Get X coordinate"""
        return self._x
    @x.setter
    def x(self, value):
        self._x = value
        self.setCoords()
    @property
    def y(self):
        """Get Y coordinate"""
        return self._y
    @y.setter
    def y(self, value):
        self._y = value
        self.setCoords()
    @property
    def z(self):
        """Get Z coordinate"""
        return self._z
    @z.setter
    def z(self, value):
        self._z = value
        self.setCoords()
    @property
    def radius(self):
        """Get hemisphere's radius in angstroms"""
        return self._radius
    @radius.setter
    def radius(self, value):
        if value <= 0.:
            raise ValueError("Radius must be greater than 0.")
        self._radius = value
        if self.hemisphere:
            maestro.set_radius(self.hemisphere, self._radius)