'''
Created on Dec 29, 2009

@author: gaborb
'''

import gtk

import os
import tempfile
import subprocess
import copy

PYGTK_RESPONSE_OK = -5


class InvalidSetting(RuntimeError):
    pass


# Helper functions for drawing GUI
def create_group(components):

    vbox = gtk.VBox()
    vbox.set_spacing( 5 )

    for comp in components:
        vbox.add( comp )

    return vbox


def create_framed_group(title, components):

    vbox = gtk.VBox()
    vbox.set_spacing( 5 )

    for comp in components:
        vbox.add( comp )

    hbox = gtk.HBox( False, 0 )
    hbox.pack_start( vbox, False, False, 0 )

    frame = gtk.Frame( title )
    frame.add( hbox )
    return frame


def create_radio_button_group(titles):

    assert titles
    f = gtk.RadioButton( None, titles[0] )
    buttons = [ f ] + [ gtk.RadioButton( f, t ) for t in titles[1:] ]
    return buttons


def create_combo_box(values, default = None):

    combo = gtk.combo_box_new_text()

    for m in values:
        combo.append_text( m )

    return combo


def create_notebook_page(notebook, title):

    frame_box = gtk.VBox( False, 20 )
    frame_box.set_border_width( 10 )
    notebook.append_page( frame_box, gtk.Label( title ) )
    return frame_box


def create_checkbox_controlled_entry_group(label, model, key, constant, data, extras = []):

    cb = gtk.CheckButton( label )
    connect_multi_selection_checkbox(
        widget = cb,
        setting = model.get( key = key ),
        constant = constant
        )
    params = create_text_entry_group( model = model, data = data )

    for w in params.get_children() + extras:
        cb.connect( "toggled", disable_if_unchecked, w )

    cb.toggled()
    group = gtk.VBox( False, 0 )
    group.pack_start( cb, False, False, 0 )
    group.pack_start( params, False, False, 0 )

    for e in extras:
        group.pack_start( e, False, False, 0 )

    return group


def create_text_entry_group(model, data):

    if not data:
        return gtk.HBox( False, 0 )

    params_box = gtk.Table( len( data ), 2, False )

    for ( index, ( label, key ) ) in enumerate( data ):
        params_box.attach( gtk.Label( label ), 0, 1, index, index + 1, xpadding = 20 )
        entry = gtk.Entry()
        params_box.attach( entry, 1, 2, index, index + 1 )
        connect_text_entry( widget = entry, setting = model.get( key = key ) )

    return params_box

# Conversion for model connection
def to_integer(value):

    return int( value )


def to_float(value):

    return float( value )


def to_value_or_none(value):

    if not value:
        return None

    else:
        return value


def to_string(value):

    return str( value ) if value is not None else ""


# Populate comboboxes
def refresh_combo(widget, data):

    ( combo, setting ) = data
    setting.update()
    liststore = gtk.ListStore(str)

    for fn in setting.allowed:
        liststore.append( [ fn ] )

    combo.set_model( liststore )
    combo.set_active( setting.index() )


# Model connections
def connect_text_entry(widget, setting):

    widget.set_text( str( setting ) )
    widget.connect( "changed", on_text_entry_change, setting )


def connect_combo_box(widget, setting):

    widget.set_active( setting.index() )
    widget.connect( "changed", on_combo_select, setting )


def connect_radio_button(widget, setting, index):

    constant = setting.allowed[ index ]
    widget.set_active( setting.value == constant )
    widget.connect( "toggled", on_radio_button_toggle, ( setting, constant ) )


def connect_single_selection_checkbox(widget, setting):

    widget.set_active( setting.value )
    widget.connect( "toggled", on_single_checkbox_toggle, setting )


def connect_multi_selection_checkbox(widget, setting, constant):

    widget.set_active( constant in setting.current )
    widget.connect( "toggled", on_multi_checkbox_toggle, ( setting, constant ) )


# Actions
def disable_if_unset(widget, data):

    data.set_sensitive( widget.get_active() != -1 )


def disable_if_unchecked(widget, data):

    data.set_sensitive( widget.get_active() )


def on_text_entry_change(widget, data):

    try:
        data.set( widget.get_text() )

    except ValueError, e:
        widget.set_text( str( data ) )


def on_radio_button_toggle(widget, data):

    if widget.get_active():
        ( setting, constant ) = data
        setting.set( value = constant )


def on_single_checkbox_toggle(widget, data):

    if widget.get_active():
        data.set()

    else:
        data.unset()


def on_multi_checkbox_toggle(widget, data):

    ( setting, constant ) = data

    if widget.get_active():
        setting.set( value = constant )

    else:
        setting.unset( value = constant )


def on_combo_select(widget, data):

        index = widget.get_active()

        if index == -1:
            value = None

        else:
            value = widget.get_model()[ index ][0]

        data.set( value = value )


def on_browse_alignment_files(widget, data):

    chooser = gtk.FileChooserDialog(
        title="Open alignment...",
        action=gtk.FILE_CHOOSER_ACTION_OPEN,
        buttons=(
            gtk.STOCK_CANCEL,
            gtk.RESPONSE_CANCEL,
            gtk.STOCK_OPEN,
            gtk.RESPONSE_OK
            )
        )
    chooser.set_default_response(gtk.RESPONSE_OK )
    chooser.set_current_folder( os.getcwd() )
    filter = gtk.FileFilter()
    filter.set_name( "All files" )
    filter.add_pattern( "*" )
    chooser.add_filter( filter )

    filter = gtk.FileFilter()
    filter.set_name( "Alignments" )
    filter.add_pattern( "*.aln" )
    filter.add_pattern( "*.ali" )
    filter.add_pattern( "*.pir" )
    chooser.add_filter( filter )

    response = chooser.run()

    if response == int( gtk.RESPONSE_OK ):
        data.set_text( chooser.get_filename() )

    chooser.destroy()


def on_similarity_calculation(widget, data):

    ( matrix, window, weighting ) = data
    c_matrix = matrix.copy()
    c_window = window.copy()
    c_weighting = weighting.copy()

    dialog = gtk.Dialog(
        title = "Calculation...",
        parent = None,
        flags = gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
        buttons = (
            gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,
            gtk.STOCK_OK, gtk.RESPONSE_ACCEPT,
            )
        )
    area = dialog.vbox

    # Matrix
    hb1 = gtk.HBox()
    hb1.pack_start( gtk.Label( "Matrix:" ) )
    matrix_combo = create_combo_box( values = c_matrix.allowed )
    connect_combo_box( widget = matrix_combo, setting = c_matrix )
    hb1.pack_start( matrix_combo )
    area.pack_start( hb1 )

    # Window
    hb2 = gtk.HBox()
    hb2.pack_start( gtk.Label( "Window:" ) )
    window_entry = gtk.Entry()
    connect_text_entry( widget = window_entry, setting = c_window )
    hb2.pack_start( window_entry )
    area.pack_start( hb2 )

    # Weighting
    hb3 = gtk.HBox()
    hb3.pack_start( gtk.Label( "Weighting:" ) )
    weighting_combo = create_combo_box( values = c_weighting.allowed )
    connect_combo_box( widget = weighting_combo, setting = c_weighting )
    hb3.pack_start( weighting_combo )
    area.pack_start( hb3 )

    # Run
    area.show_all()
    response = dialog.run()

    if response == int( gtk.RESPONSE_ACCEPT ):
        matrix.set( value = str( c_matrix.value ) )
        window.set( value = str( c_window.value ) )
        weighting.set( value = str( c_weighting.value ) )

    dialog.destroy()


def get_similarity_settings(model, root):

    properties = [ "matrix", "window", "weighting" ]
    return [ model.get( key = "%s.%s" % ( root, p ) ) for p in properties ]


class SculptorView(object):
    """
    GUI for phenix.sculptor
    """

    def __init__(self):

        # Setup functionality
        self._model = SculptorModel()
        self._preview = None

        # Setup GUI
        self.window = gtk.Window( gtk.WINDOW_TOPLEVEL )
        self.window.set_title( "Sculptor GUI" )
        self.window.connect( "delete_event", self.delete_event, None )
        self.window.connect( "destroy", self.destroy, None )

        # Setup layout
        main_box = gtk.HBox( False, 10 )
        input_box = gtk.VBox( False, 10 )
        control_box = gtk.VBox( False, 0 )
        self.window.add( main_box )
        main_box.pack_start( input_box, False, False, 5 )
        main_box.pack_start( control_box, False, False, 5 )

        # Setup control
        self.preview_button = gtk.Button( "Preview" )
        self.create_button = gtk.Button( "Create" )
        self.cancel_button = gtk.Button( "Cancel" )
        self.preview_button.connect( "clicked", self.on_preview, None )
        self.create_button.connect( "clicked", self.on_create, None )
        self.cancel_button.connect_object(
            "clicked",
            gtk.Widget.destroy,
            self.window
            )
        control_box.pack_end( self.cancel_button, False, False, 5 )
        control_box.pack_end( self.create_button, False, False, 5 )
        control_box.pack_end( self.preview_button, False, False, 5 )
        self.create_button.set_sensitive( False )

        # Alignment file input
        alignment_box = gtk.HBox( False, 10 )
        alignment_box.pack_start( gtk.Label( "Alignment:" ), False, False, 0 )

        alignment_entry = gtk.Entry()
        connect_text_entry(
            widget = alignment_entry,
            setting = self._model.get( "input.alignment.file_name" )
            )
        alignment_box.pack_start( alignment_entry, False, False, 0 )

        alignment_browse = gtk.Button( "Browse" )
        alignment_browse.connect( "clicked", on_browse_alignment_files, alignment_entry )
        alignment_box.pack_start( alignment_browse, False, False, 0 )

        input_box.pack_start( alignment_box, False, False, 5 )

        # Model selector
        imol_box = gtk.HBox( False, 10 )
        imol_box.pack_start( gtk.Label( "Model:" ), False, False, 0 )

        imol_combo = gtk.combo_box_new_text()
        imol_box.pack_start( imol_combo, False, False, 0 )
        imol_refresh = gtk.Button( "Refresh" )
        setting = self._model.get( key = "input.model.file_name" )
        imol_refresh.connect( "clicked", refresh_combo, ( imol_combo, setting ) )
        imol_refresh.clicked()
        connect_combo_box( widget = imol_combo, setting = setting )
        imol_box.pack_start( imol_refresh, False, False, 0 )

        # Chain selector
        chain_box = gtk.HBox( False, 10 )
        chain_box.pack_start( gtk.Label( "Chain:" ), False, False, 0 )
        chain_combo = gtk.combo_box_new_text()
        setting = self._model.get( key = "input.model.selection" )
        imol_combo.connect( "changed", refresh_combo, ( chain_combo, setting ) )
        imol_combo.connect( "changed", disable_if_unset, chain_combo )
        imol_combo.set_active( imol_combo.get_active() )
        connect_combo_box( widget = chain_combo, setting = setting )
        chain_box.pack_start( chain_combo, False, False, 0 )
        chain_combo.connect( "changed", disable_if_unset, self.preview_button )
        chain_combo.set_active( chain_combo.get_active() )
        input_box.pack_start( imol_box, False, False, 0 )
        input_box.pack_start( chain_box, False, False, 0 )
        input_box.pack_start( gtk.HSeparator() )

        # Generic sculptor options
        general_box = gtk.VBox( False, 10 )
        input_box.pack_start( general_box )

        # Renumbering
        renumber_rbs = create_radio_button_group(
            titles = [ "Target", "Model", "None" ]
            )

        for ( i, b ) in enumerate( renumber_rbs ):
            connect_radio_button(
                widget = b,
                setting = self._model.get( key = "mm.renumber.use" ),
                index = i
                )

        general_box.pack_start(
            create_framed_group( title = "Renumbering", components = renumber_rbs )
            )

        # Renaming
        rename_cb = gtk.CheckButton( "Rename residues" )
        connect_single_selection_checkbox(
            widget = rename_cb,
            setting = self._model.get( key = "mm.rename.use" )
            )
        general_box.pack_start( rename_cb )
        input_box.pack_start( gtk.HSeparator() )

        # Selector for normal/protocol mode
        mode_rbs = create_radio_button_group(
            titles = [ "Predefined protocols", "User-defined protocol" ]
            )

        # Predefined mode
        predefined_box = gtk.HBox( False, 10 )
        connect_radio_button(
            widget = mode_rbs[0],
            setting = self._model.get( key = "multiprotocol" ),
            index = 0
            )
        predefined_box.pack_start( mode_rbs[0] )

        setting = self._model.get( key = "mm.protocols" )
        predefined_combo = create_combo_box( values = setting.allowed )
        connect_combo_box( widget = predefined_combo, setting = setting )
        mode_rbs[0].connect( "toggled", disable_if_unchecked, predefined_combo )
        predefined_box.pack_start( predefined_combo )
        input_box.pack_start( predefined_box )

        # User defined mode
        user_defined_box = gtk.HBox( False, 10 )
        connect_radio_button(
            widget = mode_rbs[1],
            setting = self._model.get( key = "multiprotocol" ),
            index = 1
            )
        user_defined_box.pack_start( mode_rbs[1] )
        notebook = gtk.Notebook()
        input_box.pack_start( user_defined_box )
        input_box.pack_start( notebook )
        mode_rbs[1].connect( "toggled", disable_if_unchecked, notebook )

        # Deletion
        page = create_notebook_page( notebook = notebook, title = "Deletion" )
        param_groups = []
        tbs_ssc_button = gtk.Button( "Calculation..." )
        tbs_ssc_button.connect(
            "clicked",
            on_similarity_calculation,
            get_similarity_settings( model = self._model, root = "mm.deletion.tbs.calc" )
            )
        cbs_ssc_button = gtk.Button( "Calculation..." )
        cbs_ssc_button.connect(
            "clicked",
            on_similarity_calculation,
            get_similarity_settings( model = self._model, root = "mm.deletion.cbs.calc" )
            )

        creation_data = (
            ( "Gap", "gap", [], [] ),
            (
                "Threshold based similarity",
                "threshold_based_similarity",
                [ ( "Threshold:", "mm.deletion.tbs.threshold" ) ],
                [ tbs_ssc_button ],
                ),
            (
                "Completeness based similarity",
                "completeness_based_similarity",
                [ ( "Offset:", "mm.deletion.cbs.offset" ) ],
                [ cbs_ssc_button ],
                ),
            )

        for ( label, constant, data, extras ) in creation_data:
            g = create_checkbox_controlled_entry_group(
                label = label,
                model = self._model,
                key = "mm.deletion.use",
                constant = constant,
                data = data,
                extras = extras
                )
            param_groups.append( g )

        group = create_group( components = param_groups )
        page.pack_start( group, False, False, 0 )

        # Polishing
        page = create_notebook_page( notebook = notebook, title = "Polishing" )
        param_groups = []
        creation_data = (
            (
                "Remove short segments",
                "remove_short",
                [ ( "Minimum length", "mm.polishing.rs.minimum_length" ) ],
                [],
                ),
            (
                "Keep residues in SS",
                "keep_regular",
                [ ( "Maximum length", "mm.polishing.kr.maximum_length" ) ],
                []
                ),
            )

        for ( label, constant, data, extras ) in creation_data:
            g = create_checkbox_controlled_entry_group(
                label = label,
                model = self._model,
                key = "mm.polishing.use",
                constant = constant,
                data = data,
                extras = extras
                )
            param_groups.append( g )

        group = create_group( components = param_groups )
        page.pack_start( group, False, False, 0 )

        # Sidechain page
        page = create_notebook_page( notebook = notebook, title = "Sidechain" )
        sim_ssc_button = gtk.Button( "Calculation..." )
        sim_ssc_button.connect(
            "clicked",
            on_similarity_calculation,
            get_similarity_settings( model = self._model, root = "mm.pruning.sim.calc" )
            )

        param_groups = []
        creation_data = (
            (
                "Schwarzenbacher",
                "schwarzenbacher",
                [ ( "Level", "mm.pruning.sch.pruning_level" ) ],
                [],
                ),
            (
                "Similarity",
                "similarity",
                [
                    ( "Level", "mm.pruning.sim.pruning_level" ),
                    ( "Lower", "mm.pruning.sim.full_truncation_limit" ),
                    ( "Higher", "mm.pruning.sim.full_length_limit" ),
                    ],
                [ sim_ssc_button ],
                ),
            )

        for ( label, constant, data, extras ) in creation_data:
            g = create_checkbox_controlled_entry_group(
                label = label,
                model = self._model,
                key = "mm.pruning.use",
                constant = constant,
                data = data,
                extras = extras
                )
            param_groups.append( g )

        group = create_group( components = param_groups )
        page.pack_start( group, False, False, 0 )

        # Bfactor page
        page = create_notebook_page( notebook = notebook, title = "Bfactor" )
        bsim_ssc_button = gtk.Button( "Calculation..." )
        bsim_ssc_button.connect(
            "clicked",
            on_similarity_calculation,
            get_similarity_settings( model = self._model, root = "mm.bfactor.sim.calc" )
            )

        param_groups = []
        creation_data = (
            (
                "Original",
                "original",
                [ ( "Factor", "mm.bfactor.original.factor" ) ],
                [],
                ),
            (
                "Similarity",
                "similarity",
                [ ( "Factor", "mm.bfactor.sim.factor" ) ],
                [ bsim_ssc_button, ],
                ),
            (
                "ASA",
                "asa",
                [ ( "Factor", "mm.bfactor.asa.factor" ) ],
                [],
                ),
            )

        for ( label, constant, data, extras ) in creation_data:
            g = create_checkbox_controlled_entry_group(
                label = label,
                model = self._model,
                key = "mm.bfactor.use",
                constant = constant,
                data = data,
                extras = extras
                )
            param_groups.append( g )

        group = create_group( components = param_groups )
        page.pack_start( group, False, False, 0 )

        # Bfactor page
        page = create_notebook_page( notebook = notebook, title = "Completion" )
        completers = [ ( "Add missing Cbeta", "cbeta" ) ]

        for ( label, constant ) in completers:
            button = gtk.CheckButton( label )
            connect_multi_selection_checkbox(
                widget = button,
                setting = self._model.get( key = "mm.completion.use" ),
                constant = constant
                )
            page.pack_start( button, False, False, 20 )

        # View changing
        self.views = create_radio_button_group(
            titles = [
                "Residue deletion",
                "Atom deletion",
                "Atom additions",
                "Atom b-factors"
                ]
            )
        methods = [
            "highlight_residue_deletions_mode",
            "highlight_atom_deletions_mode",
            "highlight_atom_additions_mode",
            "highlight_atom_bfactors_mode",
            ]

        for ( w, m ) in zip( self.views, methods ):
            w.connect( "toggled", self.on_view_change, m )
            w.set_sensitive( False )

        input_box.pack_start(
            create_framed_group( title = "View", components = self.views )
            )
        self.views[0].set_active( True )

        self.window.show_all()

    # Button events
    def delete_event(self, widget, event, data):

        return False


    def destroy(self, widget, data):

        self._model.cleanup()

        if self._preview:
            self._preview.close()


    # Actions
    def on_preview(self, widget, data):

        # Close existing preview
        if self._preview:
            self._preview.close()
            self._preview = None
            self.create_button.set_sensitive( False )

            for w in self.views:
                w.set_sensitive( False )

            self.views[0].set_active( True )

        # Run sculptor to get blueprint
        try:
            blueprint = self._model.get_coot_output()

        except RuntimeError, e:
            info_dialog( "Operation did not succeed: %s" % e )
            return

        # Display blueprint
        try:
            self._preview = CootModel( blueprint = blueprint )

        except RuntimeError, e:
            info_dialog( "Could not create view: %s" % e )
            return

        self.create_button.set_sensitive( True )

        for w in self.views:
            w.set_sensitive( True )

        self.views[0].set_active( True )
        self.select_and_toggle_residue()


    def do_toggle_residue(self, *args):

        if not self._preview:
            return


        assert len( args ) == 1

        if len( args[0] ) == 7:
            ( model, imol, chain, resseq, icode, atom_name, altloc ) = args[0]

        elif len( args[0] ) == 6:
            ( imol, chain, resseq, icode, atom_name, altloc ) = args[0]

        else:
            # raise would just kill the thread
            return

        if self._preview.imol != imol:
            self.select_and_toggle_residue()
            return

        self._preview.toggle_residue( chain = chain, resseq = resseq, icode = icode )
        self._preview.actualize()
        self.select_and_toggle_residue()


    def select_and_toggle_residue(self):

        user_defined_click( 1, self.do_toggle_residue )


    def on_view_change(self, widget, data):

        if not widget.get_active():
            return

        if not self._preview:
            return

        getattr( self._preview, data )()


    def on_create(self, widget, data):

        assert self._preview

        try:
            self._preview.snapshot()

        except RuntimeError, e:
            info_dialog( "Error: %s" % e )


    def main(self):

        gtk.main()


class PhilList(object):
    """
    List with Phil-like printing behaviour
    """

    def __init__(self, allowed, initial = []):

        self.allowed = allowed
        self.current = set()

        for i in initial:
            self.set( i )


    def set(self, value):

        if value not in self.allowed:
            raise ValueError, "Unknown value: %s" % value

        self.current.add( value )


    def unset(self, value):

        self.current.discard( value )


    def __str__(self):

        if self.current:
            return "+".join( str( e ) for e in self.current )

        else:
            return "None"


class PhilChoice(object):
    """
    A constant from predefined choices
    """

    def __init__(self, allowed, initial = None):

        self.allowed = allowed

        if initial is None:
            initial = iter( allowed ).next()

        self.set( value = initial )


    def set(self, value):

        if value not in self.allowed:
            raise ValueError, "Unknown value: %s" % value

        self.value = value


    def index(self):

        return self.allowed.index( self.value )


    def copy(self):

        return self.__class__( allowed = self.allowed, initial = self.value )


    def __str__(self):

        return str( self.value )


class PhilBool(object):
    """
    A value with a default
    """

    allowed = [ True, False ]

    def __init__(self, is_set):

        self.value = bool( is_set )


    def set(self):

        self.value = True


    def unset(self):

        self.value = False


    def __str__(self):

        return str( self.value )


class PhilValue(object):
    """
    A value with a conversion
    """

    def __init__(self, to_value, to_string, initial):

        self.to_value = to_value
        self.to_string = to_string
        self.set( value = initial )


    def set(self, value):

        self.value = self.to_value( value )


    def copy(self):

        return self.__class__(
            to_value = self.to_value,
            to_string = self.to_string,
            initial = self.value
            )


    def __str__(self):

        return self.to_string( self.value )


    @classmethod
    def Integer(cls, initial):

        return cls( to_value = to_integer, to_string = to_string, initial = initial )


    @classmethod
    def Float(cls, initial):

        return cls( to_value = to_float, to_string = to_string, initial = initial )


    @classmethod
    def String(cls, initial):

        return cls( to_value = to_value_or_none, to_string = to_string, initial = initial )


class PhilModel(object):
    """
    A model from coot's model list
    """

    def __init__(self):

        self.update()


    def update(self):

        self.imols = model_molecule_list()
        self.allowed = [
            os.path.basename( molecule_name( imol ) ) for imol in self.imols
            ]
        self.value = None


    def set(self, value):

        if value is None:
            self.value = None

        else:
            assert value in self.allowed
            self.value = value


    def imol(self):

        index = self.index()

        if index != -1:
            return self.imols[ index ]

        else:
            return -1


    def index(self):

        return self.allowed.index( self.value ) if self.value is not None else -1


    def __str__(self):

        imol = self.imol()
        return molecule_name( imol ) if is_valid_model_molecule( imol ) else ""


class PhilChain(object):
    """
    A chain for a coot model
    """

    def __init__(self, model):

        self.model = model
        self.update()


    def update(self):

        imol = self.model.imol()
        self.allowed = []
        self.chains = []

        if is_valid_model_molecule( imol ):
            for chain in chain_ids( imol ):
                if not is_solvent_chain_qm( imol, chain ):
                    self.allowed.append( chain if chain else "<blank>" )
                    self.chains.append( chain )

        self.value = None


    def set(self, value):

        if value is None:
            self.value = None

        else:
            assert value in self.allowed
            self.value = value


    def index(self):

        return self.allowed.index( self.value ) if self.value is not None else -1


    def __str__(self):

        index = self.index()
        return "" if index == -1 else "%s" % self.chains[ index ]


class SculptorModel(object):
    """
    Application layer
    """

    SCULPTOR_EXE = "phenix.sculptor"
    MATRICES = [ "blosum50", "blosum62", "dayhoff", "identity" ]
    WEIGHTING_SCHEMES = [ "uniform", "triangular" ]

    DELETION = [ "gap", "threshold_based_similarity", "completeness_based_similarity" ]
    POLISHING = [ "remove_short", "keep_regular" ]
    SIDECHAIN = [ "schwarzenbacher", "similarity" ]
    BFACTOR = [ "original", "similarity", "asa" ]


    PHIL_PROTOCOL = """
  deletion {
    use = %(mm.deletion.use)s
    completeness_based_similarity {
      offset = %(mm.deletion.cbs.offset)s
      calculation {
        matrix = %(mm.deletion.cbs.calc.matrix)s
        window = %(mm.deletion.cbs.calc.window)s
        weighting = %(mm.deletion.cbs.calc.weighting)s
      }
    }
    threshold_based_similarity {
      threshold = %(mm.deletion.tbs.threshold)s
      calculation {
        matrix = %(mm.deletion.tbs.calc.matrix)s
        window = %(mm.deletion.tbs.calc.window)s
        weighting = %(mm.deletion.tbs.calc.weighting)s
      }
    }
  }

  polishing {
    use = %(mm.polishing.use)s
    remove_short {
      minimum_length = %(mm.polishing.rs.minimum_length)s
    }
    keep_regular {
      maximum_length = %(mm.polishing.kr.maximum_length)s
    }
  }

  pruning {
    use = %(mm.pruning.use)s
    schwarzenbacher {
      pruning_level = %(mm.pruning.sch.pruning_level)s
    }
    similarity {
      pruning_level = %(mm.pruning.sim.pruning_level)s
      full_length_limit = %(mm.pruning.sim.full_length_limit)s
      full_truncation_limit = %(mm.pruning.sim.full_truncation_limit)s
      calculation {
        matrix = %(mm.pruning.sim.calc.matrix)s
        window = %(mm.pruning.sim.calc.window)s
        weighting = %(mm.pruning.sim.calc.weighting)s
      }
    }
  }

  bfactor {
    use = %(mm.bfactor.use)s
    minimum = %(mm.bfactor.minimum)s
    asa {
      factor = %(mm.bfactor.asa.factor)s
      precision = 960
      probe_radius = 1.4
    }
    original {
      factor = %(mm.bfactor.original.factor)s
    }
    similarity {
      factor = %(mm.bfactor.sim.factor)s
      calculation {
        matrix = %(mm.bfactor.sim.calc.matrix)s
        window = %(mm.bfactor.sim.calc.window)s
        weighting = %(mm.bfactor.sim.calc.weighting)s
      }
    }
  }

  completion = %(mm.completion.use)s
  """

    PHIL_MULTIPROTOCOLS = """
protocols = %(mm.protocols)s
"""

    PHIL_HEAD = \
"""input {
  model {
    file_name = %(input.model.file_name)s
    selection = all
    remove_alternate_conformations = False
  }
  alignment {
    file_name = %(input.alignment.file_name)s
    target_index = 1
  }
}

output {
  root = %(output.root)s
  folder = %(output.folder)s
  format = coot
}

macromolecule {"""


    PHIL_TAIL = """
  renumber {
    use = %(mm.renumber.use)s
    start = 1
  }
  rename = %(mm.rename.use)s
}
hetero = %(hetero)s
min_hssp_length = 6
min_matching_fraction = 0.4
"""

    PHIL_SINGLE_PROTOCOL = PHIL_HEAD + PHIL_PROTOCOL + PHIL_TAIL
    PHIL_MULTI_PROTOCOL = PHIL_HEAD + PHIL_MULTIPROTOCOLS + PHIL_TAIL

    def __init__(self):

        self._preview = None
        self._tmp_dir = tempfile.mkdtemp()

        self._settings_for = {}
        self._settings_for[ "multiprotocol" ] = PhilChoice(
            allowed = [ True, False ],
            initial = False
            )
        self._settings_for[ "mm.protocols" ] = PhilChoice(
            allowed = [ str( i ) for i in range( 1, 14 ) ]
            )
        self._settings_for[ "input.model.file_name" ] = PhilModel()
        self._settings_for[ "input.model.selection" ] = PhilChain(
            model = self._settings_for[ "input.model.file_name" ]
            )
        self._settings_for[ "input.alignment.file_name" ] = PhilValue.String( None )
        self._settings_for[ "output.folder" ] = self._tmp_dir
        self._settings_for[ "output.root" ] = "sculptor"

        self._settings_for[ "mm.deletion.use" ] = PhilList(
            allowed = self.DELETION,
            initial = [ self.DELETION[0] ]
            )
        self._settings_for[ "mm.deletion.tbs.threshold" ] = PhilValue.Float(-0.2 )
        self._settings_for[ "mm.deletion.tbs.calc.matrix" ] = PhilChoice(
            allowed = self.MATRICES,
            initial = self.MATRICES[0]
            )
        self._settings_for[ "mm.deletion.tbs.calc.window" ] = PhilValue.Integer( 5 )
        self._settings_for[ "mm.deletion.tbs.calc.weighting" ] = PhilChoice(
            allowed = self.WEIGHTING_SCHEMES,
            initial = self.WEIGHTING_SCHEMES[0]
            )
        self._settings_for[ "mm.deletion.cbs.offset" ] = PhilValue.Float( 0.0 )
        self._settings_for[ "mm.deletion.cbs.calc.matrix" ] = PhilChoice(
            allowed = self.MATRICES,
            initial = self.MATRICES[0]
            )
        self._settings_for[ "mm.deletion.cbs.calc.window" ] = PhilValue.Integer( 5 )
        self._settings_for[ "mm.deletion.cbs.calc.weighting" ] = PhilChoice(
            allowed = self.WEIGHTING_SCHEMES,
            initial = self.WEIGHTING_SCHEMES[0]
            )

        self._settings_for[ "mm.polishing.use" ] = PhilList(
            allowed = self.POLISHING,
            initial = []
            )
        self._settings_for[ "mm.polishing.rs.minimum_length" ] = PhilValue.Integer( 3 )
        self._settings_for[ "mm.polishing.kr.maximum_length" ] = PhilValue.Integer( 1 )

        self._settings_for[ "mm.pruning.use" ] = PhilList(
            allowed = self.SIDECHAIN,
            initial = [ self.SIDECHAIN[0] ]
            )
        self._settings_for[ "mm.pruning.sch.pruning_level" ] = PhilValue.Integer( 2 )
        self._settings_for[ "mm.pruning.sim.pruning_level" ] = PhilValue.Integer( 2 )
        self._settings_for[ "mm.pruning.sim.full_length_limit" ] = PhilValue.Float( 0.5 )
        self._settings_for[ "mm.pruning.sim.full_truncation_limit" ] = PhilValue.Float( -0.6 )
        self._settings_for[ "mm.pruning.sim.calc.matrix" ] = PhilChoice(
            allowed = self.MATRICES,
            initial = self.MATRICES[0]
            )
        self._settings_for[ "mm.pruning.sim.calc.window" ] = PhilValue.Integer( 5 )
        self._settings_for[ "mm.pruning.sim.calc.weighting" ] = PhilChoice(
            allowed = self.WEIGHTING_SCHEMES,
            initial = self.WEIGHTING_SCHEMES[0]
            )

        self._settings_for[ "mm.bfactor.use" ] = PhilList(
            allowed = self.BFACTOR,
            initial = []
            )
        self._settings_for[ "mm.bfactor.minimum" ] = PhilValue.Float( 1 )
        self._settings_for[ "mm.bfactor.original.factor" ] = PhilValue.Float( 1.0 )
        self._settings_for[ "mm.bfactor.asa.factor" ] = PhilValue.Float( 2.0 )
        self._settings_for[ "mm.bfactor.sim.factor" ] = PhilValue.Float( -100 )
        self._settings_for[ "mm.bfactor.sim.calc.matrix" ] = PhilChoice(
            allowed = self.MATRICES,
            initial = self.MATRICES[0]
            )
        self._settings_for[ "mm.bfactor.sim.calc.window" ] = PhilValue.Integer( 5 )
        self._settings_for[ "mm.bfactor.sim.calc.weighting" ] = PhilChoice(
            allowed = self.WEIGHTING_SCHEMES,
            initial = self.WEIGHTING_SCHEMES[0]
            )

        self._settings_for[ "mm.completion.use" ] = PhilList(
            allowed = [ "cbeta" ],
            initial = []
            )

        self._settings_for[ "mm.renumber.use" ] = PhilChoice(
            allowed = [ "target", "model", "original" ],
            initial = "target"
            )
        self._settings_for[ "mm.rename.use" ] = PhilBool( is_set = True )
        self._settings_for[ "hetero" ] = None


    def set(self, key, value):

        assert key in self._settings_for
        self._settings_for[ key ] = value


    def get(self, key):

        assert key in self._settings_for
        return self._settings_for[ key ]


    def get_coot_output(self):

        # Check whether the files exist
        afile = str( self.get( key = "input.alignment.file_name" ) )

        if afile and not os.path.exists( afile ):
            raise RuntimeError( "Alignment file does not exist" )

        if not afile:
            afile = "None"

        # Get selected chain
        mol_imol = self._settings_for[ "input.model.file_name" ].imol()
        chain_id = str( self._settings_for[ "input.model.selection" ] )
        imol = new_molecule_by_atom_selection( mol_imol, "//%s/" % chain_id )

        if imol == -1:
            raise RuntimeError, "Empty selection for chain %s" % chain_id

        pdb_file = os.path.join( self._tmp_dir, "selection.pdb" )
        assert not os.path.exists( pdb_file )
        write_pdb_file( imol, pdb_file )
        close_molecule( imol )

        # Get sculptor parameters
        settings_for = dict(
            self._settings_for.items()
            + [
                ( "input.model.file_name", pdb_file ),
                ( "input.alignment.file_name", afile ),
                ]
            )
        outroot = "%s_selection" % os.path.join(
            settings_for[ "output.folder" ],
            settings_for[ "output.root" ]
            )

        if settings_for[ "multiprotocol" ].value:
            args = (self.SCULPTOR_EXE, "--stdin", "--mode=predefined" )
            phil = self.PHIL_MULTI_PROTOCOL % settings_for
            outfile = "%s_%s.coot" % ( outroot, settings_for[ "mm.protocols" ] )

        else:
            args = (self.SCULPTOR_EXE, "--stdin" )
            phil = self.PHIL_SINGLE_PROTOCOL % settings_for
            outfile = "%s.coot" % outroot

        if os.path.exists( outfile ):
            os.unlink( outfile )

        try:
            p = subprocess.Popen(
                args,
                stdin = subprocess.PIPE,
                stdout = subprocess.PIPE,
                stderr = subprocess.PIPE,
                shell = ( os.name == "nt" ),
                env = dict( ( key, val ) for ( key, val ) in os.environ.items()
                  if key not in [ "PYTHONHOME" ] ),
                )
            ( output, error ) = p.communicate( input = phil )

        except ( OSError, ValueError ), e:
            raise RuntimeError, "System error: %s" % e

        finally:
            os.unlink( pdb_file )

        print output

        if p.poll():
            raise RuntimeError, "Sculptor error: %s" % error

        assert os.path.exists( outfile )

        try:
            data = eval( open( outfile ).read() )

        except SyntaxError:
            raise RuntimeError, "Unexpected data returned by sculptor"

        return data


    def cleanup(self):

        if self._tmp_dir:
            for f in os.listdir( self._tmp_dir ):
                os.unlink( os.path.join( self._tmp_dir, f ) )

            os.rmdir( self._tmp_dir )
            self._tmp_dir = None


class CootModel(object):
    """
    Controller object for coot
    """

    DELETE_B = 100
    KEEP_B = 0

    def __init__(self, blueprint):

        self.repr = blueprint[0]
        self.moribund_residues = set( blueprint[1] )
        self.moribund_atoms = set( blueprint[2] )
        self.attached_atoms = set( blueprint[3] )

        self.imol = add_molecule( self.repr, "sculptor_preview_molecule" )

        if self.imol == -1:
            raise RuntimeError, "Empty molecule"

        self.pending = []
        self.highlight_residue_deletions_mode()


    def close(self):

        clear_pending_picks()
        close_molecule( self.imol )


    def toggle_residue(self, chain, resseq, icode):

        identifier = ( chain, resseq, icode )

        if identifier in self.moribund_residues:
            self.moribund_residues.remove( identifier )
            self.update_on_resurrection_policy(
                chain = chain,
                resseq = resseq,
                icode = icode
                )

        else:
            self.moribund_residues.add( identifier )
            self.update_on_deletion_policy(
                chain = chain,
                resseq = resseq,
                icode = icode
                )


    def show_residue_resurrection_on_display(self, chain, resseq, icode):

        residue_atoms = residue_info( self.imol, chain, resseq, icode )
        resid = ( chain, resseq, icode )

        for ( ( name, altloc ), properties, coords ) in residue_atoms:
             atid = resid + ( name, altloc )
             value = self.DELETE_B if atid in self.moribund_atoms else self.KEEP_B
             self.pending.append(
                [ self.imol, chain, resseq, icode, name, altloc, "B", value ]
                )


    def show_residue_deletion_on_display(self, chain, resseq, icode):

        residue_atoms = residue_info( self.imol, chain, resseq, icode )
        self.pending.extend(
            [
                [ self.imol, chain, resseq, icode, name, altloc, "B", self.DELETE_B ]
                for ( ( name, altloc ), properties, coords ) in residue_atoms
                ]
            )


    def no_display_update(self, chain, resseq, icode):

        pass


    def highlight_residue_deletions_mode(self):

        representation = copy.deepcopy( self.repr )

        for model in representation:
            for chain in model:
                cid = chain[0]
                for ( resseq, icode, resname, atoms ) in chain[1]:
                    if ( cid, resseq, icode ) in self.moribund_residues:
                        bfac = self.DELETE_B

                    else:
                        bfac = self.KEEP_B

                    for atr in atoms:
                        atr[1][1] = bfac

        clear_and_update_molecule( self.imol, representation )
        graphics_to_b_factor_cas_representation( self.imol )
        self.update_on_deletion_policy = self.show_residue_deletion_on_display
        self.update_on_resurrection_policy = self.show_residue_resurrection_on_display


    def highlight_atom_deletions_mode(self):

        representation = copy.deepcopy( self.repr )

        for model in representation:
            for chain in model:
                cid = chain[0]
                for ( resseq, icode, resname, atoms ) in chain[1]:
                    resid = ( cid, resseq, icode )

                    for atr in atoms:
                        atid = resid + ( atr[0][0], atr[0][1] )
                        if resid in self.moribund_residues or atid in self.moribund_atoms:
                            atr[1][1] = self.DELETE_B

                        else:
                            atr[1][1] = self.KEEP_B

        clear_and_update_molecule( self.imol, representation )
        graphics_to_b_factor_representation( self.imol )
        self.update_on_deletion_policy = self.show_residue_deletion_on_display
        self.update_on_resurrection_policy = self.show_residue_resurrection_on_display


    def highlight_atom_additions_mode(self):

        representation = copy.deepcopy( self.repr )

        for model in representation:
            for chain in model:
                cid = chain[0]
                for ( resseq, icode, resname, atoms ) in chain[1]:
                    resid = ( cid, resseq, icode )

                    for atr in atoms:
                        atid = resid + ( atr[0][0], atr[0][1] )

                        if atid in self.attached_atoms:
                            atr[1][1] = self.DELETE_B

                        else:
                            atr[1][1] = self.KEEP_B

        clear_and_update_molecule( self.imol, representation )
        graphics_to_b_factor_representation( self.imol )
        self.update_on_deletion_policy = self.no_display_update
        self.update_on_resurrection_policy = self.no_display_update


    def highlight_atom_bfactors_mode(self):

        clear_and_update_molecule( self.imol, self.repr )
        graphics_to_b_factor_representation( self.imol )
        self.update_on_deletion_policy = self.no_display_update
        self.update_on_resurrection_policy = self.no_display_update


    def snapshot(self):

        representation = []

        for model in self.repr:
            copy_model = []

            for chain in model:
                copy_chain = []
                cid = chain[0]

                for ( resseq, icode, resname, atoms ) in chain[1]:
                    resid = ( cid, resseq, icode )

                    if resid in self.moribund_residues:
                        continue

                    copy_atoms = [ a for a in atoms
                        if ( resid + ( a[0][0], a[0][1] ) ) not in self.moribund_atoms ]

                    if copy_atoms:
                        copy_chain.append( [ resseq, icode, resname, copy_atoms ] )

                if copy_chain:
                    copy_model.append( [ cid, copy_chain ] )

            if copy_model:
                representation.append( copy_model )

        imol = add_molecule( representation, "snapshot" )

        if imol == -1:
            raise RuntimeError, "Empty molecule"

        return imol


    def actualize(self):

        set_atom_attributes( self.pending )
        self.pending = []

        if graphics_molecule_bond_type( self.imol ) == 14:
            graphics_to_ca_representation( self.imol )
            graphics_to_b_factor_cas_representation( self.imol )

        else:
            graphics_draw()



def command_in_path(cmd):
    import string
    file_ext = "." + file_name_extension(cmd)
    executables = os.environ.get("PATHEXT", "").split(os.pathsep)
    if ((file_ext not in executables) ):
        for ext in executables:
            cmdxt = cmd + ext
            # we shall check for full path names first
            if os.path.exists(cmdxt):
                return True
            else:
                try:
                   primary_path = os.environ["PATH"]
                   for path in string.split(primary_path, os.pathsep):
                       program = os.path.join(path,cmd)
                       #print "BL DEBUG:: program_exe is", program_exe
                       if (os.path.exists(program)):
                           return True
                       program_exe = os.path.join(path,cmdxt)
                       if (os.path.exists(program_exe)):
                           return True
                except:
                    print "BL WARNING:: couldnt open $PATH"  # this shouldnt happen
        return False


if (os.name != 'nt'):
    ret = find_exe( SculptorModel.SCULPTOR_EXE, "PATH" )
else:
    ret = command_in_path( SculptorModel.SCULPTOR_EXE )

if ret:
    gui = SculptorView()
    #gui.main()
else:
    info_dialog( "Cannot find %s in the path" % SculptorModel.SCULPTOR_EXE )
