import copy
import enum
import re
from collections import namedtuple
from functools import partial
from typing import List
from typing import Set
import inflect
import itertools
from schrodinger.application.msv import command
from schrodinger.application.msv.gui import color
from schrodinger.application.msv.gui import gui_alignment
from schrodinger.application.msv.gui import viewconstants
from schrodinger.application.msv.gui.homology_modeling import hm_models
from schrodinger.application.msv.gui.picking import PickMode
from schrodinger.infra import util
from schrodinger.models import diffy
from schrodinger.models import json
from schrodinger.models import mappers
from schrodinger.models import parameters
from schrodinger.protein import annotation
from schrodinger.protein.properties import SequenceProperty
from schrodinger.protein.tasks import blast
from schrodinger.Qt import QtCore
from schrodinger.ui.qt.appframework2 import settings
from schrodinger.utils import fileutils
from schrodinger.utils import scollections
RES_PROP_ANNOS = annotation.ProteinSequenceAnnotations.RES_PROPENSITY_ANNOTATIONS
ALN_ANNO_TYPES = annotation.ProteinAlignmentAnnotations.ANNOTATION_TYPES
SEQ_ANNO_TYPES = annotation.ProteinSequenceAnnotations.ANNOTATION_TYPES
[docs]class PairwiseAlignSettingsModel(parameters.CompoundParam):
    set_constraints: bool = False
    lock_gaps: bool = False
    superimpose_after: bool = False
    sub_matrix: str = "BLOSUM62"
    gap_open_penalty: float = 10.0
    gap_extend_penalty: float = 1.0
    prevent_ss_gaps: bool = False
    penalize_end_gaps: bool = False
[docs]    @json.adapter(version=48002)
    def adapter48002(cls, json_dict):
        # These were moved to PairwiseSSAlignSettingsModel
        json_dict.pop('use_ss_prediction')
        json_dict.pop('use_gpcr_aln')
        return json_dict  
[docs]class PairwiseSSAlignSettingsModel(parameters.CompoundParam):
    set_constraints: bool = False
    use_gpcr_aln: bool = False 
[docs]class MultipleAlignSettingsModel(parameters.CompoundParam):
    find_globally_conserved: bool = False
    superimpose_after: bool = False
    aln_algorithm: viewconstants.MultAlnAlgorithm = viewconstants.MultAlnAlgorithm.Muscle
    gap_open_penalty: float = 10.0
    gap_extend_penalty: float = 0.2 
[docs]class ResidueNumberAlignSettingsModel(parameters.CompoundParam):
    superimpose_after: bool = False 
[docs]class ChainName(parameters.CompoundParam):
    seq_chain: tuple
    reference = parameters.StringParam() 
[docs]class ProteinStructureAlignSettingsModel(parameters.CompoundParam):
    force: bool = False
    align_seqs: bool = False
    seq_represents: viewconstants.StructAlnSequenceRepresents = \
        
viewconstants.StructAlnSequenceRepresents.SingleChain
    # In combined chain mode, a sequence must represent the entire entry
    seq_can_represent_chain: bool = True
    # Whether seq_represents should be toggled back to SingleChain when
    # seq_can_represent_chain is changed back to True
    seq_represented_chain: bool = True
    align_transforms: viewconstants.StructAlnTransform
    force: bool = False
    map_seqs: bool = False
    ref_asl: str
    other_asl: str
    ref_asl_mode: viewconstants.StructAlnRefASLMode
    other_define_asl: bool = False
    chain_name = parameters.ParamListParam(item_class=ChainName)
    map_seqs_enable: bool = False
    available_ref_chains: List
[docs]    def initConcrete(self):
        self.ref_asl_modeChanged.connect(self._updateRefASL)
        self.ref_aslChanged.connect(self._syncASL)
        self.other_define_aslChanged.connect(self._syncASL)
        self.seq_can_represent_chainChanged.connect(
            self._onSeqCanRepresentChainChanged) 
[docs]    @classmethod
    def fromJsonImplementation(cls, json_dict):
        # Make sure that seq_represented_chain gets restored after
        # seq_can_represent_chain and seq_represents, since changing those
        # values can overwrite seq_represented_chain
        seq_represented_chain = json_dict.pop("seq_represented_chain", True)
        model = super().fromJsonImplementation(json_dict)
        model.seq_represented_chain = seq_represented_chain
        return model 
[docs]    def initializeValue(self):
        self._updateRefASL() 
    @QtCore.pyqtSlot()
    def _updateRefASL(self):
        if self.ref_asl_mode is viewconstants.StructAlnRefASLMode.All:
            self.ref_asl = "all"
    @QtCore.pyqtSlot()
    def _syncASL(self):
        if not self.other_define_asl:
            self.other_asl = self.ref_asl
    def _onSeqCanRepresentChainChanged(self):
        SeqRep = viewconstants.StructAlnSequenceRepresents
        if self.seq_can_represent_chain:
            if self.seq_represented_chain:
                self.seq_represents = SeqRep.SingleChain
        else:
            self.seq_represented_chain = (
                self.seq_represents == SeqRep.SingleChain)
            if self.seq_represented_chain:
                self.seq_represents = SeqRep.EntireEntry
[docs]    def isAlignmentMapValid(self):
        selected_chains = {row.reference for row in self.chain_name}
        valid_aln_map = len(self.available_ref_chains) == len(selected_chains)
        return valid_aln_map 
[docs]    def validateAlignmentMap(self):
        """
        Validate the Alignment chain map,when required.
        :return: Whether the map is valid or not
        :rtype: bool
        """
        validation_required = (
            self.map_seqs and (self.align_transforms
                               == viewconstants.StructAlnTransform.Existing) and
            (self.seq_represents
             == viewconstants.StructAlnSequenceRepresents.SingleChain))
        if validation_required:
            valid_aln_map = self.isAlignmentMapValid()
            return valid_aln_map
        return True 
[docs]    def getUnusedChainWarningText(self):
        """
        Generate a warning message about reference chains that are not used
        in the alignment map.
        :return: Warning message to display
        :rtype: str
        """
        selected_ref_chains = {row.reference for row in self.chain_name}
        unused_ref_chains = set(self.available_ref_chains) - selected_ref_chains
        if not selected_ref_chains:
            return ''
        inflect_engine = inflect.engine()
        text = inflect_engine.plural("chain", len(unused_ref_chains))
        warning_text = f"Invalid mapping: {','.join(unused_ref_chains)} {text} not in use"
        return warning_text 
[docs]    def updateMapData(self, aln):
        """
        Update the params that are associated with chain mapping data in the
        Table and enable/disable the mapping options depending on input seqs.
        :param aln :Protein Sequence alignment
        :type aln : msv.gui.gui_alignment.GuiProteinAlignment
        """
        input_seqs = aln.getSelectedSequences()
        if not input_seqs:
            input_seqs = aln
        ref_seq = aln.getReferenceSeq()
        self.chain_name.clear()
        ref_chains = other_chains = []
        if ref_seq is None or not ref_seq.hasStructure():
            self.available_ref_chains.clear()
            self.map_seqs = False
            self._updateMapSeqsEnable(ref_chains, other_chains)
            return
        ref_eid = ref_seq.entry_id
        ref_chains = {ref_seq.structure_chain}
        for seq in input_seqs:
            if not seq.hasStructure():
                continue
            if seq.entry_id == ref_eid:
                ref_chains.add(seq.structure_chain)
            else:
                chain = seq.structure_chain
                other_chains.append((seq.name, chain, seq.entry_id))
        ref_chains = sorted(ref_chains)
        for other_chain, ref_chain in zip(other_chains,
                                          itertools.cycle(ref_chains)):
            other_chain_name = other_chain[1]
            if other_chain_name in ref_chains:
                ref_chain = other_chain_name
            _chain = ChainName(seq_chain=other_chain, reference=ref_chain)
            self.chain_name.append(_chain)
        self.available_ref_chains = ref_chains
        self._updateMapSeqsEnable(ref_chains, other_chains) 
    def _updateMapSeqsEnable(self, ref_chains, other_chains):
        """
        Disable/Enable the 'map_seqs_cb' depending on the input sequences.
        The mapping options should be disabled when at least 2 chains from the
        reference entry are not used in the map or when more than one chain
        from non-reference sequence is used.
        :param ref_chains: reference chains used in the map
        :type ref_chains: list(str)
        :param other_chains: non-reference chains used in the map.
        :type other_chains: tuple(seq.name,seq.chain)
        """
        enable = True
        unique_other_entries = {chain[2] for chain in other_chains}
        if (len(ref_chains) < 2 or
                len(unique_other_entries) != len(other_chains) or
                not other_chains or len(other_chains) < len(ref_chains)):
            enable = False
        self.map_seqs_enable = enable
        self.map_seqs = False 
[docs]class SuperimposeAlignSettingsModel(parameters.CompoundParam):
    align_sel_res_only: bool = False 
[docs]class BindingSiteAlignSettingsModel(parameters.CompoundParam):
    binding_site_cutoff: annotation.BindingSiteDistance = \
        
annotation.BindingSiteDistance.d5
    align_seqs: bool = False
    align_sel_res_only: bool = False
    mapping_dist: annotation.BindingSiteDistance = \
        
annotation.BindingSiteDistance.d5
    previously_aligned: bool = False 
[docs]class AlignSettingsModel(parameters.CompoundParam):
    align_only_selected_seqs: bool = True
    align_type: viewconstants.AlignType
    seq_align_mode: viewconstants.SeqAlnMode
    struct_align_mode: viewconstants.StructAlnMode
    pairwise: PairwiseAlignSettingsModel
    pairwise_ss: PairwiseSSAlignSettingsModel
    multiple: MultipleAlignSettingsModel
    residue_number: ResidueNumberAlignSettingsModel
    protein_structure: ProteinStructureAlignSettingsModel
    superimpose: SuperimposeAlignSettingsModel
    binding_site: BindingSiteAlignSettingsModel
[docs]    def getAlignMode(self):
        if self.align_type is viewconstants.AlignType.Sequence:
            return self.seq_align_mode
        elif self.align_type is viewconstants.AlignType.Structure:
            return self.struct_align_mode  
[docs]class OptionsModel(parameters.CompoundParam):
    ##################
    # Custom signals
    ##################
    all_visible_annotationsChanged = QtCore.pyqtSignal()
    ##################
    # Params
    ##################
    pick_mode: PickMode = None
    seq_filter: str
    seq_filter_enabled: bool
    auto_align: bool
    align_settings: AlignSettingsModel
    blast_local_only: bool = True
    # View Style Dialog Params
    compute_for_columns: viewconstants.ColumnMode
    font_size: int = 14
    identity_display: viewconstants.IdentityDisplayMode
    include_gaps: bool = False
    res_format: viewconstants.ResidueFormat
    show_conservation_col: bool = False
    show_identity_col: bool = True
    show_score_col: bool = False
    show_similarity_col: bool = False
    wrap_sequences: bool = True
    # Color Dialog Params
    average_in_cols: bool = False
    colors_enabled: bool = True
    seq_color_scheme: color.AbstractRowColorScheme = \
        
color.SideChainChemistryScheme()
    custom_color_scheme: color.AbstractRowColorScheme = None
    color_by_aln: viewconstants.ColorByAln
    weight_by_quality: bool = False
    ws_color_sync: bool = False
    ws_color_all_atoms: bool = False
    # Quick Annotations Dialog Params
    annotations_enabled: bool = False
    antibody_cdr: bool = False
    antibody_cdr_scheme: annotation.AntibodyCDRScheme = annotation.DEFAULT_ANTIBODY_SCHEME
    kinase_features: bool = False
    kinase_features_enabled: bool = False
    binding_site_distance: annotation.BindingSiteDistance = \
        
annotation.BindingSiteDistance.d5
    group_by: viewconstants.GroupBy
    residue_propensity_enabled: bool = False
    residue_propensity_annotations: Set[SEQ_ANNO_TYPES]
    sequence_annotations: Set[SEQ_ANNO_TYPES]
    alignment_annotations: Set[ALN_ANNO_TYPES]
    predicted_annotations: Set[SEQ_ANNO_TYPES]
    annotation_spacer_enabled: bool = True
    sequence_properties: List[SequenceProperty]
    show_hm_ligand_constraints: bool = False
    show_hm_proximity_constraints: bool = False
    @property
    def all_visible_annotations(self):
        # The ruler should always be enabled, so we just add it in here.
        visible = {ALN_ANNO_TYPES.indices}
        if self.annotations_enabled:
            visible.update(self.alignment_annotations,
                           self.sequence_annotations,
                           self.predicted_annotations)
            if self.residue_propensity_enabled:
                visible.update(self.residue_propensity_annotations)
            if self.kinase_features_enabled:
                visible.add(SEQ_ANNO_TYPES.kinase_features)
        if self.pick_mode is PickMode.Pairwise:
            visible.add(SEQ_ANNO_TYPES.pairwise_constraints)
        if (self.show_hm_ligand_constraints or
                self.pick_mode is PickMode.HMBindingSite):
            visible.add(SEQ_ANNO_TYPES.binding_sites)
        if (self.show_hm_proximity_constraints or
                self.pick_mode is PickMode.HMProximity):
            visible.add(SEQ_ANNO_TYPES.proximity_constraints)
        return frozenset(visible)
    ##################
    # Methods
    ##################
[docs]    def initConcrete(self):
        """
        @overrides: parameters.CompoundParam
        """
        self._restore_annotations_enabled = None
        self.residue_propensity_enabledChanged.connect(
            self.all_visible_annotationsChanged)
        self.sequence_annotationsChanged.connect(
            self.all_visible_annotationsChanged)
        self.alignment_annotationsChanged.connect(
            self.all_visible_annotationsChanged)
        self.annotations_enabledChanged.connect(
            self.all_visible_annotationsChanged)
        self.predicted_annotationsChanged.connect(
            self.all_visible_annotationsChanged)
        self.show_hm_ligand_constraintsChanged.connect(
            self.all_visible_annotationsChanged)
        self.show_hm_proximity_constraintsChanged.connect(
            self.all_visible_annotationsChanged)
        self.kinase_features_enabledChanged.connect(
            self.all_visible_annotationsChanged)
        self.annotations_enabledChanged.connect(
            self._onAnnotationsEnabledChanged)
        self.binding_site_distanceChanged.connect(self._enableBindingSiteAnno)
        self.residue_propensity_annotationsChanged.connect(
            self._onResiduePropensityAnnotationsChanged)
        self.pick_modeChanged.connect(self._onPickModeChanged)
        self.seq_filter_enabledChanged.connect(self._updateSeqFilter) 
[docs]    @classmethod
    def getJsonBlacklist(cls):
        """
        @overrides: parameters.CompoundParam
        """
        return [
            # Pick mode and seq_filter are state that shouldn't be retained
            # across sessions
            cls.pick_mode,
            cls.seq_filter,
            cls.seq_filter_enabled,
            # Server settings are stored using preferences
            cls.blast_local_only,
        ] 
[docs]    def toJsonImplementation(self):
        # See parent class for method documentation
        json_dict = super().toJsonImplementation()
        # If the custom color scheme is the currently selected color scheme,
        # don't bother to store it twice
        if self.seq_color_scheme.custom:
            assert self.seq_color_scheme is self.custom_color_scheme
            json_dict["custom_color_scheme"] = None
        return json_dict 
[docs]    @classmethod
    def fromJsonImplementation(cls, json_dict):
        opt_model = super().fromJsonImplementation(json_dict)
        # If the custom color scheme was the currently selected color scheme,
        # make sure that seq_color_scheme and custom_color_scheme point to the
        # same object
        if opt_model.seq_color_scheme.custom:
            opt_model.custom_color_scheme = opt_model.seq_color_scheme
        return opt_model 
[docs]    @json.adapter(version=48003)
    def adapter48003(cls, json_dict):
        color_scheme_name = json_dict["seq_color_scheme"]
        json_dict["seq_color_scheme"] = {
            "name": color_scheme_name,
            "custom": False
        }
        json_dict["custom_color_scheme"] = None
        return json_dict 
[docs]    @json.adapter(version=48007)
    def adapter48007(cls, json_dict):
        json_dict["domains"] = False
        return json_dict 
[docs]    @json.adapter(version=48009)
    def adapter48009(cls, json_dict):
        json_dict.pop("domains")
        return json_dict 
[docs]    @json.adapter(version=49001)
    def adapter49001(cls, json_dict):
        json_dict.pop('consensus_freq')
        json_dict.pop('rescode')
        return json_dict 
    @QtCore.pyqtSlot()
    def _enableBindingSiteAnno(self):
        self.sequence_annotations.add(SEQ_ANNO_TYPES.binding_sites)
    @QtCore.pyqtSlot(object)
    def _onResiduePropensityAnnotationsChanged(self, res_prop_annos):
        if self.residue_propensity_enabled:
            self.all_visible_annotationsChanged.emit()
    @QtCore.pyqtSlot(object)
    def _onPickModeChanged(self, pick_mode):
        if (self.align_settings.pairwise.set_constraints and
                pick_mode is not PickMode.Pairwise):
            self.align_settings.pairwise.set_constraints = False
            if pick_mode is not None:
                # Disabling set_constraints will turn off picking, so we need to set
                # pick_mode again
                # This assignment will call this slot but not this branch
                self.pick_mode = pick_mode
                return
        if pick_mode:
            prev_annotations_enabled = self.annotations_enabled
            self.annotations_enabled = False
            # Store `_restore_annotations_enabled` after changing
            # `annotations_enabled` because changing `annotations_enabled` clears
            # `_restore_annotations_enabled`
            self._restore_annotations_enabled = prev_annotations_enabled
        elif self._restore_annotations_enabled is not None:
            self.annotations_enabled = self._restore_annotations_enabled
        self.all_visible_annotationsChanged.emit()
        self.annotation_spacer_enabled = pick_mode is not PickMode.Pairwise
    @QtCore.pyqtSlot(object)
    @QtCore.pyqtSlot(bool)
    def _onAnnotationsEnabledChanged(self, _):
        self._restore_annotations_enabled = None
    @QtCore.pyqtSlot(object)
    @QtCore.pyqtSlot(bool)
    def _updateSeqFilter(self, seq_filter_enabled):
        """
        When disabling seq filtering, automatically clear the query text
        """
        if not seq_filter_enabled:
            self.seq_filter = "" 
[docs]class AlignmentSignals(gui_alignment.AlignmentSignals):
    @property
    def aln(self):
        return self.parent().aln 
[docs]class PageModel(parameters.CompoundParam):
    alnChanged = QtCore.pyqtSignal(object)
    split_chain_viewChanged = QtCore.pyqtSignal(object)
    _COMBINED_ALN_JSON_KEY = "combined_aln_data"
    split_aln: gui_alignment.GuiProteinAlignment
    options: OptionsModel
    _split_chain_view: bool = True
    title: str = "View"
    is_workspace: bool = False
    menu_statuses: MenuEnabledModel
    blast_task: blast.BlastTask
    homology_modeling_input: hm_models.HomologyModelingInput
    _is_null_page: bool = False
    undo_stack = parameters.NonParamAttribute()
    aln_signals = parameters.NonParamAttribute()
[docs]    def __init__(self, *args, split_chain_view=None, **kwargs):
        """
        Any of the params listed above can be provided as arguments to
        `__init__`.  (`_split_chain_view` may be given as `split_chain_view`.)
        Additional arguments:
        :param undo_stack: The undo stack.  (Toggling between split-chain view
            and combined-chain view is undoable.)
        :type undo_stack: schrodinger.application.msv.command.UndoStack
        """
        if split_chain_view is not None:
            kwargs["_split_chain_view"] = split_chain_view
        super().__init__(*args, **kwargs) 
[docs]    def initConcrete(self, undo_stack=None):
        self._combined_aln = None
        # This class maintains a separate AlignmentSignals object that always
        # mimics the AlignmentSignals object of the current alignment (i.e. the
        # split-chain or combined-chain alignment depending on the current
        # setting of split_chain_view).  That way, clients can connect to
        # signals from aln_signals and not have to worry about disconnecting and
        # reconnecting when split_chain_view changes.
        self.aln_signals = AlignmentSignals(self)
        self._aln_connected_to_aln_signals = None
        self.setUndoStack(undo_stack)
        self.split_alnChanged.connect(self._updateAlnSignals)
        self.split_alnChanged.connect(self.alnChanged)
        self._split_chain_viewChanged.connect(self._onSplitChainViewChanged)
        self._split_chain_viewChanged.connect(self.split_chain_viewChanged)
        if not self.split_chain_view:
            self._combined_aln = \
                
gui_alignment.GuiCombinedChainProteinAlignment(
                    self.split_aln)
        self._updateAlnSignals()
        self.menu_statuses.can_sort_by_chain = self.split_chain_view
        if self.is_workspace:
            self.blast_task.input.settings.download_structures = True
        aln_signals = self.aln_signals
        structure_alignment_signals = [
            aln_signals.sequencesRemoved,
            aln_signals.sequencesInserted,
            aln_signals.sequenceNameChanged,
            aln_signals.sequencesReordered,
            aln_signals.seqSelectionChanged,
            aln_signals.alignmentCleared,
        ]
        for signal in structure_alignment_signals:
            signal.connect(self._updateChainMapData) 
    def _updateChainMapData(self):
        aln = self.split_aln
        self.options.align_settings.protein_structure.updateMapData(aln)
[docs]    @classmethod
    def configureParam(cls):
        """
        @overrides: parameters.CompoundParam
        """
        hm_settings = cls.homology_modeling_input.settings
        cls.setReference(cls.options.show_hm_ligand_constraints,
                         hm_settings.ligand_dlg_model.constrain_ligand)
        cls.setReference(cls.options.show_hm_proximity_constraints,
                         hm_settings.set_constraints) 
[docs]    @classmethod
    def getJsonBlacklist(cls):
        """
        @overrides: parameters.CompoundParam
        """
        return [cls.homology_modeling_input] 
    def __deepcopy__(self, memo):
        dup = super().__deepcopy__(memo)
        if not self.split_chain_view:
            # deepcopy will automatically set dup.split_aln and
            # dup._combined_aln._split_undoable_aln to the same object
            dup._combined_aln = copy.deepcopy(self._combined_aln, memo)
        # The deepcopy modifies private params directly, so we need to make sure
        # that aln_signals is connected to the correct alignment
        dup._updateAlnSignals()
        return dup
[docs]    def isNullPage(self):
        return self._is_null_page 
[docs]    def getShownAnnIndexes(self, seq, ann):
        if ann not in self.options.all_visible_annotations:
            return None
        # TODO MSV-3293 allow hiding annotation rows from specific sequences
        # TODO MSV-3294 allow deleting specific multi-value annotation rows
        return tuple(range(seq.getNumAnnValues(ann))) 
[docs]    def toJsonImplementation(self):
        json_dict = super().toJsonImplementation()
        if not self.split_chain_view:
            combined_aln_json_data = \
                
self._combined_aln.jsonDataWithoutSplitAln()
            json_dict[self._COMBINED_ALN_JSON_KEY] = combined_aln_json_data
        return json_dict 
[docs]    @classmethod
    def fromJsonImplementation(cls, json_dict):
        combined_aln_json_data = json_dict.pop(cls._COMBINED_ALN_JSON_KEY, None)
        page_model = super().fromJsonImplementation(json_dict)
        if not page_model.split_chain_view:
            GCCPA = gui_alignment.GuiCombinedChainProteinAlignment
            page_model._combined_aln = GCCPA.fromJsonAndSplitAln(
                page_model.split_aln, combined_aln_json_data)
        # super().fromJsonImplementation modifies private params directly, so we
        # need to make sure that aln_signals is connected to the correct
        # alignment
        page_model._updateAlnSignals()
        return page_model 
    @property
    def aln(self):
        """
        The split-chain or combined-chain alignment, based on the current
        `OptionsModel.split_chain_view` setting.  Note that this value should
        always be set using a split-chain alignment.
        Assigning a new split-chain alignment to a page that is already in
        `MsvGuiModel.pages` will not work reliably. To add a page with a
        pre-existing new alignment, use `MsvGuiModel.addViewPage(aln=new_aln)`.
        :rtype: gui_alignment.GuiProteinAlignment or
            gui_alignment.GuiCombinedChainProteinAlignment
        """
        if self.split_chain_view:
            return self.split_aln
        else:
            return self._combined_aln
    @aln.setter
    def aln(self, val):
        if not isinstance(val, gui_alignment.GuiProteinAlignment):
            raise TypeError(f"Invalid type for aln: {type(val)}")
        self.split_aln = val
        self.split_aln.setSeqFilterEnabled(self.options.seq_filter_enabled)
        self.split_aln.setSeqFilterQuery(self.options.seq_filter)
        if not self.split_chain_view:
            self._combined_aln = \
                
gui_alignment.GuiCombinedChainProteinAlignment(
                    self.split_aln)
            self._updateAlnSignals()
        self.alnChanged.emit(self.aln)
[docs]    def isSplitChainViewDefault(self):
        """
        Check whether split chain view is set to its default.
        :return: Whether split_chain_view is default
        :rtype: bool
        """
        Cls = type(self)
        default = Cls._split_chain_view.defaultValue()
        return self._split_chain_view == default 
    @property
    def split_chain_view(self):
        """
        Whether the current view is split-chain (True) or combined-chain
        (False).  Note that toggling this value is undoable and will clear all
        residue anchors.
        :rtype: bool
        """
        return self._split_chain_view
    @split_chain_view.setter
    def split_chain_view(self, value):
        if value == self._split_chain_view:
            return
        aln = self.aln
        clear_text = []
        clear_funcs = []
        if aln.getAnchoredResidues():
            clear_text.append("Residue Anchors")
            clear_funcs.append(aln.clearAnchors)
        if aln.hasAlnSets():
            clear_text.append("Alignment Sets")
            clear_funcs.append(lambda: aln.removeSeqsFromAlnSet(aln))
        if clear_text:
            desc = f"Toggle Split Chain View and Clear {' and '.join(clear_text)}"
            with command.compress_command(self.undo_stack, desc):
                for func in clear_funcs:
                    func()
                self._setSplitChainViewCommand(value)
        else:
            self._setSplitChainViewCommand(value)
    @command.do_command
    def _setSplitChainViewCommand(self, value):
        # If we're undoing enabling split_chain_view, we need to restore the
        # same combined-chain instance.  Otherwise, further undos won't work
        # since they'll get undone on the wrong instance.
        if value:
            cur_combined_align = self._combined_aln
        else:
            cur_combined_align = \
                
gui_alignment.GuiCombinedChainProteinAlignment(
                    self.split_aln)
        def enable():
            self._combined_aln = None
            self._split_chain_view = True
            struc_aln_settings = self.options.align_settings.protein_structure
            struc_aln_settings.seq_can_represent_chain = True
            self.alnChanged.emit(self.aln)
        def disable():
            self._combined_aln = cur_combined_align
            self._split_chain_view = False
            struc_aln_settings = self.options.align_settings.protein_structure
            struc_aln_settings.seq_can_represent_chain = False
            self.alnChanged.emit(self.aln)
        desc = "Toggle Split Chain"
        if value:
            return enable, disable, desc
        else:
            return disable, enable, desc
    @QtCore.pyqtSlot(object)
    @QtCore.pyqtSlot(bool)
    def _onSplitChainViewChanged(self, split_chain_view):
        """
        Respond to changes in split chain view, since some items aren't
        appropriate or implemented yet for combined chain view.
        This includes aligning methods that involve use of the structure when in
        combined-chain mode, as those aligning methods don't yet work with
        combined-chain mode.  (See MSV-2362.)
        :param split_chain_view: Whether split-chain view is enabled.
        :type split_chain_view: bool
        """
        self.menu_statuses.can_sort_by_chain = split_chain_view
        self.menu_statuses.can_select_antibody_chain = split_chain_view
        self.menu_statuses.can_set_constraints = split_chain_view
        self.menu_statuses.can_renumber_residues = split_chain_view
        if not split_chain_view:
            self.options.align_settings.pairwise.set_constraints = False
        self._updateAlnSignals()
[docs]    def setUndoStack(self, undo_stack):
        self.undo_stack = undo_stack
        self.split_aln.setUndoStack(undo_stack) 
[docs]    def regenerateCombinedChainAlignment(self):
        """
        If `split_chain_view` is True, do nothing.  If False, recreate the
        combined-chain alignment using the split-chain alignment. This method
        must be called whenever the split-chain alignment is modified directly
        while in combined-chain mode.  If `split_chain_view` is False, this
        method will delete all residue anchors and the action cannot be undone.
        :note: This method should not typically be necessary. Any modifications
            should be made to the combined-chain alignment, which will
            automatically update the split-chain alignment as required. This
            method should only be called when updates must be made directly to
            the split-chain alignment.
        """
        if not self._split_chain_view:
            self._combined_aln = \
                
gui_alignment.GuiCombinedChainProteinAlignment(
                    self.split_aln)
            self._updateAlnSignals()
            self.alnChanged.emit(self._combined_aln) 
    def _updateAlnSignals(self):
        """
        Connect the current alignment's `AlignmentSignals` object to this
        model's `AlignmentSignals` object and disconnect the previous alignment.
        """
        if self.aln is self._aln_connected_to_aln_signals:
            return
        if self._aln_connected_to_aln_signals is not None:
            aln = self._aln_connected_to_aln_signals
            for signal, slot in zip(aln.signals.allSignals(),
                                    self.aln_signals.allSignals()):
                signal.disconnect(slot)
        if self.aln is not None:
            # self.aln can be None if this method gets triggered while the
            # instance is in an intermediate state (e.g. in the middle of
            # being populated during a deepcopy)
            for signal, slot in zip(self.aln.signals.allSignals(),
                                    self.aln_signals.allSignals()):
                signal.connect(slot)
        self._aln_connected_to_aln_signals = self.aln 
[docs]class NullPage(PageModel):
    _is_null_page: bool = True 
[docs]class HeteromultimerSettings(parameters.CompoundParam):
    """
    Settings for heteromultimer homology modeling
    """
    selected_pages: List[PageModel]
    settings: hm_models.HomologyModelingSettings
    ready: bool
[docs]    def initConcrete(self):
        super().initConcrete()
        self.selected_pagesChanged.connect(self._updateReady) 
    def _updateReady(self):
        self.ready = len(self.selected_pages) >= 2 and all(
            page.homology_modeling_input.ready for page in self.selected_pages) 
[docs]class MsvGuiModel(parameters.CompoundParam):
    """
    The model for the entire MSV2.
    """
    pages: List[PageModel]
    current_page: PageModel = NullPage()
    light_mode: bool = False
    blast_local_only: bool = True
    sequence_local_only: bool = True
    pdb_local_only: bool = True
    auto_align: bool = False
    edit_mode: bool = False
    auto_save_only_on_edits: bool = True
    align_settings: AlignSettingsModel
    heteromultimer_settings: HeteromultimerSettings
    custom_color_scheme: color.AbstractRowColorScheme = None
    undo_stack = parameters.NonParamAttribute()
    _hm_launcher_task: hm_models.HMLauncherTask
    _emittingMutated = util.flag_context_manager("_emitting_mutated")
[docs]    def initConcrete(self):
        """
        @overrides: parameters.CompoundParam
        """
        super().initConcrete()
        self.undo_stack = None
        self._page_option_mapper = None
        self._emitting_mutated = False
        self.pages.mutated.connect(self.onPagesMutated)
        self._updateUserSettings()
        for param in _USER_SETTINGS.keys():
            signal = param.getParamSignal(self)
            slot = partial(self._storeUserSetting, param)
            signal.connect(slot)
        self.current_pageReplaced.connect(self._onCurrentPageReplaced) 
[docs]    @json.adapter(version=49003)
    def adapter49003(cls, json_dict):
        # Switched from storing entire task to just storing input
        hm_task = json_dict.pop('homology_modeling_task')
        json_dict['homology_modeling_input'] = hm_task['input']
        return json_dict 
[docs]    @json.adapter(version=50002)
    def adapter50002(cls, json_dict):
        # Moved homology modeling input from MsvGuiModel to PageModel
        json_dict.pop('homology_modeling_input')
        return json_dict 
[docs]    @classmethod
    def getJsonBlacklist(cls):
        """
        @overrides: parameters.CompoundParam
        """
        blacklist = list(_USER_SETTINGS.keys())
        return blacklist + [
            cls.heteromultimer_settings,
            cls.align_settings,
            cls._hm_launcher_task,
            cls.edit_mode,
        ] 
[docs]    @util.skip_if("_emitting_mutated")
    @QtCore.pyqtSlot(object, object)
    def onPagesMutated(self, new_pages, old_pages):
        """
        Synchronize settings that should be global with all pages
        """
        new_current = self._getNewCurrentPage(new_pages, old_pages)
        if new_current is not None:
            with self._emittingMutated():
                # Re-emit the mutated signal because replacing `current_page`
                # will disconnect any slots of mutated in getSignalsAndSlots
                self.pages.mutated.emit(new_pages, old_pages)
            self.current_page = new_current
        mapper = mappers.TargetParamMapper()
        for page in self.pages:
            for guimodel_param, page_param in _GLOBAL_PARAMS:
                auto_tgt = mappers.ParamTargetSpec(page, page_param)
                mapper.addMapping(auto_tgt, guimodel_param)
        if self._page_option_mapper is not None:
            self._page_option_mapper.setModel(None)
        self._page_option_mapper = mapper
        self._page_option_mapper.setModel(self)
        aln = self.current_page.split_aln
        self.current_page.options.align_settings.protein_structure.updateMapData(
            aln) 
    def _getNewCurrentPage(self, new_pages, old_pages):
        if not self.pages:
            return NullPage()
        if self.current_page.isNullPage():
            return self.pages[-1]
        added, removed, moved = diffy.get_diff(new_pages, old_pages)
        if added:
            first_added_page, _ = sorted(added, key=lambda x: x[1])[0]
            return first_added_page
        elif removed:
            for removed_page, removed_idx in removed:
                if self.current_page == removed_page:
                    break
            else:
                # Current page was not removed, don't change
                return None
            # Use the same index or the last page
            new_idx = min(removed_idx, len(self.pages) - 1)
            new_page = self.pages[new_idx]
            return new_page
        return None
[docs]    def appendSavedPages(self, new_pages):
        """
        Load pages that were saved to file
        """
        # Set the global values from the first new page on the model
        page = new_pages[0]
        blacklist = scollections.IdSet(self.getJsonBlacklist())
        for guimodel_param, page_param in _GLOBAL_PARAMS:
            if guimodel_param in blacklist:
                continue
            page_value = page_param.getParamValue(page)
            guimodel_param.setParamValue(self, page_value)
        self.pages.extend(new_pages) 
[docs]    def setUndoStack(self, undo_stack):
        """
        Set the undo stack to use.  (Toggling between split-chain view and
        combined-chain view is undoable.)
        :param undo_stack: The undo stack.
        :type undo_stack: schrodinger.application.msv.command.UndoStack
        """
        self.undo_stack = undo_stack
        for page in self.pages:
            page.setUndoStack(undo_stack) 
[docs]    def addViewPage(self, *, aln=None):
        """
        Add a view page, i.e. a page that doesn't represent the workspace.
        :param aln: An alignment to use for the page
        :type aln: gui_alignment.GuiProteinAlignment
        :return: The newly created view page
        :rtype: PageModel
        """
        page_model = PageModel(undo_stack=self.undo_stack)
        if aln is not None:
            page_model.aln = aln
        num_view_tabs = sum(1 for page in self.pages if not page.is_workspace)
        page_model.title = f"View {num_view_tabs+1}"
        self.pages.append(page_model)
        return page_model 
[docs]    def addWorkspacePage(self, aln):
        """
        Add a page representing the workspace.  If a workspace page is required,
        this method must be called before any view pages are added.
        :param aln: The workspace alignment
        :type aln: gui_alignment.GuiProteinAlignment
        :return: The newly created workspace page
        :rtype: PageModel
        """
        ws_pm = PageModel(is_workspace=True,
                          title='Workspace',
                          undo_stack=self.undo_stack)
        ws_pm.aln = aln
        ws_pm.menu_statuses.can_delete_tab = False
        self.pages.append(ws_pm)
        return ws_pm 
[docs]    def hasWorkspacePage(self):
        return self.pages and self.pages[0].is_workspace 
[docs]    def getWorkspacePage(self):
        """
        Return the current workspace page if one exists.
        :rtype: PageModel
        :raises RuntimeError: If no workspace page exists.
        """
        if not self.hasWorkspacePage():
            raise RuntimeError("No workspace page present.")
        return self.pages[0] 
[docs]    def getViewPages(self):
        if self.hasWorkspacePage():
            return self.pages[1:]
        else:
            return self.pages[:] 
[docs]    def reset(self, *args, **kwargs):
        if args or kwargs:
            super().reset(*args, **kwargs)
        else:
            self.resetPages()
            if self.undo_stack is not None:
                self.undo_stack.clear() 
[docs]    def resetPages(self):
        pages = self.pages
        while pages and not pages[-1].is_workspace:
            pages.pop()
        self.addViewPage()
        self.current_page = self.pages[0] 
[docs]    def duplicatePage(self, index):
        def update_suffix(match):
            if match.group(1) is None:
                return " Copy"
            cur_count = match.group(2)
            if cur_count is None:
                new_count = 2
            else:
                new_count = int(cur_count) + 1
            return " Copy %i" % new_count
        copied_page_model = copy.deepcopy(self.pages[index])
        copied_page_model.options.pick_mode = None
        original_title = copied_page_model.title
        title = re.sub(r"( Copy(?: (\d*))?)?$", update_suffix, original_title)
        copied_page_model.title = title
        copied_page_model.is_workspace = False
        self.pages.append(copied_page_model) 
[docs]    @classmethod
    def fromJsonImplementation(cls, json_dict):
        current_page_idx = json_dict.pop('current_page_idx')
        ret = super().fromJsonImplementation(json_dict)
        if current_page_idx is None:
            ret.current_page = NullPage()
        else:
            ret.current_page = ret.pages[current_page_idx]
        return ret 
[docs]    def toJsonImplementation(self):
        """
        Save just the index of the `current_page`. We turn it back into a proper
        `PageModel` when we deserialize.
        """
        ret = super().toJsonImplementation()
        ret['pages'] = [p for p in ret['pages'] if not p.is_workspace]
        non_ws_pages = [p for p in self.pages if not p.is_workspace]
        if self.current_page.is_workspace:
            idx = None
        else:
            idx = non_ws_pages.index(self.current_page)
        ret['current_page_idx'] = idx
        ret.pop('current_page')
        return ret 
[docs]    def getAlignmentOfSequence(self, seq):
        """
        Returns the alignment that contains `seq` or `None` if none of the
        pages' alignments own `seq`.  The split-chain alignment will be returned
        regardless of the tab's current split chain view setting.  If you need
        access to the combined-chain alignment, use `getPageInfoForSequence`
        instead.
        :param seq: The split-chain sequence to find the owner of.
        :type  seq: sequence.Sequence
        :rtype: alignment.ProteinAlignment or None
        """
        for page in self.pages:
            if seq in page.split_aln:
                return page.split_aln
        # Sequence not owned by any pages alignment so return None
        return None 
[docs]    def getPageInfoForSequence(self, seq):
        """
        Returns information about the page that contains `seq` or `None` if none
        of the pages alignments own `seq`.
        :param seq: The split-chain sequence to find the owner of.
        :type  seq: sequence.Sequence
        :rtype: SequencePageInfo or None
        """
        for page in self.pages:
            if seq in page.split_aln:
                return SequencePageInfo(seq, page)
        # Sequence not owned by any pages alignment so return None
        return None 
    def _storeUserSetting(self, param, value):
        """
        When the given param changes to a not-none value, store its value in
        the user's persistent settings
        """
        if value is None:
            # Preferences cannot store None
            return
        pref_key = _USER_SETTINGS[param]
        settings.set_persistent_value(pref_key, value)
    def _updateUserSettings(self):
        """
        Update the model with the user's persistent settings
        """
        for param, pref_key in _USER_SETTINGS.items():
            value = settings.get_persistent_value(pref_key, None)
            if value is not None:
                # Only update for non-missing values
                param.setParamValue(self, value)
    def _onCurrentPageReplaced(self):
        aln = self.current_page.split_aln
        self.current_page.options.align_settings.protein_structure.updateMapData(
            aln) 
# MsvGuiModel subparams that are mapped to each page
_GLOBAL_PARAMS = [
    (MsvGuiModel.auto_align, PageModel.options.auto_align),
    (MsvGuiModel.align_settings, PageModel.options.align_settings),
    (MsvGuiModel.blast_local_only, PageModel.options.blast_local_only),
    (MsvGuiModel.custom_color_scheme, PageModel.options.custom_color_scheme),
]  # yapf: disable
# MsvGuiModel subparams that are stored as user preferences
_USER_SETTINGS = {
    MsvGuiModel.blast_local_only: 'MSV2_BLAST_LOCAL_ONLY',
    MsvGuiModel.sequence_local_only: 'MSV2_SEQUENCE_LOCAL_ONLY',
    MsvGuiModel.pdb_local_only: 'MSV2_PDB_LOCAL_ONLY',
}  # yapf: disable
[docs]class SequencePageInfo(
        namedtuple("SequencePageInfo", ("aln", "split_aln", "seq", "split_seq",
                                        "split_chain_view", "chain_offset"))):
    """
    Information about the page that a given sequence is on.
    :ivar aln: The current alignment for the page.  Can be either split-chain or
        combined-chain.
    :vartype aln: gui_alignment.GuiProteinAlignment or
        gui_alignment.GuiCombinedChainProteinAlignment
    :ivar split_aln: The split-chain alignment for the page.
    :vartype split_aln: gui_alignment.GuiProteinAlignment
    :ivar seq: The relevant sequence in `aln`.  Can be either a split-chain or
        combined-chain sequence.
    :vartype seq: sequence.ProteinSequence or
        sequence.CombinedChainProteinSequence or sequence.SequenceProxy
    :ivar split_seq: The split-chain sequence.
    :vartype seq: sequence.ProteinSequence or sequence.SequenceProxy
    :ivar split_chain_view: The current split-chain view setting for the page.
        Will be True for split-chain and False for combined-chain.
    :vartype split_chain_view: bool
    :ivar chain_offset: In combined-chain mode, gives the index of the first
        residue of `split_seq` in `seq`.  In split-chain mode, is 0.
    :vartype chain_offset: int
    """
    def __new__(cls, split_seq, page):
        """
        :param split_seq: The split-chain sequence
        :type split_seq: sequence.Sequence
        :param page: The page that the sequence is on
        :type page: PageModel
        """
        if page.split_chain_view:
            seq = split_seq
            chain_offset = 0
        else:
            seq = page.aln.combinedSeqForSplitSeq(split_seq)
            chain_offset = seq.offsetForChain(split_seq)
        return super().__new__(cls, page.aln, page.split_aln, seq, split_seq,
                               page.split_chain_view, chain_offset) 
[docs]class ExportImageModel(parameters.CompoundParam):
    """
    Model for the export image dialog. The values of the enums are what gets
    shown as text in the file dialog's combo boxes
    """
[docs]    class ExportTypes(enum.Enum):
        ENTIRE_ALN = "Entire alignment region"
        VISIBLE_ALN = "Visible alignment region"
        SEQ_LOGO = "Sequence Logo only" 
[docs]    class Dpi(enum.IntEnum):
        SEVENTY_TWO = 72
        ONE_HUNDRED_FIFTY = 150
        THREE_HUNDRED = 300
        SIX_HUNDRED = 600 
    export_type: ExportTypes
    dpi: Dpi = Dpi.ONE_HUNDRED_FIFTY
    format: Format 
[docs]class ExportSequenceModel(parameters.CompoundParam):
    """
    Model for the export sequence dialog.
    """
[docs]    class Sequences(enum.Enum):
        DISPLAYED = "Displayed Sequences"
        ALL = "All Sequences"
        SELECTED = "Selected Sequences" 
[docs]    class Residues(enum.Enum):
        ALL = "Full Alignment"
        SELECTED = "Selected Blocks" 
    SplitFileBy = enum.IntEnum('SplitFileBy', 'Chain Structure')
    format: Format
    which_sequences: Sequences
    which_residues: Residues
    preserve_indices: bool = False
    include_ss_anno: bool = False
    include_similarity: bool = False
    enable_non_format_options: bool = True
    export_descriptors: bool = False
    enable_export_descriptors: bool = False
    create_multiple_files: bool = False
    split_file_by: SplitFileBy
[docs]    def initConcrete(self):
        self.formatChanged.connect(self.onFormatChanged)
        self.enable_non_format_optionsChanged.connect(self.onEnableChanged)
        self._prev_include_ss_anno = self.include_ss_anno
        self._prev_include_similarity = self.include_similarity 
[docs]    def onEnableChanged(self):
        if self.enable_non_format_options is True:
            self.include_ss_anno = self._prev_include_ss_anno
            self.include_similarity = self._prev_include_similarity
        else:
            self._prev_include_ss_anno = self.include_ss_anno
            self._prev_include_similarity = self.include_similarity
            self.include_ss_anno = False
            self.include_similarity = False