"""
Module containing all table functionality for the WaterMap
results GUI.
"""
#- Imports -------------------------------------------------------------------
import numpy as np
from schrodinger.graphics3d import polyhedron
from schrodinger.graphics3d import sphere
from schrodinger.structutils import color
#- Globals -------------------------------------------------------------------
WATERMAP_MATERIAL = "watermap"
ENTRY_SCHEME = color.get_color_scheme("entry")
ENTRY_SCHEME_COLORS = [
    color.Color(rule.getColorName()) for rule in ENTRY_SCHEME
]
# These thresholds are used for absolute coloring (Ev:93561):
ENTROPY_MIN_THRESHOLD = 0.0
ENTROPY_MAX_THRESHOLD = 5.0
ENTHALPY_MIN_THRESHOLD = -2.5
ENTHALPY_MAX_THRESHOLD = +2.5
FREES_MIN_THRESHOLD = ENTROPY_MIN_THRESHOLD + ENTHALPY_MIN_THRESHOLD
FREES_MAX_THRESHOLD = ENTROPY_MAX_THRESHOLD + ENTHALPY_MAX_THRESHOLD
#- Classes -------------------------------------------------------------------
[docs]class ShapeFactory(object):
    """
    Class to create a 3D object from one of the available 3D objects in
    `schrodinger.graphics3d`. This class unifies the needed arguments
    regardless of object type.  For example a `schrodinger.graphics3d.box.Box`
    uses the keyword `extents` but this class will convert `radius` to
    `extents`.
    """
[docs]    def __init__(self,
                 entry_id,
                 shape_index,
                 center,
                 radius=0.2,
                 color="red",
                 resolution=20,
                 opacity=0.8,
                 style=1,
                 by_entry=False):
        """
        :param   center: The x, y, z coordinates of the HS the shape will
                         be added to.
        :type    center: list of 3 floats
        :param   radius: The radius of the hydration site. Default: 0.2 ("off")
        :type    radius: float
        :param by_entry: Whether the shapes will be singular or different based
                         on entry id.
        :type  by_entry: bool
        """
        self.entry_id = int(entry_id)
        self.shape_index = int(shape_index)
        self.center = center
        self.radius = float(radius)
        self.color = color
        self.resolution = resolution
        self.opacity = opacity
        self.style = style
        shape_map = [
            ('sphere', self.sphere),
            ('tetrahedron', self.tetrahedron),
            ('box', self.box),
            ('octahedron', self.octahedron),
            ('dodecahedron', self.dodecahedron),
            ('icosahedron', self.icosahedron),
        ]
        # Use spheres if the "Shape by Entry" is not toggled
        if not by_entry:
            shape_idx = 0
        else:
            shape_idx = self.shape_index % len(shape_map)
        self.shape_type, self.func = shape_map[shape_idx] 
    @property
    def obj(self):
        """
        Property that returns the shape object.  This needs to be set as a
        property and not in the init or there will be references left to
        the object and their `clear` method will not clear them from the
        workspace.
        """
        shape_obj = self.func()
        shape_obj.setEntryID(str(self.entry_id))
        return shape_obj
    @property
    def _volume(self):
        """
        Private `_volume` property getter. Using the property decorator because
        `_volume` is never passed into this class, only `radius`. This makes
        it much easier to have the correct value for volume.
        Note the volume is always based on a sphereical hydration site.
        """
        return (4.0 / 3.0) * np.pi * (self.radius**3)
    @property
    def _box_length(self):
        """
        Private method to get the edge length of a box based on radius.
        The hydration sites in WaterMap all represent spherical information
        so the edge of the box should not go beyond the spherical sapce.
        radius of circumscribed sphere = (sqrt(3)/2) * edge length
        Using the property decorator because `_box_length` is never passed
        into this class, only `radius`. This makes it much easier to have
        the correct value for volume.
        """
        return self._volume**(1.0 / 3.0)
    # Removed due to issues with shapes, see Ev:107523
    """
    def changeSize(self, radius):
        Change the size of the shape.
        :param radius: The new radius
        :type  radius: float
        self.radius  = radius
        if self.shape_type == 'sphere':
            self.obj.radius = radius
        elif self.shape_type == 'box':
            self.obj = self.box()
        # Must be a polyhedron
        else:
            self.obj.updateVertices(self.center, volume=self._volume)
    """
[docs]    def sphere(self):
        """ Return a `sphere.MaestroSphere` object """
        x, y, z = self.center[0], self.center[1], self.center[2]
        obj = sphere.MaestroSphere(x=x,
                                   y=y,
                                   z=z,
                                   radius=self.radius,
                                   resolution=self.resolution,
                                   opacity=self.opacity,
                                   color=self.color)
        obj.material = WATERMAP_MATERIAL
        return obj 
[docs]    def box(self):
        """ Return a `polyhedron.Cube` object """
        obj = polyhedron.MaestroCube(self.center,
                                     polyhedron.MODE_MAESTRO,
                                     length=self._box_length,
                                     color=self.color,
                                     opacity=self.opacity,
                                     style=self.style)
        return obj 
[docs]    def tetrahedron(self):
        """ Return a `polyhedron.Tetrahedron` object """
        # Decrease the size of the shape since it is least spherical,
        # making the edges quite long Ev:112063
        volume = 0.65 * self._volume
        obj = polyhedron.MaestroTetrahedron(self.center,
                                            polyhedron.MODE_MAESTRO,
                                            volume=volume,
                                            color=self.color,
                                            opacity=self.opacity,
                                            style=self.style)
        return obj 
[docs]    def octahedron(self):
        """ Return a `polyhedron.Octahedron` object """
        obj = polyhedron.MaestroOctahedron(self.center,
                                           polyhedron.MODE_MAESTRO,
                                           volume=self._volume,
                                           color=self.color,
                                           opacity=self.opacity,
                                           style=self.style)
        return obj 
[docs]    def dodecahedron(self):
        """ Return a `polyhedron.Dodecahedron` object """
        obj = polyhedron.MaestroDodecahedron(self.center,
                                             polyhedron.MODE_MAESTRO,
                                             volume=self._volume,
                                             color=self.color,
                                             opacity=self.opacity,
                                             style=self.style)
        return obj 
[docs]    def icosahedron(self):
        """Return a `polyhedron.Icosahedron` object"""
        obj = polyhedron.MaestroIcosahedron(self.center,
                                            polyhedron.MODE_MAESTRO,
                                            volume=self._volume,
                                            color=self.color,
                                            opacity=self.opacity,
                                            style=self.style)
        return obj  
[docs]def get_absolute_free_ratio(delta_dG):
    """
    Normalize the difference in free energy to 0-1 (absolute)
    :return: Normalized delta_dG in range [0.0-1.0]
    :rtype: float
    """
    delta_dg = np.clip(delta_dG, FREES_MIN_THRESHOLD, FREES_MAX_THRESHOLD)
    return ((delta_dG - FREES_MIN_THRESHOLD) /
            (FREES_MAX_THRESHOLD - FREES_MIN_THRESHOLD)) 
[docs]def get_RGB_free(free_ratio, red_blue=False):
    """
    Get the RGB corresponding to the normalized free energy difference
    :param free_ratio: free energy [0.0, 1.0]
    :type free_ratio: float
    :param red_blue: Whether to use blue for more negative free ratio (instead
        of the default green)
    :type red_blue: bool
    """
    # More positive delta G should be red:
    r = free_ratio
    # By default, more negative delta G should be green
    g = 1.0 - free_ratio
    b = 0.0
    if red_blue:
        g, b = b, g
    return r, g, b 
[docs]def get_absolute_enthalpy_entropy_ratio(delta_enthalpy, delta_entropy):
    """
    Normalize the delta enthalpy and delta entropy values to 0-1 (absolute)
    :return: Normalized delta enthalpy and delta entropy in range [0.0-1.0]
    :rtype: tuple(float, float)
    """
    delta_enthalpy = np.clip(delta_enthalpy, ENTHALPY_MIN_THRESHOLD,
                             ENTHALPY_MAX_THRESHOLD)
    delta_entropy = np.clip(delta_entropy, ENTROPY_MIN_THRESHOLD,
                            ENTROPY_MAX_THRESHOLD)
    enthalpyr = ((delta_enthalpy - ENTHALPY_MIN_THRESHOLD) /
                 (ENTHALPY_MAX_THRESHOLD - ENTHALPY_MIN_THRESHOLD))
    entropyr = ((delta_entropy - ENTROPY_MIN_THRESHOLD) /
                (ENTROPY_MAX_THRESHOLD - ENTROPY_MIN_THRESHOLD))
    return enthalpyr, entropyr 
[docs]def get_RGB_enthalpy_entropy(enthalpyr, entropyr):
    """
    Returns r, g, b based on enthalpyr and entropyr.
    :param enthalpyr: relative enthalpy within range of (0, 1.0)
    :type  enthalpyr: float (>= 0.0)
    :param entropyr: relative entropy within range of (0, 1.0)
    :type  entropyr: float (>=0.0)
    """
    # high enthalpyr and entropyr should be faint
    # low enthalpyr and entropyr should be bright
    # enthalpyr & entropyr range from 0.0 to 1.0
    # Ev:69831            R    G    B
    # energy:  cyan      0.0  1.0  1.0
    # entropy: yellow    1.0  1.0  0.0
    # if energy is less than entropy:
    #    - color is between cyan and green
    #    - R=0
    #    - G=higher values (darker) for higher energy (unhappy)
    #    - B=ratio of 1-energy to sum of 1-energy and 1-entropy scaled by 1-energy
    # if energy is greater than entropy
    #    - color is be between yellow and green
    #    - R=ratio of 1-entropy to sum of 1-energy and 1-entropy scaled by 1-entropy
    #    - G=higher values (darker) for higher entropy (unhappy)
    #    - B=0
    # lower enthalpy=brighter cyan, low entropy=brighter yellow
    if enthalpyr <= entropyr:
        r = 0.0
        # lower free energy=brighter
        g = 1.0 - 0.3 * (enthalpyr + entropyr)
        entropyr = max(1.0e-100, entropyr)  # prevent entropyr = 0.0
        # the more enthalpy contributes to free energy, the more cyan
        b = 1.0 - (enthalpyr / entropyr)
    else:
        enthalpyr = max(1.0e-100, enthalpyr)  # prevent enthalpyr = 0.0
        # the more entropy contributes to free energy, the more yellow
        r = 1.0 - (entropyr / enthalpyr)
        # lower free energy=brighther
        g = 1.0 - 0.3 * (enthalpyr + entropyr)
        b = 0.0
    return (r, g, b) 
[docs]def get_color_by_entry(entry_id, all_entry_ids, *, colors=ENTRY_SCHEME_COLORS):
    """
    Get a color from the entry color scheme relative to `entry_id`'s sorted
    position in `all_entry_ids`
    :param entry_id: The entry ID to get the color for
    :type entry_id: str or int
    :param all_entry_ids: Iterable of all entry IDs
    :type all_entry_ids: typing.Iterable[str | int]
    :param colors: Colors to use (defaults to ENTRY_SCHEME_COLORS)
    :type colors: typing.Sequence
    :return: Color item from `colors`
    :rtype: schrodinger.structutils.color.Color
    """
    # Sort ids based on int value
    wm_ids = sorted(int(i) for i in all_entry_ids)
    index = wm_ids.index(int(entry_id)) % len(colors)
    return colors[index]