"""
A class and function that help enumerate possibilities - could be enumerating
over items of a list or enumerating the element at various atom positions in a
structure, for instance.
Copyright Schrodinger, LLC. All rights reserved.
"""
import argparse
import sys
from schrodinger.application.matsci import buildcomplex
from schrodinger.structutils import analyze
from schrodinger.structutils import build
from schrodinger.utils import cmdline
_version = '$Revision: 0.0 $'
[docs]class Alchemist(object):
    """
    Class that mutates the element of an atom in a structure
    """
[docs]    def __init__(self, master, targets, new_items, child=None):
        """
        Create an Alchemist object. Alchemists modify an object by changing a
        target item to new item.  For instance, an Alchemist:
        - Might transmute the element of a particular atom
        - Might change the item of a list to a new value
        For enumeration, a series of Alchemists will work together to perform
        all necessary mutations, each alchemist passing their mutated structures
        on to another (child) alchemist. The general workflow looks like::
            receive mutated_object from parent alchemist
            for target in targets:
                for new_item in new_items:
                    make a copy_of_mutated_object
                    change item at target in copy_of_mutated_object to new_item
                    pass copy_of_mutated_object to child alchemist
        :type master: object
        :param master: The master for this object. Can be a panel or other
            object that has methods that get called by Alchemist methods. By
            default, the Alchemist class calls master.transmutationCompleted with
            each fully transmuted object.
        :type targets: list
        :param targets: list of possible positions for this Alchemist to mutate
        :type new_items: list
        :param new_items: list of items to change each target to.
        :type child: `Alchemist`
        :param child: The Alchemist to call each time this Alchemist transmutes
            an atom.  If child is None, than this Alchemist is the last in the line.
        """
        self.master = master
        self.original_targets = targets
        self.new_items = new_items
        self.child = child 
[docs]    def allTransmutationsCompleted(self, mydata, myused):
        """
        Perform whatever action is required when all transmutations have been
        performed on the mydata object
        :param mydata: The object that is fully transmuted
        :type myused: list
        :param myused: List of indexes that have been transmuted in mydata
        """
        self.master.transmutationCompleted(mydata, myused) 
[docs]    def runTransmutations(self, data, used=None):
        """
        Begin transmutations.
        This Alchemist will determine what targets to transmute based on
        its set of original targets minus those targets that have been used by
        previous Alchemists.  Each time this Alchemist transmutes an item, it
        calls its child Alchemist to do its mutations (which calls its child
        Alchemist each time it transmutes an item, and so on).  Thus is the
        iterative process of transmutating all possible combinations achieved.
        If this Alchemist does not have any children, than it is the last in the
        Alchemist line and it should call allTransmutationsCompleted when it is
        done with a transmutation.
        :type data: object
        :param data: The object to be transmuted
        :type used: list of int
        :param used: The indexes in data of the transmutations performed by
            parent Alchemists.  The last item in this list is used to ensure that
            this Alchemist does not mutate any target already covered by parent
            Alchemists.
        """
        targets = self.original_targets[:]
        if used:
            # Remove any targets that have been already used by ancestors
            try:
                index = targets.index(used[-1])
                targets = targets[index + 1:]
            except ValueError:
                pass
        else:
            # This is the original Alchemist, no targets have been used
            used = []
        # Cycle through all targets, transmuting them one by one
        for index in targets:
            # Cycle through all new items, transmuting to them one by one
            for item in self.new_items:
                # Start with a copy each time so my own trasmutations don't
                # accumulate
                myused = used[:]
                myused.append(index)
                mydata = self.performTransmutation(data, index, item)
                if self.child:
                    # Let my child iterate its transmutations on this structure
                    self.child.runTransmutations(mydata, used=myused)
                else:
                    # This is the last Alchemist in the line, we are done with
                    # this structure
                    self.allTransmutationsCompleted(mydata, myused)  
[docs]class StructureAlchemist(Alchemist):
    """
    An abstract base Alchemist class for Alchemists that transmute Structures -
    subclasses will probably need to re-implement the performTransmutation and
    allTransmutationsCompleted methods.
    """
[docs]    def __init__(self, master, targets, new_items, path, child=None):
        """
        Create an Alchemist object
        :type path: str
        :param path: The path to a temporary file to write structures to
        See parent class for additional documentation
        """
        Alchemist.__init__(self, master, targets, new_items, child=child)
        self.path = path 
[docs]    def runTransmutations(self, data, used=None):
        """
        Begin transmuating atoms.
        This Alchemist will determine what target atoms to transmute based on
        its set of original targets minus those targets that have been used by
        previous Alchemists.  Each time this Alchemist transmute an atom, it
        calls its child Alchemist to do its mutations (which calls its child
        Alchemist each time it transmutes an atom).  Thus is the iterative
        process of transmutating all possible combinations achieved.
        If this Alchemist does not have any children, than it is the last in the
        Alchemist line and it should add a new project entry each time it
        transmutes an atom.
        :type struct: `schrodinger.structure.Structure`
        :param struct: The structure object with the transmuted atoms
        :type used: list of int
        :param used: The atom indexes of the transmuted atoms by parent
            Alchemists.  The last atom in this list is used to ensure that this
            Alchemist does not mutate any target already covered by parent
            Alchemists.
        """
        Alchemist.runTransmutations(self, data, used=used)
        if not self.child:
            self.master.showProgress()  
[docs]class ElementAlchemist(StructureAlchemist):
    """
    An alchemist class that transmutes elements in a Structure
    :note: The new_items argument to the constructor for this class should have
        the following format::
            type new_items: list of tuple
            param new_items: Each item of the list is an (element, color) tuple,
            where element is the atomic symbol of the element to mutate to, and
            color is the new color for that atom in a form accepted by the
            _StructAtom.color property
    """
[docs]    def fixHydrogens(self, struct, used):
        """
        Correct the number of hydrogens bonded to each transmuted atom
        :type struct: `schrodinger.structure.Structure`
        :param struct: The structure object with the transmuted atoms
        :type used: list of int
        :param used: The atom indexes of the transmuted atoms
        """
        delatoms = []
        for index in used:
            atomobj = struct.atom[index]
            for neighbor in atomobj.bonded_atoms:
                if neighbor.element == 'H':
                    delatoms.append(neighbor.index)
        atom_map = struct.deleteAtoms(delatoms, renumber_map=True)
        used_atom_map = [atom_map[x] for x in used]
        build.add_hydrogens(struct, atom_list=used_atom_map) 
[docs]    def allTransmutationsCompleted(self, struct, used):
        """
        All transmutations have been performed on struct, write it out to a
        file if we are keeping this result
        :type struct: `schrodinger.structure.Structure`
        :param struct: The structure object with the transmuted atoms
        :type used: list of int
        :param used: The atom indexes of the transmuted atoms
        """
        element, color = self.new_items[0]
        title = '_'.join([struct.title, element] + [str(x) for x in used])
        self.fixHydrogens(struct, used)
        if self.master.transmutedStructureCompleted(struct):
            struct.retype()
            struct.title = title
            struct.append(self.path)  
[docs]class VacancyAlchemist(StructureAlchemist):
    """
    An alchemist class that deletes atoms in a Structure and the hydrogens they
    are bound to
    :note: The new_items argument to the constructor for this class should have
        the following format::
            type new_items: list
            param new_items: Should be [None]
    """
    DELETE_PROPERTY = 'b_matsci_impending_vacancy'
[docs]    def deleteAtoms(self, struct):
        """
        Delete all atoms marked for deletion
        :type struct: `schrodinger.structure.Structure`
        :param struct: The structure object with the transmuted atoms
        """
        delatoms = analyze.evaluate_asl(struct,
                                        'atom.%s' % self.DELETE_PROPERTY)
        struct.deleteAtoms(delatoms) 
[docs]    def allTransmutationsCompleted(self, struct, used):
        """
        All transmutations have been performed on struct, write it out to a
        file if we are keeping this result
        :type struct: `schrodinger.structure.Structure`
        :param struct: The structure object with the transmuted atoms
        :type used: list of int
        :param used: The atom indexes of the transmuted atoms
        """
        title = '_'.join([struct.title, 'x'] + [str(x) for x in used])
        self.deleteAtoms(struct)
        if self.master.transmutedStructureCompleted(struct):
            struct.retype()
            struct.title = title
            struct.append(self.path)  
description = __doc__
[docs]def main(*args):
    parser = argparse.ArgumentParser(description=description, add_help=False)
    parser.add_argument('-h',
                        '-help',
                        action='help',
                        default=argparse.SUPPRESS,
                        help='Show this help message and exit.')
    parser.add_argument('-v',
                        '-version',
                        action='version',
                        default=argparse.SUPPRESS,
                        version=_version,
                        help="Show the program's version number and exit.")
    options = parser.parse_args(args) 
# For running outside of Maestro:
if __name__ == '__main__':
    cmdline.main_wrapper(main, *sys.argv[1:])