import schrodinger
from schrodinger import project
from schrodinger.application.jaguar import input as jaginput
from schrodinger.infra import mm
from schrodinger.ui.qt import entryselector
from .base_tab import BaseTab
from .base_tab import ProvidesBasisMixin
from .base_tab import ProvidesStructuresMixin
maestro = schrodinger.get_maestro()
[docs]class MultiStructureTab(ProvidesBasisMixin, ProvidesStructuresMixin, BaseTab):
    """
    A parent class for tabs that allow transition state, reactant, and product
    structures to be selected.  MultiStructureTabs also contain a Molecule
    subtab.  Note that this class is not intended to be directly instantiated
    and instead should be subclassed (such as the IRC tab or Transition State
    tabs).
    """
    ALL_STRUCS = list(range(3))
    TRANSITION_STATE, REACTANT, PRODUCT = ALL_STRUCS
    ENTRY_TYPE_NAMES = ("transition state", "reactant", "product")
    STRUC_DIFF_WARNING = ("Warning:  The %s and %s\n"
                          "entries contain different %s.  This must\n"
                          "be resolved before running the job.")
    ATOMS = "atoms"
    ATOM_LABELS = "atom labels"
[docs]    def setup(self):
        self.ui.transition_state_btn.clicked.connect(
            lambda: self.chooseEntry(self.TRANSITION_STATE))
        self.ui.reactant_btn.clicked.connect(
            lambda: self.chooseEntry(self.REACTANT))
        self.ui.product_btn.clicked.connect(
            lambda: self.chooseEntry(self.PRODUCT))
        self.ui.transition_state_cb.toggled.connect(
            lambda: self.structureCbToggled(self.TRANSITION_STATE))
        self.ui.reactant_cb.toggled.connect(
            lambda: self.structureCbToggled(self.REACTANT))
        self.ui.product_cb.toggled.connect(
            lambda: self.structureCbToggled(self.PRODUCT))
        self.struc_les = [
            self.ui.transition_state_le, self.ui.reactant_le, self.ui.product_le
        ]
        self.struc_cbs = [
            self.ui.transition_state_cb, self.ui.reactant_cb, self.ui.product_cb
        ]
        self.basis_changed = self.ui.molecule_sub_tab.basis_changed
        self.reset() 
[docs]    def reset(self):
        """
        Clear the stored structures
        """
        self._entry_ids = [None] * 3
        self._updateStrucListings()
        self.ui.molecule_sub_tab.reset() 
[docs]    def setStructureSelectorsEnabled(self, transition_state, reactant, product):
        """
        Enable or disable the three structure selection rows
        """
        self.ui.transition_state_cb.setEnabled(
            transition_state and
            self._entry_ids[self.TRANSITION_STATE] is not None)
        self.ui.transition_state_lbl.setEnabled(transition_state)
        self.ui.transition_state_le.setEnabled(transition_state)
        self.ui.transition_state_btn.setEnabled(transition_state)
        self.ui.reactant_cb.setEnabled(
            reactant and self._entry_ids[self.REACTANT] is not None)
        self.ui.reactant_lbl.setEnabled(reactant)
        self.ui.reactant_le.setEnabled(reactant)
        self.ui.reactant_btn.setEnabled(reactant)
        self.ui.product_cb.setEnabled(product and
                                      self._entry_ids[self.PRODUCT] is not None)
        self.ui.product_lbl.setEnabled(product)
        self.ui.product_le.setEnabled(product)
        self.ui.product_btn.setEnabled(product)
        self._updateStructureWarningLabel() 
[docs]    def chooseEntry(self, struc_type):
        """
        Open the EntrySelector dialog for the specified structure and save the
        selected entry.
        :param struc_type: The structure to select.  Must  be one of
            self.TRANSITION_STATE, self.REACTANT, or self.PRODUCT.
        :type struc_type: int
        """
        entry = entryselector.get_entry(self)
        if entry is not None:
            self._entry_ids[struc_type] = entry
            self._updateStrucListings()
            self.structureChanged.emit() 
    def _updateStrucListings(self):
        """
        Update the contents and status of all structure selection widgets based
        on the contents of self._entry_ids and the structures currently loaded
        into the project table.
        """
        proj = maestro.project_table_get()
        for struc_type in self.ALL_STRUCS:
            eid = self._entry_ids[struc_type]
            line_edit = self.struc_les[struc_type]
            row = self._getRow(proj, eid)
            if row is None:
                self._entry_ids[struc_type] = None
                line_edit.setText("")
                self.struc_cbs[struc_type].setEnabled(False)
                self.struc_cbs[struc_type].setChecked(False)
            else:
                title = row.title
                line_edit.setText("%s:%s" % (eid, title))
                in_workspace = (row.in_workspace != project.NOT_IN_WORKSPACE)
                enabled = self.struc_les[struc_type].isEnabled()
                self.struc_cbs[struc_type].setEnabled(enabled)
                self.struc_cbs[struc_type].setChecked(in_workspace)
        self._updateStructureWarningLabel()
        self._updateMoleculeSubTabStruc()
    def _getRow(self, proj, eid):
        """
        Retrieve the specified row from the project table.  If there is no such
        row, return None.
        :param proj: The maestro project
        :type proj: `schrodinger.project.Project`
        :param eid: The entry ID to retrieve
        :type eid: str
        :return: The requested row, or None if there is no such row.
        :rtype: `schrodinger.project.ProjectRow` or NoneType
        """
        if eid is None:
            return None
        try:
            return proj.getRow(eid)
        except (ValueError, mm.MmException):
            return None
    def _updateMoleculeSubTabStruc(self):
        """
        Update the Molecule sub-tab structure.  This structure is used for basis
        set calculations.
        """
        representative_struc = self.getRepresentativeStructure()
        self.ui.molecule_sub_tab.setStructure(representative_struc)
[docs]    def projectUpdated(self):
        """
        Update the listing of structures when the project is updated.  Note that
        the callback for this function is registered in the
        base_panel.MultiStructureMixin class so that it can be unregistered
        when the panel is closed.
        """
        try:
            self._updateStrucListings()
        except project.ProjectException:
            pass 
    def _updateStructureWarningLabel(self):
        """
        Update the label that warns about mismatched structures
        """
        diff = self._compareStrucs()
        if diff is None:
            text = ""
        else:
            text = self.STRUC_DIFF_WARNING % diff
        self.ui.struc_warning_lbl.setText(text)
[docs]    def validate(self):
        """
        Make sure that all of the appropriate structures are loaded and that
        they all have the same atoms.
        """
        for entry_type in self.ALL_STRUCS:
            if (self.struc_les[entry_type].isEnabled() and
                    self._entry_ids[entry_type] is None):
                return ("No structure loaded for %s." %
                        self.ENTRY_TYPE_NAMES[entry_type])
        diff = self._compareStrucs()
        if diff is not None:
            return "The %s and %s entries contain different %s." % diff
        basis_selector_err = self.ui.molecule_sub_tab.validate()
        if basis_selector_err:
            return basis_selector_err 
[docs]    def structureCbToggled(self, struc_type):
        """
        Respond to the specified structure inclusion checkbox being toggled by
        including or excluding the relevant structure from the workspace.
        """
        eid = self._entry_ids[struc_type]
        if eid is None:
            return
        if self.struc_cbs[struc_type].isChecked():
            include = project.IN_WORKSPACE
        else:
            include = project.NOT_IN_WORKSPACE
        proj = maestro.project_table_get()
        proj.getRow(eid).in_workspace = include 
    def _compareStrucs(self):
        """
        Compare all structures loaded into this tab to make sure that they have
        the same atoms.  Two structures are the same if they:
          - Have the same number of atoms
          - Have atoms with the same elements in the same order
          - Have atoms with the same names after Jaguar atom naming is applied
        :return: None if all structures are the same.  Otherwise, returns a
            tuple of:
              - The name of the first structure that's different
              - The name of the second structure that's different
              - What's different about the structures.  This will be "atoms" if
                there are a different number of atoms or atoms with different
                elements, and it will be "atom labels" if there are atoms with
                different names after Jaguar atom naming is applied.
        :rtype: NoneType or tuple
        """
        entry_types, elems, atom_names = self._getAtomicNumbersAndAtomNames()
        return self._compareAtomicNumbersAndAtomNames(entry_types, elems,
                                                      atom_names)
    def _getAtomicNumbersAndAtomNames(self):
        """
        Retrieve the atomic numbers and atom names for all structures that are
        relevant to the current job type (i.e. for all rows that are populated
        and enabled)
        :return: A tuple of:
              - A list of entry types for all structures are relevant
              - A list of [atomic numbers for the first relevant structure,
                atomic numbers for the second relevant structure, ...]
              - A list of [atom names for the first relevant structure,
                atom names for the second relevant structure, ...]
        :rtype: tuple
        """
        entry_types = []
        elems = []
        atom_names = []
        proj = maestro.project_table_get()
        for entry_type in self.ALL_STRUCS:
            struc = self._getStruc(proj, entry_type)
            if struc is None:
                continue
            jaginput.apply_jaguar_atom_naming(struc)
            non_dummy_atoms = [
                atom for atom in struc.atom if atom.atomic_number != -2
            ]
            cur_elems = [atom.atomic_number for atom in non_dummy_atoms]
            cur_atom_names = [atom.name for atom in non_dummy_atoms]
            entry_types.append(entry_type)
            elems.append(cur_elems)
            atom_names.append(cur_atom_names)
        return entry_types, elems, atom_names
    def _compareAtomicNumbersAndAtomNames(self, entry_types, elems, atom_names):
        """
        Compare the provided atomic numbers and atom names and return a
        description of the differences
        :param entry_types: A list of entry types for all structures are
            relevant
        :type entry_types: list
        :param elems: A list of [atomic numbers for the first relevant
            structure, atomic numbers for the second relevant structure, ...]
        :type elems: list
        :param atom_names: A list of [atom names for the first relevant
            structure, atom names for the second relevant structure, ...]
        :type atom_names: list
        :return: None if all structures are the same.  Otherwise, returns a
            tuple of:
              - The name of the first structure that's different
              - The name of the second structure that's different
              - What's different about the structures ("atoms" or "atom labels")
        :rtype: NoneType or tuple
        """
        diff = None
        if len(elems) > 1:
            if elems[0] != elems[1]:
                diff = [0, 1, self.ATOMS]
            elif atom_names[0] != atom_names[1]:
                diff = [0, 1, self.ATOM_LABELS]
        if len(elems) > 2:
            if elems[0] != elems[2]:
                diff = [0, 2, self.ATOMS]
            elif atom_names[0] != atom_names[2]:
                diff = [0, 2, self.ATOM_LABELS]
        if diff is not None:
            diff[0] = self.ENTRY_TYPE_NAMES[entry_types[diff[0]]]
            diff[1] = self.ENTRY_TYPE_NAMES[entry_types[diff[1]]]
            diff = tuple(diff)
        return diff
[docs]    def getStructures(self):
        """
        Get all of the structures that are loaded into the tab.  Each structure
        will be a `schrodinger.structure.Structure` object containing the
        loaded structure, or None if no structure has been loaded.
        :return: A list of:
              - the transition state structure
              - the reactant structure
              - the product structure
        :rtype: list
        """
        proj = maestro.project_table_get()
        strucs = [
            self._getStruc(proj, entry_type) for entry_type in self.ALL_STRUCS
        ]
        return strucs 
    def _getStruc(self, proj, entry_type):
        """
        Retrieve the specified structure
        :param proj: The current project
        :type proj: `schrodinger.project.Project`
        :param struc_type: The structure to retrieve.  Must  be one of
            self.TRANSITION_STATE, self.REACTANT, or self.PRODUCT.
        :type struc_type: int
        """
        eid = self._entry_ids[entry_type]
        if eid is not None and self.struc_les[entry_type].isEnabled():
            cur_struc = proj.getRow(eid).getStructure()
            return cur_struc
        else:
            return None
[docs]    def setStructures(self, entry_ids, jag_input=None):
        """
        Load the specified entry IDs into the tab
        :param entry_ids: A list of:
            - the transition state structure entry ID
            - the reactant structure entry ID
            - the product structure entry ID
            Note that this value must be a list, not a tuple.
        :type entry_ids: list
        :param jag_input: A JaguarInput object containing the job settings.
            Note that this argument is only used for the Transition State tab
            version of this function in order to determine if the search method
            is LST, as this will affect how the structures are loaded.
        :type jag_input: `schrodinger.application.jaguar.input.JaguarInput`
        """
        self._entry_ids = entry_ids
        self._updateStrucListings() 
[docs]    def getRepresentativeStructure(self):
        """
        Retrieve one structure that can be used for basis selector calculations.
        :return: A single representative structure, or None if no structures are
            loaded.
        :rtype: `schrodinger.structure.Structure` or NoneType
        """
        try:
            proj = maestro.project_table_get()
        except project.ProjectException:
            return None
        for entry_type in self.ALL_STRUCS:
            cur_struc = self._getStruc(proj, entry_type)
            if cur_struc is not None:
                return cur_struc 
[docs]    def getEids(self):
        """
        Get the entry ids loaded into the tab
        :return: A tuple of
                - A list of entry ids loaded into the tab
                - Whether the structures have matching atoms and atom names (bool)
        :rtype: tuple
        """
        eids = [eid for eid in self._entry_ids if eid is not None]
        diff = self._compareStrucs()
        acceptable = diff is None
        return (eids, acceptable) 
[docs]    def getStructureTitleForJobname(self):
        # See ProvidesStructuresMixin for method documentation
        struc = self.getRepresentativeStructure()
        if struc is None:
            return None
        else:
            return struc.title 
    # The following functions simply call the equivalent Molecule sub-tab
    # function
[docs]    def getDefaultKeywords(self):
        return self.ui.molecule_sub_tab.getDefaultKeywords() 
[docs]    def getMmJagKeywords(self):
        return self.ui.molecule_sub_tab.getMmJagKeywords() 
[docs]    def getBasis(self, ignored=None):
        return self.ui.molecule_sub_tab.getBasis() 
[docs]    def loadSettings(self, jag_input):
        ret = self.ui.molecule_sub_tab.loadSettings(jag_input)
        self.structureChanged.emit()
        return ret