import contextlib
import itertools
import logging
from typing import Optional
import weakref
import schrodinger
import schrodinger.utils.log as log
from schrodinger.application.msv.gui import gui_models
from schrodinger.application.msv.gui.gui_alignment import \
    GuiCombinedChainProteinAlignment
from schrodinger.application.msv.gui.homology_modeling import constants
from schrodinger.application.msv.gui.homology_modeling import hm_models
from schrodinger.application.msv.gui.homology_modeling import \
    homology_modeling_ui
from schrodinger.application.msv.gui.homology_modeling import steps
from schrodinger.application.msv.gui.homology_modeling.constants import Mode
from schrodinger.application.msv.gui.picking import PickMode
from schrodinger.application.ska import pairwise_align_ct
from schrodinger.Qt import QtCore
from schrodinger.Qt import QtGui
from schrodinger.Qt import QtWidgets
from schrodinger.tasks import tasks
from schrodinger.ui.qt import mapperwidgets
from schrodinger.ui.qt import widgetmixins
from schrodinger.ui.qt.utils import wrap_qt_tag
logger = log.get_output_logger("msv2_homology_panel")
if schrodinger.in_dev_env():
    logger.setLevel(logging.DEBUG)
maestro = schrodinger.get_maestro()
dockwidget_stylesheet = """
QDockWidget::title {
    color: #000;
    background: #eee;
    text-align: center;
}
"""
top_stylesheet = """
QWidget#homology_modeling {
    background-color: white;
    color: black;
}
"""
[docs]class HomologyPanel(widgetmixins.TaskPanelMixin,
                    widgetmixins.DockableMixinCollection,
                    QtWidgets.QDockWidget):
    """
    :ivar copySeqsRequested: Signal emitted to request copying sequences to new
        tabs for heteromultimer homology modeling.
    :ivar importSeqRequested: Signal emitted to request importing sequence(s).
        Emitted with whether the first new sequence should become the ref seq.
    :ivar importTemplateRequested: Signal emitted to request importing template(s).
    :ivar showBlastResultsRequested: Signal emitted to request showing BLAST
        results.
    :ivar taskStarted: Signal emitted with a task that was just started.
    """
    copySeqsRequested = QtCore.pyqtSignal()
    importSeqRequested = QtCore.pyqtSignal(bool)
    importTemplateRequested = QtCore.pyqtSignal(bool)
    showBlastResultsRequested = QtCore.pyqtSignal()
    taskStarted = QtCore.pyqtSignal(tasks.AbstractTask)
    model_class = gui_models.MsvGuiModel
    ui_module = homology_modeling_ui
    PRESETS_FEATURE_FLAG = False
    PANEL_TASKS = (model_class._hm_launcher_task,)
[docs]    def __init__(self, *args, msv_tab_widget, **kwargs):
        self._msv_tab_widget = msv_tab_widget
        super().__init__(*args, **kwargs) 
[docs]    def initSetOptions(self):
        super().initSetOptions()
        self.help_topic = "MSV_BUILD_HOMOLOGY_MODEL"
        self._modified = True
        self._msv_widget = None
        self._dummy_model = self.model_class()
        self._dummy_model.current_page.split_aln = None
        self._homology_job_map = weakref.WeakValueDictionary()
        self.setAllowedAreas(QtCore.Qt.RightDockWidgetArea)
        self.setWindowTitle("Build Homology Model") 
[docs]    def initSetUp(self):
        super().initSetUp()
        for enum_item in Mode:
            text = constants.MODE_TEXTS[enum_item]
            self.ui.mode_combo.addItem(text, enum_item)
        for enum_item, tooltip in constants.MODE_TOOLTIPS:
            index = self.ui.mode_combo.findData(enum_item)
            if index < 0:
                continue
            tooltip = wrap_qt_tag(tooltip)
            self.ui.mode_combo.setItemData(index, tooltip,
                                           QtCore.Qt.ToolTipRole)
        sep_index = self.ui.mode_combo.findData(Mode.MANY_ONE)
        self.ui.mode_combo.insertSeparator(sep_index)
        self.steps_stacked_widget = mapperwidgets.EnumStackedWidget()
        for mode in Mode:
            workflow_cls = steps.MODE_WORKFLOWS[mode]
            widget = workflow_cls()
            widget.importSeqAsRefRequested.connect(self._importSeqChangeRef)
            widget.findHomologsRequested.connect(self._findHomologs)
            widget.importHomologsRequested.connect(self.importTemplateRequested)
            widget.alignSeqsRequested.connect(self._alignSeqs)
            widget.removeLigandConstraintsRequested.connect(
                self._removeLigandConstraints)
            widget.removeResidueConstraintsRequested.connect(
                self._removeResidueConstraints)
            widget.ligandDialogClosed.connect(self._stopLigandPicking)
            if mode is Mode.MANY_ONE:
                widget.setAsReferenceRequested.connect(
                    self._setTemplateAsReference)
                widget.importSeqRequested.connect(self._importSeq)
            elif mode is Mode.HETEROMULTIMER:
                widget.copySeqsRequested.connect(self.copySeqsRequested)
            elif mode is Mode.ONE_ONE:
                widget.optimizeAlignmentRequested.connect(
                    self._optimizeAlignment)
            if mode in (Mode.CHIMERA, Mode.HETEROMULTIMER, Mode.CONSENSUS):
                widget.alignStructsRequested.connect(self._alignStructs)
            setattr(self, mode.name, widget)
            self.steps_stacked_widget.addWidget(widget)
        self.steps_stacked_widget.setEnum(Mode)
        # TaskPanelMixin setup
        self.setStandardBaseName("homology_modeling")
        self.getTaskManager().taskStarted.connect(self._onTaskStarted)
        taskbar = self.getTaskBar()
        self._start_btn = taskbar.start_btn 
[docs]    def initLayOut(self):
        super().initLayOut()
        self.ui.step_layout.addWidget(self.steps_stacked_widget)
        self.setStyleSheet(dockwidget_stylesheet)
        self.widget().setObjectName("homology_modeling")
        self.widget().setStyleSheet(top_stylesheet) 
[docs]    def initFinalize(self):
        super().initFinalize()
        self._setCurrentMSVWidget() 
[docs]    def makeInitialModel(self):
        """
        @overrides: MapperMixin
        We use `None` as our initial model as a performance optimization.
        MsvGui is responsible for setting the model on the panel
        """
        return None 
[docs]    def setModel(self, model):
        super().setModel(model)
        if self.model is not self._dummy_model:
            self._saved_model = self.model 
[docs]    def getSignalsAndSlots(self, model):
        input_ = model.current_page.homology_modeling_input
        return [
            (model.current_pageReplaced, self._setCurrentMSVWidget),
            (input_.target_template_pairsChanged, self._updateModelStatus),
            (input_.readyChanged, self._updateModelStatus),
            (input_.settings.prime_settings.num_output_structChanged, self._updateModelStatus),
            (model.current_page.homology_modeling_inputChanged, self._setModified),
            (model.current_page.options.pick_modeChanged, self._onPickModeChanged),
            (input_.settings.ligand_dlg_model.constrain_ligandChanged, self._updateLigandPicking),
        ]  # yapf: disable 
[docs]    def defineMappings(self):
        M = self.model_class
        input_ = M.current_page.homology_modeling_input
        mappings = [
            (self.ui.mode_combo, input_.mode),
            (self.steps_stacked_widget, input_.mode),
            (self._onModeChanged, input_.mode),
            (self._onModeChanged, input_.heteromultimer_mode),
            (self._updateEnabled, input_.ready),
            (self._updateEnabled, M.heteromultimer_settings.ready),
        ]  # yapf: disable
        for idx in range(self.steps_stacked_widget.count()):
            workflow_wdg = self.steps_stacked_widget.widget(idx)
            if isinstance(workflow_wdg, steps.HeteromultimerWidget):
                # Heteromultimer tab table needs access to M.pages
                param = M
            else:
                param = M.current_page.homology_modeling_input
            mappings.append((workflow_wdg, param))
        return mappings 
[docs]    def showEvent(self, event):
        self._enableModel()
        super().showEvent(event) 
[docs]    def hideEvent(self, event):
        self._disableModel()
        super().hideEvent(event) 
    def _disableModel(self):
        """
        Disconnect the real model from the panel and clear homology status
        """
        # Clear homology markers from all alignments
        for page in self.model.pages:
            aln = page.aln
            aln.resetHomologyCache()
        self._clearChimericPicking()
        self._stopLigandPicking()
        self.setMSVWidget(None)
        self.setModel(self._dummy_model)
        # Disconnect all alignment signals from task input
        for page in self._saved_model.pages:
            page.homology_modeling_input._setAlignment(None)
    def _enableModel(self):
        """
        Set the real model on the panel and update homology status
        """
        self.setModel(self._saved_model)
        self._setCurrentMSVWidget()
        self._showChimericHighlights()
        self.model.current_page.homology_modeling_input.updateHomologyStatus()
        self._updateHeteromultimerPages()
    def _updateModelStatus(self):
        input_ = self.model.current_page.homology_modeling_input
        if (input_.mode is Mode.MANY_ONE and input_.ready):
            num_queries = len(input_.target_template_pairs)
            num_models = input_.settings.prime_settings.num_output_struct
            text = f"{num_queries * num_models} models\nwill be created"
            self.status_bar.showMessage(text, color=QtGui.QColor("#c7a049"))
        else:
            self.status_bar.clearMessage()
    def _onModeChanged(self):
        mode = self.model.current_page.homology_modeling_input.mode
        text = "Generate Models" if mode is Mode.MANY_ONE else "Generate Model"
        self._start_btn.setText(text)
        self._updateChimericHighlights()
        if not self.model.current_page.homology_modeling_input.usesLigands():
            self._stopLigandPicking()
        self._updateHeteromultimerPages()
    def _updateHeteromultimerPages(self):
        if self.mode is Mode.HETEROMULTIMER:
            het_widget = getattr(self, Mode.HETEROMULTIMER.name)
            het_widget.updateSelectedPages()
    def _updateChimericHighlights(self):
        """
        Show or hide chimeric highlights based on the homology mode
        """
        if self.model.current_page.homology_modeling_input.isChimera():
            self._showChimericHighlights()
        else:
            self._clearChimericPicking()
    def _clearChimericPicking(self):
        """
        Disable chimeric picking and hide chimeric region display
        """
        for idx, page in enumerate(self.model.pages):
            if page.options.pick_mode is PickMode.HMChimera:
                page.options.pick_mode = None
            widget = self._msv_tab_widget.widget(idx)
            widget.view.setChimeraShown(False)
    def _showChimericHighlights(self):
        """
        Show chimeric region display if the mode is chimeric
        """
        if not self.model.current_page.homology_modeling_input.isChimera():
            return
        for idx, page in enumerate(self.model.pages):
            widget = self._msv_tab_widget.widget(idx)
            widget.view.setChimeraShown(True)
    def _stopLigandPicking(self):
        for page in self.model.pages:
            if page.options.pick_mode is PickMode.HMBindingSite:
                page.options.pick_mode = None
    def _onPickModeChanged(self):
        """
        Hide chimeric highlights if picking is off and no regions are specified
        """
        if self._msv_widget is None:
            return
        page = self._msv_widget.model
        input_ = page.homology_modeling_input
        if (page.options.pick_mode is not PickMode.HMChimera and
                not input_.composite_regions):
            self._msv_widget.view.setChimeraShown(False)
    def _updateLigandPicking(self, do_constrain):
        """
        Show/hide ligand constraint picks based on whether constraints are on.
        If constraints are off, exit constraint picking mode.
        """
        if not do_constrain:
            self._stopLigandPicking()
        for idx, page in enumerate(self.model.pages):
            widget = self._msv_tab_widget.widget(idx)
            widget.view.setLigandConstraintsShown(do_constrain)
    @property
    def mode(self):
        return self.model.current_page.homology_modeling_input.mode
    @property
    def workflow(self):
        return self.steps_stacked_widget.currentWidget()
    def _setCurrentMSVWidget(self):
        if self._msv_widget is not None:
            # Heteromultimer mode uses multiple tabs, so users are likely
            # to want another heteromultimer tab when they switch tabs
            prev_mode = self._msv_widget.model.homology_modeling_input.mode
            if prev_mode is Mode.HETEROMULTIMER:
                self.model.current_page.homology_modeling_input.mode = prev_mode
        self.setMSVWidget(self._msv_tab_widget.currentWidget())
    def _getMSVWidgetByAlignment(self, aln):
        for idx, page in enumerate(self.model.pages):
            if page.aln == aln:
                msv_widget = self._msv_tab_widget.widget(idx)
                return msv_widget
    def _setAlignmentOnTask(self, aln):
        if self.model is None:
            return
        if isinstance(aln, GuiCombinedChainProteinAlignment):
            response = self.question(
                text="Homology modeling is not yet supported in combined chain "
                "mode. Press OK to return to split chain mode or Cancel to "
                "close the homology modeling panel.")
            if response:
                self.model.current_page.split_chain_view = True
            else:
                self.close()
            return
        self.model.current_page.homology_modeling_input._setAlignment(aln)
    def _onTaskStarted(self, launcher_task):
        task = launcher_task.subtask
        task.job_idChanged.connect(self._onTaskJobIdChanged)
        self.taskStarted.emit(task)
    def _onTaskJobIdChanged(self, job_id):
        # The Job ID isn't immediately available so we have to wait until the
        # Job ID is set to store the associated page
        task = self.sender()
        msv_widget = self._getMSVWidgetByAlignment(task.aln)
        if msv_widget:
            self._homology_job_map[job_id] = msv_widget.model
[docs]    def getHomologyJobPage(self, job_id: str) -> Optional[gui_models.PageModel]:
        """
        Get the page associated with the given job ID, if any.
        """
        return self._homology_job_map.get(job_id) 
    def _onTaskStatusChanged(self, status):
        """
        Report the status of an individual task
        @overrides: TaskPanelMixin
        """
        super()._onTaskStatusChanged()
        launcher_task = self.sender()
        task = launcher_task.subtask
        self._reportStatus(task, status)
        if not maestro and status is task.DONE:
            msv_widget = self._getMSVWidgetByAlignment(task.aln)
            if msv_widget is None:
                curr_aln = self.model.current_page.aln
                msv_widget = self._getMSVWidgetByAlignment(curr_aln)
            seqs = msv_widget.loadPdbs(task.getOutputStructureFiles())
            if seqs:
                aln = msv_widget.getAlignment()
                aln.seq_selection_model.setSelectionState(seqs, False)
    @QtCore.pyqtSlot()
    def _setModified(self, *, modified=True):
        self._modified = modified
        self._delayedUpdateEnabled()
    def _delayedUpdateEnabled(self):
        # Enable doesn't always paint without timer
        QtCore.QTimer.singleShot(0, self._updateEnabled)
    def _updateEnabled(self):
        """
        Update widgets based on whether the task is ready and none of taskman's
        tasks are running
        """
        if self._msv_widget is None:
            return
        hm_input = self.model.current_page.homology_modeling_input
        is_startable = not self._msv_widget.isWorkspace()
        if hm_input.mode is Mode.HETEROMULTIMER:
            ready = self.model.heteromultimer_settings.ready
        else:
            ready = hm_input.ready
        enable_run = is_startable and ready and self._modified
        self.steps_stacked_widget.setEnabled(is_startable)
        self._start_btn.setEnabled(enable_run)
        if not is_startable:
            tooltip = (
                "Homology modeling is not available on the Workspace tab.\n"
                "Copy sequences to a new tab or just switch tabs to proceed.")
        elif not ready:
            tooltip = "Required Homology Modeling steps are not complete"
        elif not self._modified:
            tooltip = ("Homology modeling was already started.\n"
                       "Change settings to generate another model.")
        else:
            tooltip = ""
        self._start_btn.setToolTip(tooltip)
[docs]    def defineTaskPreprocessors(self, model):
        # Note: this sets the preprocessors on the dummy task; the
        # preprocessors are manually set on the real task in `getTask`
        return [
            (model._hm_launcher_task, self._checkMode),
            (model._hm_launcher_task, self._validate),
            (model._hm_launcher_task, self._initSubtask),
        ] 
    @tasks.preprocessor(order=tasks.BEFORE_TASKDIR - 2)
    def _checkMode(self):
        if not maestro and self.mode is Mode.HETEROMULTIMER:
            # TODO MSV-3239
            msg = ("Heteromultimer homology modeling does not work "
                   "standalone, please open MSV in Maestro.")
            return False, msg
    @tasks.preprocessor(order=tasks.BEFORE_TASKDIR - 1)
    def _validate(self):
        """
        Check that homology modeling can be run.
        """
        response = self._checkIfAligned()
        if not response:  # Canceled
            # Explicitly return False; None will allow task to start
            return False
        min_quality = self.model.current_page.homology_modeling_input.getMinAlignmentQuality(
        )
        if min_quality in (constants.AlignmentQuality.WEAK,
                           constants.AlignmentQuality.LOW):
            # TODO change the dialog to have the info icon - self.question is
            # the only dialog that returns the response (can't use self.info).
            # In PANEL-12506 we may expand the API to allow specifying the icon.
            response = self.question(wrap_qt_tag(constants.LOW_PROCEED_TEXT),
                                     "Low-Identity Template",
                                     save_response_key="msv2_homology_low")
        return bool(response)
    @tasks.preprocessor(order=tasks.BEFORE_TASKDIR)
    def _initSubtask(self):
        subtask = hm_models.get_task_from_model(self.model)
        subtask.aln = self._msv_widget.getAlignment()
        task = self.getTask()
        task.subtask = subtask
    def _checkIfAligned(self):
        """
        Guess whether the sequences have been aligned and prompt user
        """
        input_ = self.model.current_page.homology_modeling_input
        min_quality = input_.getMinAlignmentQuality()
        is_low_and_ungapped = (min_quality is constants.AlignmentQuality.LOW and
                               not input_.hasGaps())
        is_weak = min_quality is constants.AlignmentQuality.WEAK
        if is_weak or is_low_and_ungapped:
            if is_weak:
                text = constants.WEAK_REALIGN_TEXT
                response_key = "msv2_homology_weak"
            else:
                text = constants.LOW_REALIGN_TEXT
                response_key = "msv2_homology_low_realign"
            response = self.question(wrap_qt_tag(text),
                                     "Sequences Are Not Aligned",
                                     save_response_key=response_key,
                                     yes_text="Align")
            if response is True:
                self._alignSeqs()
                # Allow timer to run to update quality
                QtWidgets.QApplication.instance().processEvents()
            return response
        return True
    def _setTemplateAsReference(self):
        """
        For many-one (batch) homology modeling, set the appropriate structured
        sequence as the reference
        """
        widget = self._msv_widget
        if widget is None:
            raise ValueError("Can't set reference seq without msv_widget")
        if self.mode is not Mode.MANY_ONE:
            raise ValueError("The template can only be set as reference in "
                             "batch homology modeling")
        aln = widget.getAlignment()
        new_ref = next(hm_models.get_possible_templates(aln), None)
        if new_ref is None:
            any_structured = any(
                seq.hasStructure() for seq in itertools.islice(aln, 1, None))
            any_selected = aln.seq_selection_model.hasSelection()
            if any_structured and any_selected:
                msg_text = ("None of the selected sequences have structure. "
                            "Select a structured sequence to set as Reference.")
            else:
                msg_text = "None of the sequences have structure."
            self.warning(msg_text)
            return
        aln.setReferenceSeq(new_ref)
[docs]    @contextlib.contextmanager
    def updateNewSeqSelection(self):
        """
        Update sequence selection for newly added sequences based on original
        selection state.
        If there were no non-reference sequences selected, the new sequences
        should not be selected. If there were non-reference sequences selected,
        both the new and original sequences should be selected.
        """
        widget = self._msv_widget
        if widget is None:
            yield
        else:
            aln = widget.getAlignment()
            sel_model = aln.seq_selection_model
            orig_seqs = set(aln)
            orig_sel = set(sel_model.getSelection())
            select_new = bool(orig_sel - {aln.getReferenceSeq()})
            yield
            if select_new:
                new_seqs = {seq for seq in aln if seq not in orig_seqs}
                new_sel = orig_sel | new_seqs
            else:
                new_sel = orig_sel
            sel_model.clearSelection()
            sel_model.setSelectionState(new_sel, True) 
    def _findHomologs(self):
        widget = self._msv_widget
        if widget is None:
            raise ValueError("Can't find homologs without msv_widget")
        blast_settings = widget.model.blast_task.input.settings
        og_structures = blast_settings.download_structures
        og_align = blast_settings.align_after_download
        og_multiple = blast_settings.allow_multiple_chains
        blast_settings.download_structures = True
        blast_settings.align_after_download = True
        if self.mode is Mode.HOMOMULTIMER:
            blast_settings.allow_multiple_chains = True
        try:
            if widget.hasBlastResults(for_ref_seq=True):
                self.showBlastResultsRequested.emit()
            else:
                widget.openBlastSearchDialog()
        finally:
            blast_settings.download_structures = og_structures
            blast_settings.align_after_download = og_align
            blast_settings.allow_multiple_chains = og_multiple
    def _importSeqChangeRef(self):
        change_ref = True
        self.importSeqRequested.emit(change_ref)
    def _importSeq(self):
        with self.updateNewSeqSelection():
            change_ref = False
            self.importSeqRequested.emit(change_ref)
    def _alignSeqs(self):
        """
        Align sequences for homology modeling
        """
        widget = self._msv_widget
        if widget is None:
            raise ValueError("Can't align seqs without msv_widget")
        input_ = self.model.current_page.homology_modeling_input
        seqs_to_align = input_.getSeqsToAlign()
        seq_sel_model = widget.getAlignment().seq_selection_model
        with seq_sel_model.suspendSelection():
            settings = widget.model.options.align_settings
            orig_value = settings.align_only_selected_seqs
            try:
                settings.align_only_selected_seqs = True
                seq_sel_model.setSelectionState(seqs_to_align, True)
                widget.multipleAlignment()
            finally:
                settings.align_only_selected_seqs = orig_value
    def _alignStructs(self):
        """
        Align structures for homology modeling
        """
        widget = self._msv_widget
        if widget is None:
            raise ValueError("Can't align seqs without msv_widget")
        input_ = self.model.current_page.homology_modeling_input
        seqs_to_disassociate = input_.getSeqsToStructureAlign()
        widget._splitAllChains(seqs_to_disassociate, prompt=False)
        # Disassociate creates new sequences, so we have to get the seqs again
        seqs_to_align = input_.getSeqsToStructureAlign()
        ref_st, *other_sts = (seq.getStructure() for seq in seqs_to_align)
        query = (steps.get_seq_display_name(seqs_to_align[0]), ref_st)
        templist = [(f"seq{idx + 1}", st) for idx, st in enumerate(other_sts)]
        pairwise_align_ct(query, templist, save_props=True)
        for seq, st in zip(seqs_to_align[1:], other_sts):
            seq.setStructure(st)
        # Force update of RMSD display
        pairs = input_.target_template_pairs
        input_.target_template_pairsChanged.emit(pairs)
    def _optimizeAlignment(self, ligand_asl, residue_asl, radius):
        widget = self._msv_widget
        if widget is None:
            raise ValueError("Can't align seqs without msv_widget")
        input_ = self.model.current_page.homology_modeling_input
        if not input_.target_template_pairs:
            return
        pair = input_.target_template_pairs[0]
        if not pair.is_valid:
            return
        widget.optimizeAlignment(pair.target_seq,
                                 pair.template_seq,
                                 ligand_asl=ligand_asl,
                                 residue_asl=residue_asl,
                                 radius=radius)
    def _removeLigandConstraints(self):
        widget = self._msv_widget
        if widget is None:
            raise ValueError("Can't set reference seq without msv_widget")
        widget.resetPick(PickMode.HMBindingSite)
    def _removeResidueConstraints(self):
        widget = self._msv_widget
        if widget is None:
            raise ValueError("Can't remove res constraints without msv_widget")
        widget.resetPick(PickMode.HMProximity)
    def _reportStatus(self, task, status):
        logger.debug(f"Task is now {status.name}")
        if status is task.FAILED:
            logger.debug(task.failure_info)
    def _runSlot(self):
        """
        @overrides: TaskPanelMixin
        """
        self.model.current_page.options.pick_mode = None
        super()._runSlot()
        self._setModified(modified=False)