# coding=UTF-8
# ex:ts=4:sw=4:et=on

# Copyright (c) 2013, Mathijs Dumon
# All rights reserved.
# Complete license can be found in the LICENSE file.

import logging
from pyxrd.generic.gtk_tools.utils import run_when_idle
logger = logging.getLogger(__name__)
import gtk, gobject
import sys

from mvc import Controller, Observer

from pyxrd.generic.views.treeview_tools import new_text_column, new_pb_column, new_toggle_column
from pyxrd.generic.mathtext_support import create_pb_from_mathtext
from pyxrd.generic.controllers import DialogController, BaseController, ObjectListStoreController

from pyxrd.refinement.parspace import ParameterSpaceGenerator
from pyxrd.refinement.refine_context import RefineSetupError
from pyxrd.mixture.models import Mixture
from pyxrd.mixture.views import EditMixtureView, RefinementView, RefinementResultView
from pyxrd.generic.controllers.objectliststore_controllers import wrap_list_property_to_treemodel
from contextlib import contextmanager

class RefinementResultsController(DialogController):
    """
        A controller for a RefinementContext object that keeps track
        of the solutions and residuals generated by the refinement
        algorithm. This allows to show a nice dialog with the end
        results and some graphs about the parameter space.
    """

    register_lazy = False
    auto_adapt = True
    auto_adapt_included = [
        "initial_residual",
        "last_residual",
        "best_residual"
    ]
    solutions = None

    # ------------------------------------------------------------
    #      Initialisation and other internals
    # ------------------------------------------------------------
    def __init__(self, *args, **kwargs):
        super(RefinementResultsController, self).__init__(*args, **kwargs)
        self.parspace_gen = ParameterSpaceGenerator()

        density = 199
        for var, val in self.model.options.iteritems():
            if var == "num_samples":
                density = val

        self.parspace_gen.initialize(self.model.ranges, density)

    # ------------------------------------------------------------
    #      Notifications of observable properties
    # ------------------------------------------------------------
    @Controller.observe("solution_added", signal=True)
    def notif_solution_added(self, model, prop_name, info):
        if self.model.mixture.refiner.make_psp_plots:
            new_solution, new_residual = info.arg
            self.parspace_gen.record(new_solution, new_residual)

    # ------------------------------------------------------------
    #      GTK Signal handlers
    # ------------------------------------------------------------
    def on_cancel(self):
        self.view.hide()
        self.parent.parent.view.parent.show()

    def on_btn_initial_clicked(self, event):
        self.model.apply_initial_solution()
        self.model.mixture.refiner.delete_context()
        self.on_cancel()
        return True

    def on_btn_best_clicked(self, event):
        self.model.apply_best_solution()
        self.model.mixture.refiner.delete_context()
        self.on_cancel()
        return True

    def on_btn_last_clicked(self, event):
        self.model.apply_last_solution()
        self.model.mixture.refiner.delete_context()
        self.on_cancel()
        return True

    # ------------------------------------------------------------
    #      Methods & Functions
    # ------------------------------------------------------------
    def generate_images(self, output_dir="", density=200):
        """
            Generate the parameter space plots
        """
        self.parspace_gen.plot_images(
            figure=self.view.figure,
            centroid=self.model.best_solution,
            labels=[ref_prop.title for ref_prop in self.model.ref_props],
            density=density
        )

    def clear_images(self):
        self.parspace_gen.clear_image(figure=self.view.figure)

    pass # end of class

class RefinementController(DialogController):

    auto_adapt_included = [
        "refine_method",
        "refinables",
        "make_psp_plots",
    ]

    gui_timeout_id = None

    @property
    def treemodel(self):
        return self.model.refinables

    def setup_refinables_tree_view(self, store, widget):
        """
            Setup refinables TreeView layout
        """
        widget.set_show_expanders(True)

        if sys.platform == "win32":
            def get_label(column, cell, model, itr, user_data=None):
                ref_prop = model.get_user_data(itr)
                cell.set_property("text", ref_prop.get_text_title())
                return
            widget.append_column(new_text_column('Name/Prop', xalign=0.0, data_func=get_label))
        else:
            # Labels are parsed for mathtext markup into pb's:
            def get_pb(column, cell, model, itr, user_data=None):
                ref_prop = model.get_user_data(itr)
                try:
                    if not hasattr(ref_prop, "pb") or not ref_prop.pb:
                        ref_prop.pb = create_pb_from_mathtext(
                            ref_prop.title,
                            align='left',
                            weight='medium'
                        )
                    cell.set_property("pixbuf", ref_prop.pb)
                except RuntimeError:
                    logger.warning("An error occured when trying to convert a property title to a PixBuf")
                    raise
                return
            widget.append_column(new_pb_column('Name/Prop', xalign=0.0, data_func=get_pb))

        # Editable floats:
        def get_value(column, cell, model, itr, *args):
            col = column.get_col_attr('markup')
            try:
                value = model.get_value(itr, col)
                value = "%.5f" % value
            except TypeError: value = ""
            cell.set_property("markup", value)
            return
        def on_float_edited(rend, path, new_text, model, col):
            itr = model.get_iter(path)
            try:
                model.set_value(itr, col, float(new_text))
            except ValueError:
                return False
            return True

        def_float_args = {
            "sensitive_col": store.c_refinable,
            "editable_col": store.c_refinable,
            "visible_col": store.c_refinable,
            "data_func": get_value
        }

        widget.append_column(new_text_column(
            "Value", markup_col=store.c_value,
            edited_callback=(
                on_float_edited,
                (store, store.c_value,)
            ), **def_float_args
        ))
        widget.append_column(new_text_column(
            "Min", markup_col=store.c_value_min,
            edited_callback=(
                on_float_edited,
                (store, store.c_value_min,)
            ), **def_float_args
        ))
        widget.append_column(new_text_column(
            "Max", markup_col=store.c_value_max,
            edited_callback=(
                on_float_edited,
                (store, store.c_value_max,)
            ), **def_float_args
        ))

        # The 'refine' checkbox:
        widget.append_column(new_toggle_column(
            "Refine",
            toggled_callback=(self.refine_toggled, (store,)),
            resizable=False,
            expand=False,
            active_col=store.c_refine,
            sensitive_col=store.c_refinable,
            activatable_col=store.c_refinable,
            visible_col=store.c_refinable
        ))

    def _update_method_options_store(self):
        """
            Update the method options tree store (when a new method is selected)
        """
        tv = self.view['tv_method_options']
        store = gtk.ListStore(str)
        for arg in self.model.refine_options.keys():
            store.append([arg])
        tv.set_model(store)
        return tv

    def _setup_method_options_treeview(self):
        """
            Initial method options tree view layout & behavior setup
        """
        # Update the method options store to match the currently selected
        # refinement method
        tv = self._update_method_options_store()

        # The name of the option:

        def get_name(column, cell, model, itr, *args):
            arg, = tv.get_model().get(itr, 0)
            cell.set_property(
                "markup", self.model.get_refinement_option(arg).description)

        tv.append_column(new_text_column("Name", text_col=0, data_func=get_name))

        # The value of the option:
        def get_value(column, cell, model, itr, *args):
            arg, = tv.get_model().get(itr, 0)
            cell.set_property("sensitive", True)
            cell.set_property("editable", True)
            cell.set_property("markup", self.model.get_refinement_option_value(arg))
            return
        def on_value_edited(rend, path, new_text, col):
            store = tv.get_model()
            itr = store.get_iter(path)
            arg, = store.get(itr, 0)
            try:
                self.model.set_refinement_option_value(arg, new_text)
            except ValueError:
                pass
            return True
        tv.append_column(new_text_column(
            "Value", text_col=0,
            data_func=get_value,
            edited_callback=(on_value_edited, (0,)),
        ))

    def register_view(self, view):
        # Create the method treeview:
        self._setup_method_options_treeview()
        # Connect the cancel button (custom widget):
        view.connect_cancel_request(self._on_refinement_cancelled)

    def cleanup(self):
        if hasattr(self, "view"):
            del self.view
        if hasattr(self, "results_view"):
            del self.results_view
        if hasattr(self, "results_controller"):
            del self.results_controller
        if hasattr(self, "model"):
            self.relieve_model(self.model)



    # ------------------------------------------------------------
    #      Notifications of observable properties
    # ------------------------------------------------------------
    @Observer.observe("refine_method", assign=True)
    def on_prop_changed(self, model, prop_name, info):
        self._update_method_options_store()

    # ------------------------------------------------------------
    #      GTK Signal handlers
    # ------------------------------------------------------------
    def on_cancel(self):
        if self.view is not None:
            self.view.hide()
            self.parent.view.parent.show()

    def refine_toggled(self, cell, path, model):
        if model is not None:
            itr = model.get_iter(path)
            model.set_value(itr, model.c_refine, not cell.get_active())
        return True

    def on_btn_randomize_clicked(self, event):
        self.model.randomize()

    def on_auto_restrict_clicked(self, event):
        self.model.auto_restrict()

    @DialogController.status_message("Refining mixture...", "refine_mixture")
    def on_refine_clicked(self, event):
        with self.model.mixture.needs_update.hold():
            with self.model.mixture.data_changed.hold():
                if len(self.model.mixture.specimens) > 0:

                    # Create the refinement context
                    try:
                        self.model.setup_context(store=True)
                    except RefineSetupError, msg:
                        self.run_information_dialog("There was an error when creating the refinement setup:\n%s" % msg, parent=self.view.get_toplevel())
                        return

                    # Setup results controller, this needs to be done prior to running
                    # the refinement to allow for solutions to be recorded!
                    self.results_view = RefinementResultView(parent=self.view.parent)
                    self.results_controller = RefinementResultsController(
                        model=self.model.context,
                        view=self.results_view,
                        parent=self
                    )

                    # Launch a timeout GUI updater:
                    self.gui_timeout_id = gobject.timeout_add(250, self._on_update_gui)

                    # Run the refinement, threaded:
                    self.model.refine(
                        threaded=True,
                        on_complete=self._on_refinement_complete)

                    # Show the context updates in the gui:
                    self.view.show_refinement_info()
                    self.view.start_spinner()

                else:
                    self.run_information_dialog("Cannot refine an empty mixture!", parent=self.view.get_toplevel())

    @run_when_idle
    def _on_refinement_complete(self, context, *args, **kwargs):
        """ Called when the refinement is completed """
        gobject.source_remove(self.gui_timeout_id)
        self.view.stop_spinner()
        self.view.update_refinement_status("Processing...")
        if self.model.make_psp_plots:
            self.model.context.status_message = "Generating parameter space plots..."
            self.results_controller.generate_images()
        self.view.hide_refinement_info()

        self.view.hide()
        self.results_view.present()

    def _on_refinement_cancelled(self, context, *args, **kwargs):
        """ Called when the refinement is cancelled by the user """
        self.view.stop_spinner()
        self.view.update_refinement_status("Cancelling...")
        self.model.cancel()
        self.view.hide_refinement_info()

    def _on_update_gui(self):
        """ Called using a gobject timeout """
        self.view.update_refinement_info(
            self.model.context.last_residual,
            self.model.context.status_message
        )
        return True

    pass # end of class

class EditMixtureController(BaseController):

    auto_adapt_excluded = [
        "refine_method",
        "refinables",
        "make_psp_plots"
    ]

    ref_view = None

    @property
    def specimens_treemodel(self):
        if self.model.project is not None:
            prop = self.model.project.Meta.get_prop_intel_by_name("specimens")
            return wrap_list_property_to_treemodel(self.model.project, prop)
        else:
            return None

    @property
    def phases_treemodel(self):
        if self.model.project is not None:
            prop = self.model.project.Meta.get_prop_intel_by_name("phases")
            return wrap_list_property_to_treemodel(self.model.project, prop)
        else:
            return None

    def register_adapters(self):
        self.create_ui()

    def create_ui(self):
        """
            Creates a complete new UI for the Mixture model
        """
        self.view.reset_view()
        for index in range(len(self.model.phases)):
            self._add_phase_view(index)
        for index in range(len(self.model.specimens)):
            self._add_specimen_view(index)

    def _add_phase_view(self, phase_slot):
        """
            Adds a new view for the given phase slot.
        """
        def on_label_changed(editable):
            self.model.phases[phase_slot] = editable.get_text()

        def on_fraction_changed(editable):
            try: self.model.fractions[phase_slot] = float(editable.get_text())
            except ValueError: return # ignore ValueErrors

        def on_phase_delete(widget):
            self.model.del_phase_slot(phase_slot)
            widget.disconnect(widget.get_data("deleventid"))

        self.view.add_phase_slot(self.phases_treemodel,
            on_phase_delete, on_label_changed, on_fraction_changed,
            self.on_combo_changed, label=self.model.phases[phase_slot],
            fraction=self.model.fractions[phase_slot], phases=self.model.phase_matrix)

    def _add_specimen_view(self, specimen_slot):
        """
            Adds a new view for the given specimen slot
        """
        def on_scale_changed(editable):
            try: self.model.scales[specimen_slot] = float(editable.get_text())
            except ValueError: return # ignore ValueErrors

        def on_bgs_changed(editable):
            try: self.model.bgshifts[specimen_slot] = float(editable.get_text())
            except ValueError: return # ignore ValueErrors

        def on_specimen_changed(combobox):
            itr = combobox.get_active_iter()
            specimen = self.specimens_treemodel.get_user_data(itr) if itr is not None else None
            self.model.set_specimen(specimen_slot, specimen)

        def on_specimen_delete(widget):
            self.model.del_specimen_slot(specimen_slot)
            widget.disconnect(widget.get_data("deleventid"))

        self.view.add_specimen_slot(self.phases_treemodel,
            self.specimens_treemodel, on_specimen_delete, on_scale_changed,
            on_bgs_changed, on_specimen_changed, self.on_combo_changed,
            scale=self.model.scales[specimen_slot], bgs=self.model.bgshifts[specimen_slot],
            specimen=self.model.specimens[specimen_slot], phases=self.model.phase_matrix)

    # ------------------------------------------------------------
    #      Notifications of observable properties
    # ------------------------------------------------------------
    @Controller.observe("data_changed", signal=True)
    def notif_has_changed(self, model, prop_name, info):
        self.view.update_all(self.model.fractions, self.model.scales, self.model.bgshifts)

    @Controller.observe("needs_reset", signal=True)
    def notif_needs_reset(self, model, prop_name, info):
        self.create_ui()

    # ------------------------------------------------------------
    #      GTK Signal handlers
    # ------------------------------------------------------------
    def on_combo_changed(self, combobox, row, col):
        itr = combobox.get_active_iter()
        phase = self.phases_treemodel.get_user_data(itr) if itr is not None else None
        self.model.set_phase(row, col, phase)

    def on_add_phase(self, widget, *args):
        with self.model.data_changed.hold():
            index = self.model.add_phase_slot("New Phase", 1.0)
            self._add_phase_view(index)

    def on_add_specimen(self, widget, *args):
        with self.model.data_changed.hold():
            index = self.model.add_specimen_slot(None, 1.0, 0.0)
            self._add_specimen_view(index)

    def on_add_both(self, widget, *args):
        with self.model.data_changed.hold():
            self.on_add_specimen(widget, *args)
            self.on_add_phase(widget, *args)

    def on_optimize_clicked(self, widget, *args):
        self.model.optimize()

    def on_refine_clicked(self, widget, *args):
        self.model.refiner.update_refinement_treestore()
        if self.ref_view is not None:
            self.ref_view.hide()
            self.ref_ctrl.cleanup()
        self.view.parent.hide()
        self.ref_view = RefinementView(parent=self.parent.view)
        self.ref_ctrl = RefinementController(model=self.model.refiner, view=self.ref_view, parent=self)
        self.ref_view.present()

    def on_composition_clicked(self, widget, *args):
        comp = "The composition of the specimens in this mixture:\n\n\n"
        comp += "<span font-family=\"monospace\">"
        # get the composition matrix (first columns contains strings with elements, others are specimen compositions)
        import re
        for row in self.model.get_composition_matrix():
            comp += "%s %s\n" % (re.sub(r'(\d+)', r'<sub>\1</sub>', row[0]), " ".join(row[1:]))
        comp += "</span>"
        self.run_information_dialog(comp, parent=self.view.get_toplevel())

    pass # end of class

class MixturesController(ObjectListStoreController):

    treemodel_property_name = "mixtures"
    treemodel_class_type = Mixture
    columns = [ ("Mixture name", "c_name") ]
    delete_msg = "Deleting a mixture is irreverisble!\nAre You sure you want to continue?"
    obj_type_map = [
        (Mixture, EditMixtureView, EditMixtureController),
    ]

    def get_mixtures_tree_model(self, *args):
        return self.treemodel

    # ------------------------------------------------------------
    #      GTK Signal handlers
    # ------------------------------------------------------------
    def on_load_object_clicked(self, event):
        pass # cannot load mixtures
    def on_save_object_clicked(self, event):
        pass # cannot save mixtures

    def create_new_object_proxy(self):
        return Mixture(parent=self.model)

    @contextmanager
    def _multi_operation_context(self):
        with self.model.data_changed.hold():
            yield

    pass # end of class
