"""
A module containing a class for labeling atoms in the workspace.
Copyright Schrodinger, LLC. All rights reserved.
"""
from schrodinger.structutils import analyze
try:
    from schrodinger.maestro import maestro
except ImportError:
    maestro = None
SAVE_FMT_PROP = 's_bioluminate_saved_label_format'
DEFAULT_OPTIONS = {
    '1charge': 'FALSE',
    '1chargeformatstring': '%C1.3',
    '2charge': 'FALSE',
    '2chargeformatstring': '%C2.3',
    'acolor': 'TRUE',
    'anum': 'FALSE',
    'anumformatstring': '%NU',
    'atomicnumber': 'FALSE',
    'atomname': 'FALSE',
    'atomnameformatstring': '%AT',
    'atype': 'FALSE',
    'atypeformatstring': '%TY',
    'chain': 'FALSE',
    'chainformatstring': '%CH',
    'chirality': 'FALSE',
    'chiralityformatstring': '%CY',
    'cindex': '2',
    'compositionfields': '',
    'dmsopka': 'FALSE',
    'dmsopkaformatstring': '%DP',
    'element': 'FALSE',
    'elementformatstring': '%EL',
    'entryname': 'FALSE',
    'entrynameformatstring': '%EN',
    'entrypropertynames': '',
    'font': '',
    'font_size': '14',
    'font_style': 'bold',
    'formalcharge': 'FALSE',
    'formalchargeformatstring': '%FC',
    'growname': 'FALSE',
    'grownameformatstring': '%GN',
    'h2opka': 'FALSE',
    'h2opkaformatstring': '%HP',
    'headings': 'FALSE',
    'isotopenumber': 'FALSE',
    'isotopenumformatstring': '%IN',
    'keeplabels': 'on_top',
    'mode': 'replace',
    'molnum': 'FALSE',
    'molnumentry': 'FALSE',
    'molnumentryformatstring': '%ME',
    'numentry': 'FALSE',
    'numentryformatstring': '%NE',
    'nummol': 'FALSE',
    'nummolformatstring': '%NM',
    'occupancy': 'FALSE',
    'occupancyformatstring': '%OC',
    'oneletter': 'FALSE',
    'pdbname': 'FALSE',
    'pdbnameformatstring': '%PA',
    'reapply': 'FALSE',
    'reapplylabels': 'FALSE',
    'resname': 'FALSE',
    'resnameformatstring': '%RT',
    'resnum': 'FALSE',
    'resnumformatstring': '%RN',
    'separator': '',
    'showhisotopes': 'TRUE',
    'showlabel': 'FALSE',
    'stereochemistry': 'FALSE',
    'stereochemistryformatstring': '%ST',
    'title': 'FALSE',
    'titleformatstring': '%ET',
    'user': 'FALSE',
    'useratompropertyformatstring': '%UA',
    'utext': '',
    'xoffset': '0.05',
    'xyz': 'FALSE',
    'xyzformatstring': '%XY',
    'yoffset': '-0.15'
}
[docs]def removeBadOptions():
    """
    Test each option in DEFAULT_OPTIONS and remove any bad or deprecated ones.
    """
    badoptions = set()
    for option in DEFAULT_OPTIONS:
        try:
            maestro.get_command_option('labelatom', option)
        except KeyError:  #If an option has been deprecated or removed
            badoptions.add(option)
    for option in badoptions:
        del DEFAULT_OPTIONS[option] 
if maestro:
    removeBadOptions()
[docs]class WorkspaceLabeler(object):
    """
    Class with methods for manipulating workspace atom labels.
    """
[docs]    def __init__(self):
        self.saved_options = {} 
[docs]    def resetLabelOptions(self):
        """
        Resets Maestro's internally persistent label options to ensure
        consistent behavior of the labeler regardless of intervening calls to
        the Maestro command 'labelatom' that might change label options.
        It is generally not necessary to use this command; however it could be
        useful when invoking custom 'labelatom' commands.
        """
        maestro.command('labelatomfieldsnone') 
[docs]    def saveLabelOptions(self, options=None):
        """
        Saves the state of all of Maestro's 'labelatom' options, generally for
        the purpose of changing the label options by the WorkspaceLabeler class
        or by other Python scripts and then changing them back to their original
        state.
        :type options: dict
        :param options: dictionary containing a set of options and values
            Default: saved_options
        """
        if not options:
            options = self.saved_options
        for option in DEFAULT_OPTIONS:
            options[option] = maestro.get_command_option('labelatom', option) 
[docs]    def restoreLabelOptions(self, options=None):
        """
        Restores the state of all of Maestro's 'labelatom' options from a
        dictionary, generally for the purpose of restoring them to their
        original state.
        :type options: dict
        :param options: dictionary containing a set of options and values
            Default: saved_options
        """
        if not options:
            options = self.saved_options
        option_string = ''
        for option, value in options.items():
            option_string += option + '="' + value + '" '
        maestro.command('labelatom ' + option_string) 
[docs]    def labelAtoms(self, asl, options='atomname=TRUE'):
        """
        Labels the specified atoms. By default labelAtoms will label with
        atomname, but the label content can be customized with the options
        parameter.
        :type asl: str
        :param asl: ASL string specifying atoms to label
        :type options: str
        :param options: An options string that will be passed to the Maestro
                command 'labelatom' to specify the labeling behavior. See the
                Maestro Command Reference for full documentation.
            Default: 'atomname=TRUE'
        """
        self.saveLabelOptions()  #Save persistent option state
        self.resetLabelOptions()
        asl = '(' + asl + ')'  #Doesn't hurt and simplifies testing
        commandstring = ' '.join(('labelatom', options, asl))
        maestro.command(commandstring)
        self.restoreLabelOptions()  #Restore persistent option state 
[docs]    def clearLabels(self, asl):
        """
        Deletes all labels from the specified atoms.
        :type asl: str
        :param asl: ASL string specifying atoms to remove labels from
        """
        asl = '(' + asl + ')'  #Doesn't hurt and simplifies testing
        maestro.command('labelclear ' + asl) 
[docs]    def labelResidues(self, asl, options='resname=TRUE resnum=TRUE'):
        """
        Labels the alpha carbon of every residue that has at least one atom in
        the specified ASL. This means that labels may be added to atoms that
        are outside the ASL.
        By default labelAtoms will label each CA with the 3-letter residue code
        and residue number, but the label content can be customized with the
        options parameter.
        :type asl: str
        :param asl: ASL string specifying atoms that are in residues to label
        :type options: str
        :param options: An options string that will be passed to the Maestro
                command 'labelatom' to specify the labeling behavior. See the
                Maestro Command Reference for full documentation.
            Default: 'resname=TRUE resnum=TRUE'
        """
        ca_asl = 'fillres(' + asl + ') AND atom. CA'
        self.labelAtoms(ca_asl, options) 
[docs]    def labelChains(self, asl, options='chain=TRUE'):
        """
        Labels the first and last alpha carbon within the specified ASL of each
        chain that has at least one atom specified by the ASL. First and last
        CA are determined by residue numbering.
        By default, labelChains will label each of the two CA's with the chain
        name, but the label content can be customized with the options paramter.
        :type asl: str
        :param asl: ASL string specifying atoms that are in residues to label
        :type options: str
        :param options: An options string that will be passed to the Maestro
                command 'labelatom' to specify the labeling behavior. See the
                Maestro Command Reference for full documentation.
            Default: 'chain=TRUE'
        """
        workspace = maestro.workspace_get()
        asl = '(' + asl + ') AND not water'  # Don't want to label water
        atoms = analyze.get_atoms_from_asl(workspace, asl)
        firstatoms = {
        }  # Dictionaries keyed by (entryid, chain.name) containing
        lastatoms = {}  # (res, index), where res is (resnum, inscode)
        # Store the index of the highest- and lowest-index atom from each chain
        for atom in atoms:
            chainkey = (atom.entry_id, atom.chain)
            res = (atom.resnum, atom.inscode)
            try:
                if res < firstatoms[chainkey][0]:  # First element is res-tuple
                    firstatoms[chainkey] = (res, atom.index)
            except KeyError:
                firstatoms[chainkey] = (res, atom.index)
            try:
                if res > lastatoms[chainkey][0]:  # First element is res-tuple
                    lastatoms[chainkey] = (res, atom.index)
            except KeyError:
                lastatoms[chainkey] = (res, atom.index)
        # Two loops to collect all the atom indices for labeling
        labelatoms = set()
        for val in firstatoms.values():
            labelatoms.add(str(val[1]))  # Second element is atom index
        for val in lastatoms.values():
            labelatoms.add(str(val[1]))  # Second element is atom index
        self.labelResidues('atom.n ' + ' '.join(labelatoms), 'chain=TRUE') 
[docs]    def hideLabels(self, asl):
        """
        Hides the labels on the specified atoms. The difference between
        hideLabels() and clearLabels() is that labels that have been hidden can
        be unhidden. Any time a hidden label is replaced or updated, the new
        label is visible, as in PyMol. Hidden labels can also be cleared;
        they do not then reappear on showLabels().
        :type asl: str
        :param asl: ASL string specifying atoms to hide labels on
        """
        workspace = maestro.workspace_get()
        atoms = analyze.evaluate_asl(workspace, asl)
        for atom in atoms:
            fmt = workspace.atom[atom].label_format
            # Save label_format for each atom
            if not fmt == '%HIDDEN':  # Don't double-hide
                workspace.atom[atom].property[SAVE_FMT_PROP] = fmt
            # Maestro ignores meaningless label_format '%HIDDEN'. This is non-
            # standard behavior which is not guaranteed to work
            workspace.atom[atom].label_format = '%HIDDEN'
        maestro.command('labelclear ' + asl)
        maestro.workspace_set(workspace)  # Must come after labelclear 
[docs]    def showLabels(self, asl):
        """
        Unhides previously hidden labels on the specified atoms.
        :type asl: str
        :param asl: ASL string specifying atoms to unhide labels on
        """
        workspace = maestro.workspace_get()
        atoms = analyze.evaluate_asl(workspace, asl)
        for atom in atoms:
            try:
                # Recall saved label_format
                fmt = workspace.atom[atom].property[SAVE_FMT_PROP]
            except KeyError:  # Unhiding a label that has never been hidden
                pass
            # Only untouched hidden labels will retain the '%HIDDEN' format
            if workspace.atom[atom].label_format == '%HIDDEN':
                workspace.atom[atom].label_format = fmt
        maestro.workspace_set(workspace)