"""
    Fitting perspective
"""
################################################################################
#This software was developed by the University of Tennessee as part of the
#Distributed Data Analysis of Neutron Scattering Experiments (DANSE)
#project funded by the US National Science Foundation.
#
#See the license text in license.txt
#
#copyright 2009, University of Tennessee
################################################################################


import re
import sys
import os
import wx
import logging
import numpy as np
import time
from copy import deepcopy
import traceback

from sas.sascalc.dataloader.loader import Loader
from sas.sasgui.guiframe.dataFitting import Data2D
from sas.sasgui.guiframe.dataFitting import Data1D
from sas.sasgui.guiframe.dataFitting import check_data_validity
from sas.sasgui.guiframe.events import NewPlotEvent
from sas.sasgui.guiframe.events import StatusEvent
from sas.sasgui.guiframe.events import EVT_SLICER_PANEL
from sas.sasgui.guiframe.events import EVT_SLICER_PARS_UPDATE
from sas.sasgui.guiframe.gui_style import GUIFRAME_ID
from sas.sasgui.guiframe.plugin_base import PluginBase
from sas.sasgui.guiframe.data_processor import BatchCell
from sas.sascalc.fit.BumpsFitting import BumpsFit as Fit
from sas.sasgui.perspectives.fitting.console import ConsoleUpdate
from sas.sasgui.perspectives.fitting.fitproblem import FitProblemDictionary
from sas.sasgui.perspectives.fitting.fitpanel import FitPanel
from sas.sasgui.perspectives.fitting.resultpanel import ResultPanel, PlotResultEvent

from sas.sasgui.perspectives.fitting.fit_thread import FitThread
from sas.sasgui.perspectives.fitting.pagestate import Reader
from sas.sasgui.perspectives.fitting.fitpage import Chi2UpdateEvent
#from sas.sasgui.perspectives.calculator.model_editor import TextDialog
#from sas.sasgui.perspectives.calculator.model_editor import EditorWindow
from sas.sasgui.guiframe.gui_manager import MDIFrame
from sas.sasgui.guiframe.documentation_window import DocumentationWindow
from sas.sasgui.perspectives.fitting.gpu_options import GpuOptions

from . import models

logger = logging.getLogger(__name__)

MAX_NBR_DATA = 4

(PageInfoEvent, EVT_PAGE_INFO) = wx.lib.newevent.NewEvent()


if sys.platform == "win32":
    ON_MAC = False
else:
    ON_MAC = True

import bumps.options
from bumps.gui.fit_dialog import show_fit_config
try:
    from bumps.gui.fit_dialog import EVT_FITTER_CHANGED
except ImportError:
    # CRUFT: bumps 0.7.5.8 and below
    EVT_FITTER_CHANGED = None  # type: wx.PyCommandEvent

class Plugin(PluginBase):
    """
    Fitting plugin is used to perform fit
    """
    def __init__(self):
        PluginBase.__init__(self, name="Fitting")

        #list of panel to send to guiframe
        self.mypanels = []
        # reference to the current running thread
        self.calc_2D = None
        self.calc_1D = None

        self.color_dict = {}

        self.fit_thread_list = {}
        self.residuals = None
        self.weight = None
        self.fit_panel = None
        self.plot_panel = None
        # Start with a good default
        self.elapsed = 0.022
        self.fit_panel = None
        ## dictionary of page closed and id
        self.closed_page_dict = {}
        ## Relative error desired in the sum of squares (float)
        self.batch_reset_flag = True
        #List of selected data
        self.selected_data_list = []
        ## list of slicer panel created to display slicer parameters and results
        self.slicer_panels = []
        # model 2D view
        self.model2D_id = None
        #keep reference of the simultaneous fit page
        self.sim_page = None
        self.sim_menu = None
        self.batch_page = None
        self.batch_menu = None
        self.index_model = 0
        self.test_model_color = None
        #Create a reader for fit page's state
        self.state_reader = None
        self._extensions = '.fitv'
        self.menu1 = None
        self.new_model_frame = None

        self.temp_state = []
        self.state_index = 0
        self.sfile_ext = None
        # take care of saving  data, model and page associated with each other
        self.page_finder = {}
        # Log startup
        logger.info("Fitting plug-in started")
        self.batch_capable = self.get_batch_capable()

    def get_batch_capable(self):
        """
        Check if the plugin has a batch capability
        """
        return True

    def create_fit_problem(self, page_id):
        """
        Given an ID create a fitproblem container
        """
        self.page_finder[page_id] = FitProblemDictionary()

    def delete_fit_problem(self, page_id):
        """
        Given an ID create a fitproblem container
        """
        if page_id in iter(self.page_finder.keys()):
            del self.page_finder[page_id]

    def add_color(self, color, id):
        """
        adds a color as a key with a plot id as its value to a dictionary
        """
        self.color_dict[id] = color

    def on_batch_selection(self, flag):
        """
        switch the the notebook of batch mode or not
        """
        self.batch_on = flag
        if self.fit_panel is not None:
            self.fit_panel.batch_on = self.batch_on

    def populate_menu(self, owner):
        """
        Create a menu for the Fitting plug-in

        :param id: id to create a menu
        :param owner: owner of menu

        :return: list of information to populate the main menu

        """
        #Menu for fitting
        self.menu1 = wx.Menu()
        id1 = wx.NewId()
        simul_help = "Add new fit panel"
        self.menu1.Append(id1, '&New Fit Page', simul_help)
        wx.EVT_MENU(owner, id1, self.on_add_new_page)
        self.menu1.AppendSeparator()
        self.id_simfit = wx.NewId()
        simul_help = "Constrained or Simultaneous Fit"
        self.menu1.Append(self.id_simfit, '&Constrained or Simultaneous Fit', simul_help)
        wx.EVT_MENU(owner, self.id_simfit, self.on_add_sim_page)
        self.sim_menu = self.menu1.FindItemById(self.id_simfit)
        self.sim_menu.Enable(False)
        #combined Batch
        self.id_batchfit = wx.NewId()
        batch_help = "Combined Batch"
        self.menu1.Append(self.id_batchfit, '&Combine Batch Fit', batch_help)
        wx.EVT_MENU(owner, self.id_batchfit, self.on_add_sim_page)
        self.batch_menu = self.menu1.FindItemById(self.id_batchfit)
        self.batch_menu.Enable(False)

        self.menu1.AppendSeparator()
        self.id_bumps_options = wx.NewId()
        bopts_help = "Fitting options"
        self.menu1.Append(self.id_bumps_options, 'Fit &Options', bopts_help)
        wx.EVT_MENU(owner, self.id_bumps_options, self.on_bumps_options)
        self.bumps_options_menu = self.menu1.FindItemById(self.id_bumps_options)
        self.bumps_options_menu.Enable(True)

        self.id_gpu_options_panel = wx.NewId()
        self.menu1.Append(self.id_gpu_options_panel, "OpenCL Options", "Choose OpenCL driver or turn it off")
        wx.EVT_MENU(owner, self.id_gpu_options_panel, self.on_gpu_options)

        self.id_result_panel = wx.NewId()
        self.menu1.Append(self.id_result_panel, "Fit Results", "Show fit results panel")
        wx.EVT_MENU(owner, self.id_result_panel, self.on_fit_results)
        self.menu1.AppendSeparator()

        self.id_reset_flag = wx.NewId()
        resetf_help = "BatchFit: If checked, the initial param values will be "
        resetf_help += "propagated from the previous results. "
        resetf_help += "Otherwise, the same initial param values will be used "
        resetf_help += "for all fittings."
        self.menu1.AppendCheckItem(self.id_reset_flag,
                                   "Chain Fitting [BatchFit Only]",
                                   resetf_help)
        wx.EVT_MENU(owner, self.id_reset_flag, self.on_reset_batch_flag)
        chain_menu = self.menu1.FindItemById(self.id_reset_flag)
        chain_menu.Check(not self.batch_reset_flag)
        chain_menu.Enable(self.batch_on)

        self.menu1.AppendSeparator()
        self.edit_model_menu = wx.Menu()
        # Find and put files name in menu
        try:
            self.set_edit_menu(owner=owner)
        except:
            raise

        self.id_edit = wx.NewId()
        self.menu1.AppendMenu(self.id_edit, "Plugin Model Operations",
                              self.edit_model_menu)
        #create  menubar items
        return [(self.menu1, self.sub_menu)]

    def edit_custom_model(self, event):
        """
        Get the python editor panel
        """
        event_id = event.GetId()
        label = self.edit_menu.GetLabel(event_id)
        from sas.sasgui.perspectives.calculator.pyconsole import PyConsole
        filename = os.path.join(models.find_plugins_dir(), label)
        frame = PyConsole(parent=self.parent, manager=self,
                          panel=self.fit_panel,
                          title='Advanced Plugin Model Editor',
                          filename=filename)
        self.put_icon(frame)
        frame.Show(True)

    def delete_custom_model(self, event):
        """
        Delete custom model file
        """
        event_id = event.GetId()
        label = self.delete_menu.GetLabel(event_id)
        toks = os.path.splitext(label)
        path = os.path.join(models.find_plugins_dir(), toks[0])
        try:
            for ext in ['.py', '.pyc']:
                p_path = path + ext
                os.remove(p_path)
            self.update_custom_combo()
            if os.path.isfile(p_path):
                msg = "Sorry! unable to delete the default "
                msg += "plugin model... \n"
                msg += "Please manually remove the files (.py, .pyc) "
                msg += "in the 'plugin_models' folder \n"
                msg += "inside of the SasView application, "
                msg += "and try it again."
                wx.MessageBox(msg, 'Info')
                #evt = StatusEvent(status=msg, type='stop', info='warning')
                #wx.PostEvent(self.parent, evt)
            else:
                self.delete_menu.Delete(event_id)
                for item in self.edit_menu.GetMenuItems():
                    if item.GetLabel() == label:
                        self.edit_menu.DeleteItem(item)
                        msg = "The plugin model, %s, has been deleted." % label
                        evt = StatusEvent(status=msg, type='stop', info='info')
                        wx.PostEvent(self.parent, evt)
                        break
        except Exception:
            import traceback; traceback.print_exc()
            msg = 'Delete Error: \nCould not delete the file; Check if in use.'
            wx.MessageBox(msg, 'Error')

    def make_sum_model(self, event):
        """
        Edit summodel template and make one
        """
        event_id = event.GetId()
        model_manager = models.ModelManager()
        model_list = model_manager.get_model_name_list()
        plug_dir = models.find_plugins_dir()
        textdial = TextDialog(None, self, wx.ID_ANY, 'Easy Sum/Multi(p1, p2) Editor',
                              model_list, plug_dir)
        self.put_icon(textdial)
        textdial.ShowModal()
        textdial.Destroy()

    def make_new_model(self, event):
        """
        Make new model
        """
        if self.new_model_frame is not None:
            self.new_model_frame.Show(False)
            self.new_model_frame.Show(True)
        else:
            event_id = event.GetId()
            dir_path = models.find_plugins_dir()
            title = "New Plugin Model Function"
            self.new_model_frame = EditorWindow(parent=self, base=self,
                                                path=dir_path, title=title)
            self.put_icon(self.new_model_frame)
        self.new_model_frame.Show(True)

    def load_plugin_models(self, event):
        """
        Update of models in plugin_models folder
        """
        event_id = event.GetId()
        self.update_custom_combo()

    def update_custom_combo(self):
        """
        Update custom model list in the fitpage combo box
        """
        custom_model = 'Plugin Models'
        try:
            # Update edit menus
            self.set_edit_menu_helper(self.parent, self.edit_custom_model)
            self.set_edit_menu_helper(self.parent, self.delete_custom_model)
            temp = self.fit_panel.reset_pmodel_list()
            if temp:
                # Set the new plugin model list for all fit pages
                for uid, page in self.fit_panel.opened_pages.items():
                    if hasattr(page, "formfactorbox"):
                        page.model_list_box = temp
                        current_val = page.formfactorbox.GetLabel()
                        #if page.plugin_rbutton.GetValue():
                        mod_cat = page.categorybox.GetStringSelection()
                        if mod_cat == custom_model:
                            #pos = page.formfactorbox.GetSelection()
                            page._show_combox_helper()
                            new_val = page.formfactorbox.GetLabel()
                            if current_val != new_val and new_val != '':
                                page.formfactorbox.SetLabel(new_val)
                            else:
                                page.formfactorbox.SetLabel(current_val)
        except:
            logger.error("update_custom_combo: %s", sys.exc_info()[1])

    def set_edit_menu(self, owner):
        """
        Set list of the edit model menu labels
        """
        wx_id = wx.NewId()
        #new_model_menu = wx.Menu()
        self.edit_model_menu.Append(wx_id, 'New Plugin Model',
                                   'Add a new model function')
        wx.EVT_MENU(owner, wx_id, self.make_new_model)
        
        wx_id = wx.NewId()
        self.edit_model_menu.Append(wx_id, 'Sum|Multi(p1, p2)',
                                    'Sum of two model functions')
        wx.EVT_MENU(owner, wx_id, self.make_sum_model)

        e_id = wx.NewId()
        self.edit_menu = wx.Menu()
        self.edit_model_menu.AppendMenu(e_id,
                                    'Advanced Plugin Editor', self.edit_menu)
        self.set_edit_menu_helper(owner, self.edit_custom_model)

        d_id = wx.NewId()
        self.delete_menu = wx.Menu()
        self.edit_model_menu.AppendMenu(d_id,
                                        'Delete Plugin Models', self.delete_menu)
        self.set_edit_menu_helper(owner, self.delete_custom_model)

        wx_id = wx.NewId()
        self.edit_model_menu.Append(wx_id, 'Load Plugin Models',
          '(Re)Load all models present in user plugin_models folder')
        wx.EVT_MENU(owner, wx_id, self.load_plugin_models)
                
    def set_edit_menu_helper(self, owner=None, menu=None):
        """
        help for setting list of the edit model menu labels
        """
        if menu is None:
            menu = self.edit_custom_model
        list_fnames = os.listdir(models.find_plugins_dir())
        list_fnames.sort()
        for f_item in list_fnames:
            name = os.path.basename(f_item)
            toks = os.path.splitext(name)
            if toks[-1] == '.py' and not toks[0] == '__init__':
                if menu == self.edit_custom_model:
                    if toks[0] == 'easy_sum_of_p1_p2':
                        continue
                    submenu = self.edit_menu
                else:
                    submenu = self.delete_menu
                has_file = False
                for item in submenu.GetMenuItems():
                    if name == submenu.GetLabel(item.GetId()):
                        has_file = True
                if not has_file:
                    wx_id = wx.NewId()
                    submenu.Append(wx_id, name)
                    wx.EVT_MENU(owner, wx_id, menu)
                    has_file = False

    def put_icon(self, frame):
        """
        Put icon in the frame title bar
        """
        if hasattr(frame, "IsIconized"):
            if not frame.IsIconized():
                try:
                    icon = self.parent.GetIcon()
                    frame.SetIcon(icon)
                except:
                    pass

    def on_add_sim_page(self, event):
        """
        Create a page to access simultaneous fit option
        """
        event_id = event.GetId()
        caption = "Const & Simul Fit"
        page = self.sim_page
        if event_id == self.id_batchfit:
            caption = "Combined Batch"
            page = self.batch_page

        def set_focus_page(page):
            page.Show(True)
            page.Refresh()
            page.SetFocus()
            #self.parent._mgr.Update()
            msg = "%s already opened\n" % str(page.window_caption)
            wx.PostEvent(self.parent, StatusEvent(status=msg))

        if page is not None:
            return set_focus_page(page)
        if caption == "Const & Simul Fit":
            self.sim_page = self.fit_panel.add_sim_page(caption=caption)
        else:
            self.batch_page = self.fit_panel.add_sim_page(caption=caption)

    def get_context_menu(self, plotpanel=None):
        """
        Get the context menu items available for P(r).them allow fitting option
        for Data2D and Data1D only.

        :param graph: the Graph object to which we attach the context menu

        :return: a list of menu items with call-back function

        :note: if Data1D was generated from Theory1D
                the fitting option is not allowed

        """
        self.plot_panel = plotpanel
        graph = plotpanel.graph
        fit_option = "Select data for fitting"
        fit_hint = "Dialog with fitting parameters "

        if graph.selected_plottable not in plotpanel.plots:
            return []
        item = plotpanel.plots[graph.selected_plottable]
        if item.__class__.__name__ is "Data2D":
            if hasattr(item, "is_data"):
                if item.is_data:
                    return [[fit_option, fit_hint, self._onSelect]]
                else:
                    return []
            return [[fit_option, fit_hint, self._onSelect]]
        else:

            # if is_data is true , this in an actual data loaded
            #else it is a data created from a theory model
            if hasattr(item, "is_data"):
                if item.is_data:
                    return [[fit_option, fit_hint, self._onSelect]]
                else:
                    return []
            return [[fit_option, fit_hint, self._onSelect]]
        return []

    def get_panels(self, parent):
        """
        Create and return a list of panel objects
        """
        self.parent = parent
        #self.parent.Bind(EVT_FITSTATE_UPDATE, self.on_set_state_helper)
        # Creation of the fit panel
        self.frame = MDIFrame(self.parent, None, 'None', (100, 200))
        self.fit_panel = FitPanel(parent=self.frame, manager=self)
        self.frame.set_panel(self.fit_panel)
        self._frame_set_helper()
        self.on_add_new_page(event=None)
        #Set the manager for the main panel
        self.fit_panel.set_manager(self)
        # List of windows used for the perspective
        self.perspective = []
        self.perspective.append(self.fit_panel.window_name)

        self.result_frame = MDIFrame(self.parent, None, ResultPanel.window_caption, (220, 200))
        self.result_panel = ResultPanel(parent=self.result_frame, manager=self)
        self.perspective.append(self.result_panel.window_name)

        #index number to create random model name
        self.index_model = 0
        self.index_theory = 0
        self.parent.Bind(EVT_SLICER_PANEL, self._on_slicer_event)
        self.parent.Bind(EVT_SLICER_PARS_UPDATE, self._onEVT_SLICER_PANEL)

        # CRUFT: EVT_FITTER_CHANGED is not None for bumps 0.7.5.9 and above
        if EVT_FITTER_CHANGED is not None:
            self.parent.Bind(EVT_FITTER_CHANGED, self.on_fitter_changed)
        self._set_fitter_label(bumps.options.FIT_CONFIG)

        #self.parent._mgr.Bind(wx.aui.EVT_AUI_PANE_CLOSE,self._onclearslicer)
        #Create reader when fitting panel are created
        self.state_reader = Reader(self.set_state)
        #append that reader to list of available reader
        loader = Loader()
        loader.associate_file_reader(".fitv", self.state_reader)
        #Send the fitting panel to guiframe
        self.mypanels.append(self.fit_panel)
        self.mypanels.append(self.result_panel)
        return self.mypanels

    def clear_panel(self):
        """
        """
        self.fit_panel.clear_panel()

    def delete_data(self, data):
        """
        delete  the given data from panel
        """
        self.fit_panel.delete_data(data)

    def set_data(self, data_list=None):
        """
        receive a list of data to fit
        """
        if data_list is None:
            data_list = []
        selected_data_list = []
        if self.batch_on:
            self.add_fit_page(data=data_list)
        else:
            if len(data_list) > MAX_NBR_DATA:
                from .fitting_widgets import DataDialog
                dlg = DataDialog(data_list=data_list, nb_data=MAX_NBR_DATA)
                if dlg.ShowModal() == wx.ID_OK:
                    selected_data_list = dlg.get_data()
                dlg.Destroy()

            else:
                selected_data_list = data_list
            try:
                group_id = wx.NewId()
                for data in selected_data_list:
                    if data is not None:
                        # 2D has no same group_id
                        if data.__class__.__name__ == 'Data2D':
                            group_id = wx.NewId()
                        data.group_id = group_id
                        if group_id not in data.list_group_id:
                            data.list_group_id.append(group_id)
                        self.add_fit_page(data=[data])
            except:
                msg = "Fitting set_data: " + str(sys.exc_info()[1])
                wx.PostEvent(self.parent, StatusEvent(status=msg, info="error"))

    def set_theory(self, theory_list=None):
        """
        """
        #set the model state for a given theory_state:
        for item in theory_list:
            try:
                _, theory_state = item
                self.fit_panel.set_model_state(theory_state)
            except Exception:
                msg = "Fitting: cannot deal with the theory received"
                evt = StatusEvent(status=msg, info="error")
                logger.error("set_theory " + msg + "\n" + str(sys.exc_info()[1]))
                wx.PostEvent(self.parent, evt)

    def set_state(self, state=None, datainfo=None, format=None):
        """
        Call-back method for the fit page state reader.
        This method is called when a .fitv/.svs file is loaded.

        : param state: PageState object
        : param datainfo: data
        """
        from pagestate import PageState
        from .simfitpage import SimFitPageState
        if isinstance(state, PageState):
            state = state.clone()
            self.temp_state.append(state)
        elif isinstance(state, SimFitPageState):
            state.load_from_save_state(self)
        else:
            self.temp_state = []
        # index to start with for a new set_state
        self.state_index = 0
        # state file format
        self.sfile_ext = format

        self.on_set_state_helper(event=None)

    def  on_set_state_helper(self, event=None):
        """
        Set_state_helper. This actually sets state
        after plotting data from state file.

        : event: FitStateUpdateEvent called
            by dataloader.plot_data from guiframe
        """
        if len(self.temp_state) == 0:
            if self.state_index == 0 and len(self.mypanels) <= 0 \
            and self.sfile_ext == '.svs':
                self.temp_state = []
                self.state_index = 0
            return

        try:
            # Load fitting state
            state = self.temp_state[self.state_index]
            #panel state should have model selection to set_state
            if state.formfactorcombobox is not None:
                #set state
                data = self.parent.create_gui_data(state.data)
                data.group_id = state.data.group_id
                self.parent.add_data(data_list={data.id: data})
                wx.PostEvent(self.parent, NewPlotEvent(plot=data,
                                        title=data.title))
                #need to be fix later make sure we are sendind guiframe.data
                #to panel
                state.data = data
                page = self.fit_panel.set_state(state)
            else:
                #just set data because set_state won't work
                data = self.parent.create_gui_data(state.data)
                data.group_id = state.data.group_id
                self.parent.add_data(data_list={data.id: data})
                wx.PostEvent(self.parent, NewPlotEvent(plot=data,
                                        title=data.title))
                page = self.add_fit_page([data])
                caption = page.window_caption
                self.store_data(uid=page.uid, data_list=page.get_data_list(),
                        caption=caption)
                self.mypanels.append(page)

            # get ready for the next set_state
            self.state_index += 1

            #reset state variables to default when all set_state is finished.
            if len(self.temp_state) == self.state_index:

                self.temp_state = []
                #self.state_index = 0
                # Make sure the user sees the fitting panel after loading
                #self.parent.set_perspective(self.perspective)
                self.on_perspective(event=None)
        except:
            self.state_index = 0
            self.temp_state = []
            raise

    def set_param2fit(self, uid, param2fit):
        """
        Set the list of param names to fit for fitprobelm
        """
        self.page_finder[uid].set_param2fit(param2fit)

    def set_graph_id(self, uid, graph_id):
        """
        Set graph_id for fitprobelm
        """
        self.page_finder[uid].set_graph_id(graph_id)

    def get_graph_id(self, uid):
        """
        Set graph_id for fitprobelm
        """
        return self.page_finder[uid].get_graph_id()

    def save_fit_state(self, filepath, fitstate):
        """
        save fit page state into file
        """
        self.state_reader.write(filename=filepath, fitstate=fitstate)

    def set_fit_weight(self, uid, flag, is2d=False, fid=None):
        """
        Set the fit weights of a given page for all
        its data by default. If fid is provide then set the range
        only for the data with fid as id
        :param uid: id corresponding to a fit page
        :param fid: id corresponding to a fit problem (data, model)
        :param weight: current dy data
        """
        # If we are not dealing with a specific fit problem, then
        # there is no point setting the weights.
        if fid is None:
            return
        if uid in list(self.page_finder.keys()):
            self.page_finder[uid].set_weight(flag=flag, is2d=is2d)

    def set_fit_range(self, uid, qmin, qmax, fid=None):
        """
        Set the fitting range of a given page for all
        its data by default. If fid is provide then set the range
        only for the data with fid as id
        :param uid: id corresponding to a fit page
        :param fid: id corresponding to a fit problem (data, model)
        :param qmin: minimum  value of the fit range
        :param qmax: maximum  value of the fit range
        """
        if uid in list(self.page_finder.keys()):
            self.page_finder[uid].set_range(qmin=qmin, qmax=qmax, fid=fid)

    def schedule_for_fit(self, value=0, uid=None):
        """
        Set the fit problem field to 0 or 1 to schedule that problem to fit.
        Schedule the specified fitproblem or get the fit problem related to
        the current page and set value.
        :param value: integer 0 or 1
        :param uid: the id related to a page contaning fitting information
        """
        if uid in list(self.page_finder.keys()):
            self.page_finder[uid].schedule_tofit(value)

    def get_page_finder(self):
        """
        return self.page_finder used also by simfitpage.py
        """
        return self.page_finder

    def set_page_finder(self, modelname, names, values):
        """
        Used by simfitpage.py to reset a parameter given the string constrainst.

        :param modelname: the name ot the model for with the parameter
                            has to reset
        :param value: can be a string in this case.
        :param names: the paramter name
        """
        sim_page_id = self.sim_page.uid
        for uid, value in self.page_finder.items():
            if uid != sim_page_id and uid != self.batch_page.uid:
                model_list = value.get_model()
                model = model_list[0]
                if model.name == modelname:
                    value.set_model_param(names, values)
                    break

    def split_string(self, item):
        """
        receive a word containing dot and split it. used to split parameterset
        name into model name and parameter name example: ::

            paramaterset (item) = M1.A
            Will return model_name = M1 , parameter name = A

        """
        if item.find(".") >= 0:
            param_names = re.split("\.", item)
            model_name = param_names[0]
            ##Assume max len is 3; eg., M0.radius.width
            if len(param_names) == 3:
                param_name = param_names[1] + "." + param_names[2]
            else:
                param_name = param_names[1]
            return model_name, param_name

    def on_bumps_options(self, event=None):
        """
        Open the bumps options panel.
        """
        show_fit_config(self.parent, help=self.on_help)

    def on_fitter_changed(self, event):
        self._set_fitter_label(event.config)

    def _set_fitter_label(self, config):
        self.fit_panel.parent.SetTitle(self.fit_panel.window_name
                                       + " - Active Fitting Optimizer: "
                                       + config.selected_name)

    def on_help(self, algorithm_id):
        _TreeLocation = "user/sasgui/perspectives/fitting/optimizer.html"
        _anchor = "#fit-"+algorithm_id
        DocumentationWindow(self.parent, wx.ID_ANY, _TreeLocation, _anchor, "Optimizer Help")


    def on_fit_results(self, event=None):
        """
        Make the Fit Results panel visible.
        """
        self.result_frame.Show()
        self.result_frame.Raise()

    def on_gpu_options(self, event=None):
        """
        Make the Fit Results panel visible.
        """
        dialog = GpuOptions(None, wx.ID_ANY, "")
        dialog.Show()

    def stop_fit(self, uid):
        """
        Stop the fit
        """
        if uid in list(self.fit_thread_list.keys()):
            calc_fit = self.fit_thread_list[uid]
            if calc_fit is not  None and calc_fit.isrunning():
                calc_fit.stop()
                msg = "Fit stop!"
                wx.PostEvent(self.parent, StatusEvent(status=msg, type="stop"))
            del self.fit_thread_list[uid]
        #set the fit button label of page when fit stop is trigger from
        #simultaneous fit pane
        sim_flag = self.sim_page is not None and uid == self.sim_page.uid
        batch_flag = self.batch_page is not None and uid == self.batch_page.uid
        if sim_flag or batch_flag:
            for uid, value in self.page_finder.items():
                if value.get_scheduled() == 1:
                    if uid in list(self.fit_panel.opened_pages.keys()):
                        panel = self.fit_panel.opened_pages[uid]
                        panel._on_fit_complete()

    def set_smearer(self, uid, smearer, fid, qmin=None, qmax=None, draw=True,
                    enable_smearer=False):
        """
        Get a smear object and store it to a fit problem of fid as id. If proper
        flag is enable , will plot the theory with smearing information.

        :param smearer: smear object to allow smearing data of id fid
        :param enable_smearer: Define whether or not all (data, model) contained
            in the structure of id uid will be smeared before fitting.
        :param qmin: the maximum value of the theory plotting range
        :param qmax: the maximum value of the theory plotting range
        :param draw: Determine if the theory needs to be plot
        """
        if uid not in list(self.page_finder.keys()):
            return
        self.page_finder[uid].enable_smearing(flag=enable_smearer)
        self.page_finder[uid].set_smearer(smearer, fid=fid)
        if draw:
            ## draw model 1D with smeared data
            data = self.page_finder[uid].get_fit_data(fid=fid)
            if data is None:
                msg = "set_mearer requires at least data.\n"
                msg += "Got data = %s .\n" % str(data)
                return
                #raise ValueError, msg
            model = self.page_finder[uid].get_model(fid=fid)
            if model is None:
                return
            enable1D = issubclass(data.__class__, Data1D)
            enable2D = issubclass(data.__class__, Data2D)
            ## if user has already selected a model to plot
            ## redraw the model with data smeared
            smear = self.page_finder[uid].get_smearer(fid=fid)

            # compute weight for the current data
            weight = self.page_finder[uid].get_weight(fid=fid)

            self.draw_model(model=model, data=data, page_id=uid, smearer=smear,
                enable1D=enable1D, enable2D=enable2D,
                qmin=qmin, qmax=qmax, weight=weight)

    def draw_model(self, model, page_id, data=None, smearer=None,
                   enable1D=True, enable2D=False,
                   state=None,
                   fid=None,
                   toggle_mode_on=False,
                   qmin=None, qmax=None,
                   update_chisqr=True, weight=None, source='model'):
        """
        Draw model.

        :param model: the model to draw
        :param name: the name of the model to draw
        :param data: the data on which the model is based to be drawn
        :param description: model's description
        :param enable1D: if true enable drawing model 1D
        :param enable2D: if true enable drawing model 2D
        :param qmin:  Range's minimum value to draw model
        :param qmax:  Range's maximum value to draw model
        :param qstep: number of step to divide the x and y-axis
        :param update_chisqr: update chisqr [bool]

        """
        #self.weight = weight
        if issubclass(data.__class__, Data1D) or not enable2D:
            ## draw model 1D with no loaded data
            self._draw_model1D(model=model,
                               data=data,
                               page_id=page_id,
                               enable1D=enable1D,
                               smearer=smearer,
                               qmin=qmin,
                               qmax=qmax,
                               fid=fid,
                               weight=weight,
                               toggle_mode_on=toggle_mode_on,
                               state=state,
                               update_chisqr=update_chisqr,
                               source=source)
        else:
            ## draw model 2D with no initial data
            self._draw_model2D(model=model,
                                page_id=page_id,
                                data=data,
                                enable2D=enable2D,
                                smearer=smearer,
                                qmin=qmin,
                                qmax=qmax,
                                fid=fid,
                                weight=weight,
                                state=state,
                                toggle_mode_on=toggle_mode_on,
                                update_chisqr=update_chisqr,
                                source=source)

    def onFit(self, uid):
        """
        Get series of data, model, associates parameters and range and send then
        to  series of fitters. Fit data and model, display result to
        corresponding panels.
        :param uid: id related to the panel currently calling this fit function.
        """
        if uid is None: raise RuntimeError("no page to fit") # Should never happen

        sim_page_uid = getattr(self.sim_page, 'uid', None)
        batch_page_uid = getattr(self.batch_page, 'uid', None)

        if uid == sim_page_uid:
            fit_type = 'simultaneous'
        elif uid == batch_page_uid:
            fit_type = 'combined_batch'
        else:
            fit_type = 'single'

        fitter_list = []
        sim_fitter = None
        if fit_type == 'simultaneous':
            # for simultaneous fitting only one fitter is needed
            sim_fitter = Fit()
            sim_fitter.fitter_id = self.sim_page.uid
            fitter_list.append(sim_fitter)

        self.current_pg = None
        list_page_id = []
        fit_id = 0
        for page_id, page_info in self.page_finder.items():
            # For simulfit (uid give with None), do for-loop
            # if uid is specified (singlefit), do it only on the page.
            if page_id in (sim_page_uid, batch_page_uid): continue
            if fit_type == "single" and page_id != uid: continue

            try:
                if page_info.get_scheduled() == 1:
                    page_info.nbr_residuals_computed = 0
                    page = self.fit_panel.get_page_by_id(page_id)
                    self.set_fit_weight(uid=page.uid,
                                     flag=page.get_weight_flag(),
                                     is2d=page._is_2D())
                    if not page.param_toFit:
                        msg = "No fitting parameters for %s" % page.window_caption
                        evt = StatusEvent(status=msg, info="error", type="stop")
                        wx.PostEvent(page.parent.parent, evt)
                        return False
                    if not page._update_paramv_on_fit():
                        msg = "Fitting range or parameter values are"
                        msg += " invalid in %s" % \
                                    page.window_caption
                        evt = StatusEvent(status=msg, info="error", type="stop")
                        wx.PostEvent(page.parent.parent, evt)
                        return False

                    pars = [str(element[1]) for element in page.param_toFit]
                    fitproblem_list = list(page_info.values())
                    for fitproblem in  fitproblem_list:
                        if sim_fitter is None:
                            fitter = Fit()
                            fitter.fitter_id = page_id
                            fitter_list.append(fitter)
                        else:
                            fitter = sim_fitter
                        self._add_problem_to_fit(fitproblem=fitproblem,
                                             pars=pars,
                                             fitter=fitter,
                                             fit_id=fit_id)
                        fit_id += 1
                    list_page_id.append(page_id)
                    page_info.clear_model_param()
            except KeyboardInterrupt:
                msg = "Fitting terminated"
                evt = StatusEvent(status=msg, info="info", type="stop")
                wx.PostEvent(self.parent, evt)
                return True
            except:
                raise
                msg = "Fitting error: %s" % str(sys.exc_info()[1])
                evt = StatusEvent(status=msg, info="error", type="stop")
                wx.PostEvent(self.parent, evt)
                return False
        ## If a thread is already started, stop it
        #if self.calc_fitis not None and self.calc_fit.isrunning():
        #    self.calc_fit.stop()
        msg = "Fitting is in progress..."
        wx.PostEvent(self.parent, StatusEvent(status=msg, type="progress"))

        #Handler used to display fit message
        handler = ConsoleUpdate(parent=self.parent,
                                manager=self,
                                improvement_delta=0.1)

        # batch fit
        batch_inputs = {}
        batch_outputs = {}
        if fit_type == "simultaneous":
            page = self.sim_page
        elif fit_type == "combined_batch":
            page = self.batch_page
        else:
            page = self.fit_panel.get_page_by_id(uid)
        if page.batch_on:
            calc_fit = FitThread(handler=handler,
                                 fn=fitter_list,
                                 pars=pars,
                                 batch_inputs=batch_inputs,
                                 batch_outputs=batch_outputs,
                                 page_id=list_page_id,
                                 completefn=self._batch_fit_complete,
                                 reset_flag=self.batch_reset_flag)
        else:
            ## Perform more than 1 fit at the time
            calc_fit = FitThread(handler=handler,
                                    fn=fitter_list,
                                    batch_inputs=batch_inputs,
                                    batch_outputs=batch_outputs,
                                    page_id=list_page_id,
                                    updatefn=handler.update_fit,
                                    completefn=self._fit_completed)
        #self.fit_thread_list[current_page_id] = calc_fit
        self.fit_thread_list[uid] = calc_fit
        calc_fit.queue()
        calc_fit.ready(2.5)
        msg = "Fitting is in progress..."
        wx.PostEvent(self.parent, StatusEvent(status=msg, type="progress"))

        return True

    def remove_plot(self, uid, fid=None, theory=False):
        """
        remove model plot when a fit page is closed
        :param uid: the id related to the fitpage to close
        :param fid: the id of the fitproblem(data, model, range,etc)
        """
        if uid not in list(self.page_finder.keys()):
            return
        fitproblemList = self.page_finder[uid].get_fit_problem(fid)
        for fitproblem in fitproblemList:
            data = fitproblem.get_fit_data()
            model = fitproblem.get_model()
            plot_id = None
            if model is not None:
                plot_id = data.id + model.name
            if theory:
                plot_id = data.id + model.name
            group_id = data.group_id
            wx.PostEvent(self.parent, NewPlotEvent(id=plot_id,
                                                   group_id=group_id,
                                                   action='remove'))

    def store_data(self, uid, data_list=None, caption=None):
        """
        Recieve a list of data and store them ans well as a caption of
        the fit page where they come from.
        :param uid: if related to a fit page
        :param data_list: list of data to fit
        :param caption: caption of the window related to these data
        """
        if data_list is None:
            data_list = []

        self.page_finder[uid].set_fit_data(data=data_list)
        if caption is not None:
            self.page_finder[uid].set_fit_tab_caption(caption=caption)

    def on_add_new_page(self, event=None):
        """
        ask fit panel to create a new empty page
        """
        try:
            page = self.fit_panel.add_empty_page()
            # add data associated to the page created
            if page is not None:
                evt = StatusEvent(status="Page Created", info="info")
                wx.PostEvent(self.parent, evt)
            else:
                msg = "Page was already Created"
                evt = StatusEvent(status=msg, info="warning")
                wx.PostEvent(self.parent, evt)
        except:
            msg = "Creating Fit page: %s" % sys.exc_info()[1]
            wx.PostEvent(self.parent, StatusEvent(status=msg, info="error"))

    def add_fit_page(self, data):
        """
        given a data, ask to the fitting panel to create a new fitting page,
        get this page and store it into the page_finder of this plug-in
        :param data: is a list of data
        """
        page = self.fit_panel.set_data(data)
        # page could be None when loading state files
        if page is None:
            return page
        #append Data1D to the panel containing its theory
        #if theory already plotted
        if page.uid in self.page_finder:
            data = page.get_data()
            theory_data = self.page_finder[page.uid].get_theory_data(data.id)
            if issubclass(data.__class__, Data2D):
                data.group_id = wx.NewId()
                if theory_data is not None:
                    group_id = str(page.uid) + " Model1D"
                    wx.PostEvent(self.parent,
                             NewPlotEvent(group_id=group_id,
                                               action="delete"))
                    self.parent.update_data(prev_data=theory_data,
                                             new_data=data)
            else:
                if theory_data is not None:
                    group_id = str(page.uid) + " Model2D"
                    data.group_id = theory_data.group_id
                    wx.PostEvent(self.parent,
                             NewPlotEvent(group_id=group_id,
                                               action="delete"))
                    self.parent.update_data(prev_data=theory_data,
                                             new_data=data)
        self.store_data(uid=page.uid, data_list=page.get_data_list(),
                        caption=page.window_caption)
        if self.sim_page is not None and not self.batch_on:
            self.sim_page.draw_page()
        if self.batch_page is not None and self.batch_on:
            self.batch_page.draw_page()

        return page

    def _onEVT_SLICER_PANEL(self, event):
        """
        receive and event telling to update a panel with a name starting with
        event.panel_name. this method update slicer panel
        for a given interactor.

        :param event: contains type of slicer , paramaters for updating
            the panel and panel_name to find the slicer 's panel concerned.
        """
        event.panel_name
        for item in self.parent.panels:
            name = event.panel_name
            if self.parent.panels[item].window_caption.startswith(name):
                self.parent.panels[item].set_slicer(event.type, event.params)

        #self.parent._mgr.Update()

    def _closed_fitpage(self, event):
        """
        request fitpanel to close a given page when its unique data is removed
        from the plot. close fitpage only when the a loaded data is removed
        """
        if event is None or event.data is None:
            return
        if hasattr(event.data, "is_data"):
            if not event.data.is_data or \
                event.data.__class__.__name__ == "Data1D":
                self.fit_panel.close_page_with_data(event.data)

    def _reset_schedule_problem(self, value=0, uid=None):
        """
        unschedule or schedule all fitproblem to be fit
        """
        # case that uid is not specified
        if uid is None:
            for page_id in list(self.page_finder.keys()):
                self.page_finder[page_id].schedule_tofit(value)
        # when uid is given
        else:
            if uid in list(self.page_finder.keys()):
                self.page_finder[uid].schedule_tofit(value)

    def _add_problem_to_fit(self, fitproblem, pars, fitter, fit_id):
        """
        Create and set fitter with series of data and model
        """
        data = fitproblem.get_fit_data()
        model = fitproblem.get_model()
        smearer = fitproblem.get_smearer()
        qmin, qmax = fitproblem.get_range()

        #Extra list of parameters and their constraints
        listOfConstraint = []
        param = fitproblem.get_model_param()
        if len(param) > 0:
            for item in param:
                ## check if constraint
                if item[0] is not None and item[1] is not None:
                    listOfConstraint.append((item[0], item[1]))
        new_model = model
        fitter.set_model(new_model, fit_id, pars, data=data,
                         constraints=listOfConstraint)
        fitter.set_data(data=data, id=fit_id, smearer=smearer, qmin=qmin,
                        qmax=qmax)
        fitter.select_problem_for_fit(id=fit_id, value=1)

    def _onSelect(self, event):
        """
        when Select data to fit a new page is created .Its reference is
        added to self.page_finder
        """
        panel = self.plot_panel
        if panel is None:
            raise ValueError("Fitting:_onSelect: NonType panel")
        Plugin.on_perspective(self, event=event)
        self.select_data(panel)

    def select_data(self, panel):
        """
        """
        for plottable in panel.graph.plottables:
            if plottable.__class__.__name__ in ["Data1D", "Theory1D"]:
                data_id = panel.graph.selected_plottable
                if plottable == panel.plots[data_id]:
                    data = plottable
                    self.add_fit_page(data=[data])
                    return
            else:
                data = plottable
                self.add_fit_page(data=[data])

    def update_fit(self, result=None, msg=""):
        """
        """
        print("update_fit result", result)

    def _batch_fit_complete(self, result, pars, page_id,
                            batch_outputs, batch_inputs, elapsed=None):
        """
        Display fit result in batch
        :param result: list of objects received from fitters
        :param pars: list of  fitted parameters names
        :param page_id: list of page ids which called fit function
        :param elapsed: time spent at the fitting level
        """
        uid = page_id[0]
        if uid in list(self.fit_thread_list.keys()):
            del self.fit_thread_list[uid]

        wx.CallAfter(self._update_fit_button, page_id)
        t1 = time.time()
        str_time = time.strftime("%a, %d %b %Y %H:%M:%S ", time.localtime(t1))
        msg = "Fit completed on %s \n" % str_time
        msg += "Duration time: %s s.\n" % str(elapsed)
        evt = StatusEvent(status=msg, info="info", type="stop")
        wx.PostEvent(self.parent, evt)

        if batch_outputs is None:
            batch_outputs = {}

        # format batch_outputs
        batch_outputs["Chi2"] = []
        #Don't like these loops
        # Need to create dictionary of all fitted parameters
        # since the number of parameters can differ between each fit result
        for list_res in result:
            for res in list_res:
                model, data = res.inputs[0]
                if model is not None and hasattr(model, "model"):
                    model = model.model
                #get all fittable parameters of the current model
                for param in  model.getParamList():
                    if param  not in list(batch_outputs.keys()):
                        batch_outputs[param] = []
                for param in model.getDispParamList():
                    if not model.is_fittable(param) and \
                        param in list(batch_outputs.keys()):
                        del batch_outputs[param]
                # Add fitted parameters and their error
                for param in res.param_list:
                    if param not in list(batch_outputs.keys()):
                        batch_outputs[param] = []
                    err_param = "error on %s" % str(param)
                    if err_param not in list(batch_inputs.keys()):
                        batch_inputs[err_param] = []
        msg = ""
        for list_res in result:
            for res in list_res:
                pid = res.fitter_id
                model, data = res.inputs[0]
                correct_result = False
                if model is not None and hasattr(model, "model"):
                    model = model.model
                if data is not None and hasattr(data, "sas_data"):
                    data = data.sas_data

                is_data2d = issubclass(data.__class__, Data2D)
                #check consistency of arrays
                if not is_data2d:
                    if len(res.theory) == len(res.index[res.index]) and \
                        len(res.index) == len(data.y):
                        correct_result = True
                else:
                    copy_data = deepcopy(data)
                    new_theory = copy_data.data
                    new_theory[res.index] = res.theory
                    new_theory[res.index == False] = np.nan
                    correct_result = True
                #get all fittable parameters of the current model
                param_list = model.getParamList()
                for param in model.getDispParamList():
                    if not model.is_fittable(param) and \
                        param in param_list:
                        param_list.remove(param)
                if not correct_result or res.fitness is None or \
                    not np.isfinite(res.fitness) or \
                        np.any(res.pvec is None) or not \
                        np.all(np.isfinite(res.pvec)):
                    data_name = str(None)
                    if data is not None:
                        data_name = str(data.name)
                    model_name = str(None)
                    if model is not None:
                        model_name = str(model.name)
                    msg += "Data %s and Model %s did not fit.\n" % (data_name,
                                                                    model_name)
                    ERROR = np.NAN
                    cell = BatchCell()
                    cell.label = res.fitness
                    cell.value = res.fitness
                    batch_outputs["Chi2"].append(ERROR)
                    for param in param_list:
                        # save value of  fixed parameters
                        if param not in res.param_list:
                            batch_outputs[str(param)].append(ERROR)
                        else:
                            #save only fitted values
                            batch_outputs[param].append(ERROR)
                            batch_inputs["error on %s" % str(param)].append(ERROR)
                else:
                    # TODO: Why sometimes res.pvec comes with np.float64?
                    # probably from scipy lmfit
                    if res.pvec.__class__ == np.float64:
                        res.pvec = [res.pvec]

                    cell = BatchCell()
                    cell.label = res.fitness
                    cell.value = res.fitness
                    batch_outputs["Chi2"].append(cell)
                    # add parameters to batch_results
                    for param in param_list:
                        # save value of  fixed parameters
                        if param not in res.param_list:
                            batch_outputs[str(param)].append(model.getParam(param))
                        else:
                            index = res.param_list.index(param)
                            #save only fitted values
                            batch_outputs[param].append(res.pvec[index])
                            if res.stderr is not None and \
                                len(res.stderr) == len(res.param_list):
                                item = res.stderr[index]
                                batch_inputs["error on %s" % param].append(item)
                            else:
                                batch_inputs["error on %s" % param].append('-')
                            model.setParam(param, res.pvec[index])
                #fill the batch result with emtpy value if not in the current
                #model
                EMPTY = "-"
                for key in list(batch_outputs.keys()):
                    if key not in param_list and key not in ["Chi2", "Data"]:
                        batch_outputs[key].append(EMPTY)

                self.page_finder[pid].set_batch_result(batch_inputs=batch_inputs,
                                                       batch_outputs=batch_outputs)

                cpage = self.fit_panel.get_page_by_id(pid)
                cpage._on_fit_complete()
                self.page_finder[pid][data.id].set_result(res)
                fitproblem = self.page_finder[pid][data.id]
                qmin, qmax = fitproblem.get_range()
                plot_result = False
                if correct_result:
                    if not is_data2d:
                        self._complete1D(x=data.x[res.index], y=res.theory, page_id=pid,
                                         elapsed=None,
                                         index=res.index, model=model,
                                         weight=None, fid=data.id,
                                         toggle_mode_on=False, state=None,
                                         data=data, update_chisqr=False,
                                         source='fit', plot_result=plot_result)
                    else:
                        self._complete2D(image=new_theory, data=data,
                                         model=model,
                                         page_id=pid, elapsed=None,
                                         index=res.index,
                                         qmin=qmin,
                                         qmax=qmax, fid=data.id, weight=None,
                                         toggle_mode_on=False, state=None,
                                         update_chisqr=False,
                                         source='fit', plot_result=plot_result)
                self.on_set_batch_result(page_id=pid,
                                         fid=data.id,
                                         batch_outputs=batch_outputs,
                                         batch_inputs=batch_inputs)

        evt = StatusEvent(status=msg, error="info", type="stop")
        wx.PostEvent(self.parent, evt)
        # Remove parameters that are not shown
        cpage = self.fit_panel.get_page_by_id(uid)
        tbatch_outputs = {}
        shownkeystr = cpage.get_copy_params()
        for key in list(batch_outputs.keys()):
            if key in ["Chi2", "Data"] or shownkeystr.count(key) > 0:
                tbatch_outputs[key] = batch_outputs[key]

        wx.CallAfter(self.parent.on_set_batch_result, tbatch_outputs,
                     batch_inputs, self.sub_menu)

    def on_set_batch_result(self, page_id, fid, batch_outputs, batch_inputs):
        """
        """
        pid = page_id
        if fid not in self.page_finder[pid]:
            return
        fitproblem = self.page_finder[pid][fid]
        index = self.page_finder[pid].nbr_residuals_computed - 1
        residuals = fitproblem.get_residuals()
        theory_data = fitproblem.get_theory_data()
        data = fitproblem.get_fit_data()
        model = fitproblem.get_model()
        #fill batch result information
        if "Data" not in list(batch_outputs.keys()):
            batch_outputs["Data"] = []
        from sas.sasgui.guiframe.data_processor import BatchCell
        cell = BatchCell()
        cell.label = data.name
        cell.value = index

        if theory_data is not None:
            #Suucessful fit
            theory_data.id = wx.NewId()
            theory_data.name = model.name + "[%s]" % str(data.name)
            if issubclass(theory_data.__class__, Data2D):
                group_id = wx.NewId()
                theory_data.group_id = group_id
                if group_id not in theory_data.list_group_id:
                    theory_data.list_group_id.append(group_id)

            try:
                # associate residuals plot
                if issubclass(residuals.__class__, Data2D):
                    group_id = wx.NewId()
                    residuals.group_id = group_id
                    if group_id not in residuals.list_group_id:
                        residuals.list_group_id.append(group_id)
                batch_outputs["Chi2"][index].object = [residuals]
            except:
                pass

        cell.object = [data, theory_data]
        batch_outputs["Data"].append(cell)
        for key, value in data.meta_data.items():
            if key not in list(batch_inputs.keys()):
                batch_inputs[key] = []
            #if key.lower().strip() != "loader":
            batch_inputs[key].append(value)
        param = "temperature"
        if hasattr(data.sample, param):
            if param not in  list(batch_inputs.keys()):
                batch_inputs[param] = []
            batch_inputs[param].append(data.sample.temperature)

    def _fit_completed(self, result, page_id, batch_outputs,
                       batch_inputs=None, pars=None, elapsed=None):
        """
        Display result of the fit on related panel(s).
        :param result: list of object generated when fit ends
        :param pars: list of names of parameters fitted
        :param page_id: list of page ids which called fit function
        :param elapsed: time spent at the fitting level
        """
        t1 = time.time()
        str_time = time.strftime("%a, %d %b %Y %H:%M:%S ", time.localtime(t1))
        msg = "Fit completed on %s \n" % str_time
        msg += "Duration time: %s s.\n" % str(elapsed)
        evt = StatusEvent(status=msg, info="info", type="stop")
        wx.PostEvent(self.parent, evt)
        wx.PostEvent(self.result_panel, PlotResultEvent(result=result))
        wx.CallAfter(self._update_fit_button, page_id)
        result = result[0]
        self.fit_thread_list = {}
        if page_id is None:
            page_id = []
        ## fit more than 1 model at the same time
        try:
            index = 0
            # Update potential simfit page(s)
            if self.sim_page is not None:
                self.sim_page._on_fit_complete()
            if self.batch_page:
                self.batch_page._on_fit_complete()
            # Update all fit pages
            for uid in page_id:
                res = result[index]
                fit_msg = res.mesg
                if res.fitness is None or \
                    not np.isfinite(res.fitness) or \
                        np.any(res.pvec is None) or \
                    not np.all(np.isfinite(res.pvec)):
                    fit_msg += "\nFitting did not converge!!!"
                    wx.CallAfter(self._update_fit_button, page_id)
                else:
                    #set the panel when fit result are float not list
                    if res.pvec.__class__ == np.float64:
                        pvec = [res.pvec]
                    else:
                        pvec = res.pvec
                    if res.stderr.__class__ == np.float64:
                        stderr = [res.stderr]
                    else:
                        stderr = res.stderr
                    cpage = self.fit_panel.get_page_by_id(uid)
                    # Make sure we got all results
                    #(CallAfter is important to MAC)
                    try:
                        #if res is not None:
                        wx.CallAfter(cpage.onsetValues, res.fitness,
                                     res.param_list,
                                     pvec, stderr)
                        index += 1
                        wx.CallAfter(cpage._on_fit_complete)
                    except KeyboardInterrupt:
                        fit_msg += "\nSingular point: Fitting stopped."
                    except:
                        fit_msg += "\nSingular point: Fitting error occurred."
                if fit_msg:
                   evt = StatusEvent(status=fit_msg, info="warning", type="stop")
                   wx.PostEvent(self.parent, evt)

        except:
            msg = ("Fit completed but the following error occurred: %s"
                   % sys.exc_info()[1])
            #import traceback; msg = "\n".join((traceback.format_exc(), msg))
            evt = StatusEvent(status=msg, info="warning", type="stop")
            wx.PostEvent(self.parent, evt)

    def _update_fit_button(self, page_id):
        """
        Update Fit button when fit stopped

        : parameter page_id: fitpage where the button is
        """
        if page_id.__class__.__name__ != 'list':
            page_id = [page_id]
        for uid in page_id:
            page = self.fit_panel.get_page_by_id(uid)
            page._on_fit_complete()

    def _on_show_panel(self, event):
        """
        """
        pass

    def on_reset_batch_flag(self, event):
        """
        Set batch_reset_flag
        """
        event.Skip()
        if self.menu1 is None:
            return
        menu_item = self.menu1.FindItemById(self.id_reset_flag)
        flag = menu_item.IsChecked()
        if not flag:
            menu_item.Check(False)
            self.batch_reset_flag = True
        else:
            menu_item.Check(True)
            self.batch_reset_flag = False

        ## post a message to status bar
        msg = "Set Chain Fitting: %s" % str(not self.batch_reset_flag)
        wx.PostEvent(self.parent, StatusEvent(status=msg))


    def _on_slicer_event(self, event):
        """
        Receive a panel as event and send it to guiframe

        :param event: event containing a panel

        """
        if event.panel is not None:
            self.slicer_panels.append(event.panel)
            # Set group ID if available
            event_id = self.parent.popup_panel(event.panel)
            event.panel.uid = event_id
            self.mypanels.append(event.panel)

    def _onclearslicer(self, event):
        """
        Clear the boxslicer when close the panel associate with this slicer
        """
        name = event.GetEventObject().frame.GetTitle()
        for panel in self.slicer_panels:
            if panel.window_caption == name:

                for item in self.parent.panels:
                    if hasattr(self.parent.panels[item], "uid"):
                        if self.parent.panels[item].uid == panel.base.uid:
                            self.parent.panels[item].onClearSlicer(event)
                            #self.parent._mgr.Update()
                            break
                break

    def _on_model_panel(self, evt):
        """
        react to model selection on any combo box or model menu.plot the model

        :param evt: wx.combobox event

        """
        model = evt.model
        uid = evt.uid
        qmin = evt.qmin
        qmax = evt.qmax
        caption = evt.caption
        enable_smearer = evt.enable_smearer
        if model is None:
            return
        if uid not in list(self.page_finder.keys()):
            return
        # save the name containing the data name with the appropriate model
        self.page_finder[uid].set_model(model)
        self.page_finder[uid].enable_smearing(enable_smearer)
        self.page_finder[uid].set_range(qmin=qmin, qmax=qmax)
        self.page_finder[uid].set_fit_tab_caption(caption=caption)
        if self.sim_page is not None and not self.batch_on:
            self.sim_page.draw_page()
        if self.batch_page is not None and self.batch_on:
            self.batch_page.draw_page()

    def _update1D(self, x, output):
        """
        Update the output of plotting model 1D
        """
        msg = "Plot updating ... "
        wx.PostEvent(self.parent, StatusEvent(status=msg, type="update"))

    def create_theory_1D(self, x, y, page_id, model, data, state,
                         data_description, data_id, dy=None):
        """
            Create a theory object associate with an existing Data1D
            and add it to the data manager.
            @param x: x-values of the data
            @param y: y_values of the data
            @param page_id: fit page ID
            @param model: model used for fitting
            @param data: Data1D object to create the theory for
            @param state: model state
            @param data_description: title to use in the data manager
            @param data_id: unique data ID
        """
        new_plot = Data1D(x=x, y=y)
        if dy is None:
            new_plot.is_data = False
            new_plot.dy = np.zeros(len(y))
            # If this is a theory curve, pick the proper symbol to make it a curve
            new_plot.symbol = GUIFRAME_ID.CURVE_SYMBOL_NUM
        else:
            new_plot.is_data = True
            new_plot.dy = dy
        new_plot.interactive = True
        new_plot.dx = None
        new_plot.dxl = None
        new_plot.dxw = None
        _yaxis, _yunit = data.get_yaxis()
        _xaxis, _xunit = data.get_xaxis()
        new_plot.title = data.name
        new_plot.group_id = data.group_id
        if new_plot.group_id is None:
            new_plot.group_id = data.group_id
        new_plot.id = data_id
        # Find if this theory was already plotted and replace that plot given
        # the same id
        self.page_finder[page_id].get_theory_data(fid=data.id)

        if data.is_data:
            data_name = str(data.name)
        else:
            data_name = str(model.__class__.__name__)

        new_plot.name = data_description + " [" + data_name + "]"
        new_plot.xaxis(_xaxis, _xunit)
        new_plot.yaxis(_yaxis, _yunit)
        self.page_finder[page_id].set_theory_data(data=new_plot,
                                                  fid=data.id)
        self.parent.update_theory(data_id=data.id, theory=new_plot,
                                   state=state)
        return new_plot

    def _complete1D(self, x, y, page_id, elapsed, index, model,
                    weight=None, fid=None,
                    toggle_mode_on=False, state=None,
                    data=None, update_chisqr=True,
                    source='model', plot_result=True,
                    unsmeared_model=None, unsmeared_data=None,
                    unsmeared_error=None, sq_model=None, pq_model=None):
        """
            Complete plotting 1D data
            @param unsmeared_model: fit model, without smearing
            @param unsmeared_data: data, rescaled to unsmeared model
            @param unsmeared_error: data error, rescaled to unsmeared model
        """
        try:
            np.nan_to_num(y)
            new_plot = self.create_theory_1D(x, y, page_id, model, data, state,
                                             data_description=model.name,
                                             data_id=str(page_id) + " " + data.name)
            if unsmeared_model is not None:
                self.create_theory_1D(x, unsmeared_model, page_id, model, data, state,
                                      data_description=model.name + " unsmeared",
                                      data_id=str(page_id) + " " + data.name + " unsmeared")

                if unsmeared_data is not None and unsmeared_error is not None:
                    self.create_theory_1D(x, unsmeared_data, page_id, model, data, state,
                                          data_description="Data unsmeared",
                                          data_id="Data  " + data.name + " unsmeared",
                                          dy=unsmeared_error)
            # Comment this out until we can get P*S models with correctly populated parameters
            #if sq_model is not None and pq_model is not None:
            #    self.create_theory_1D(x, sq_model, page_id, model, data, state,
            #                          data_description=model.name + " S(q)",
            #                          data_id=str(page_id) + " " + data.name + " S(q)")
            #    self.create_theory_1D(x, pq_model, page_id, model, data, state,
            #                          data_description=model.name + " P(q)",
            #                          data_id=str(page_id) + " " + data.name + " P(q)")

            current_pg = self.fit_panel.get_page_by_id(page_id)
            title = new_plot.title
            batch_on = self.fit_panel.get_page_by_id(page_id).batch_on
            if not batch_on:
                wx.PostEvent(self.parent, NewPlotEvent(plot=new_plot,
                                            title=str(title)))
            elif plot_result:
                top_data_id = self.fit_panel.get_page_by_id(page_id).data.id
                if data.id == top_data_id:
                    wx.PostEvent(self.parent, NewPlotEvent(plot=new_plot,
                                            title=str(title)))
            caption = current_pg.window_caption
            self.page_finder[page_id].set_fit_tab_caption(caption=caption)

            self.page_finder[page_id].set_theory_data(data=new_plot,
                                                      fid=data.id)
            if toggle_mode_on:
                wx.PostEvent(self.parent,
                             NewPlotEvent(group_id=str(page_id) + " Model2D",
                                          action="Hide"))
            else:
                if update_chisqr:
                    wx.PostEvent(current_pg,
                                 Chi2UpdateEvent(output=self._cal_chisqr(
                                                                data=data,
                                                                fid=fid,
                                                                weight=weight,
                                                            page_id=page_id,
                                                            index=index)))
                else:
                    self._plot_residuals(page_id=page_id, data=data, fid=fid,
                                         index=index, weight=weight)

            msg = "Computation  completed!"
            wx.PostEvent(self.parent, StatusEvent(status=msg, type="stop"))
        except:
            raise

    def _calc_exception(self, etype, value, tb):
        """
        Handle exception from calculator by posting it as an error.
        """
        logger.error("".join(traceback.format_exception(etype, value, tb)))
        msg = traceback.format_exception(etype, value, tb, limit=1)
        evt = StatusEvent(status="".join(msg), type="stop", info="error")
        wx.PostEvent(self.parent, evt)

    def _update2D(self, output, time=None):
        """
        Update the output of plotting model
        """
        msg = "Plot updating ... "
        wx.PostEvent(self.parent, StatusEvent(msg, type="update"))

    def _complete2D(self, image, data, model, page_id, elapsed, index, qmin,
                qmax, fid=None, weight=None, toggle_mode_on=False, state=None,
                     update_chisqr=True, source='model', plot_result=True):
        """
        Complete get the result of modelthread and create model 2D
        that can be plot.
        """
        np.nan_to_num(image)
        new_plot = Data2D(image=image, err_image=data.err_data)
        new_plot.name = model.name + '2d'
        new_plot.title = "Analytical model 2D "
        new_plot.id = str(page_id) + " " + data.name
        new_plot.group_id = str(page_id) + " Model2D"
        new_plot.detector = data.detector
        new_plot.source = data.source
        new_plot.is_data = False
        new_plot.qx_data = data.qx_data
        new_plot.qy_data = data.qy_data
        new_plot.q_data = data.q_data
        new_plot.mask = data.mask
        ## plot boundaries
        new_plot.ymin = data.ymin
        new_plot.ymax = data.ymax
        new_plot.xmin = data.xmin
        new_plot.xmax = data.xmax
        title = data.title

        new_plot.is_data = False
        if data.is_data:
            data_name = str(data.name)
        else:
            data_name = str(model.__class__.__name__) + '2d'

        if len(title) > 1:
            new_plot.title = "Model2D for %s " % model.name + data_name
        new_plot.name = model.name + " [" + \
                                    data_name + "]"
        theory_data = deepcopy(new_plot)

        self.page_finder[page_id].set_theory_data(data=theory_data,
                                                  fid=data.id)
        self.parent.update_theory(data_id=data.id,
                                       theory=new_plot,
                                       state=state)
        current_pg = self.fit_panel.get_page_by_id(page_id)
        title = new_plot.title
        if not source == 'fit' and plot_result:
            wx.PostEvent(self.parent, NewPlotEvent(plot=new_plot,
                                               title=title))
        if toggle_mode_on:
            wx.PostEvent(self.parent,
                             NewPlotEvent(group_id=str(page_id) + " Model1D",
                                               action="Hide"))
        else:
            # Chisqr in fitpage
            if update_chisqr:
                wx.PostEvent(current_pg,
                             Chi2UpdateEvent(output=self._cal_chisqr(data=data,
                                                                    weight=weight,
                                                                    fid=fid,
                                                         page_id=page_id,
                                                         index=index)))
            else:
                self._plot_residuals(page_id=page_id, data=data, fid=fid,
                                      index=index, weight=weight)
        msg = "Computation  completed!"
        wx.PostEvent(self.parent, StatusEvent(status=msg, type="stop"))

    def _draw_model2D(self, model, page_id, qmin,
                      qmax,
                      data=None, smearer=None,
                      description=None, enable2D=False,
                      state=None,
                      fid=None,
                      weight=None,
                      toggle_mode_on=False,
                       update_chisqr=True, source='model'):
        """
        draw model in 2D

        :param model: instance of the model to draw
        :param description: the description of the model
        :param enable2D: when True allows to draw model 2D
        :param qmin: the minimum value to  draw model 2D
        :param qmax: the maximum value to draw model 2D
        :param qstep: the number of division of Qx and Qy of the model to draw

        """
        if not enable2D:
            return None
        try:
            from .model_thread import Calc2D
            ## If a thread is already started, stop it
            if (self.calc_2D is not None) and self.calc_2D.isrunning():
                self.calc_2D.stop()
                ## stop just raises a flag to tell the thread to kill
                ## itself -- see the fix in Calc1D implemented to fix
                ## an actual problem.  Seems the fix should also go here
                ## and may be the cause of other noted instabilities
                ##
                ##    -PDB August 12, 2014 
                while self.calc_2D.isrunning():
                    time.sleep(0.1)
            self.calc_2D = Calc2D(model=model,
                                  data=data,
                                  page_id=page_id,
                                  smearer=smearer,
                                  qmin=qmin,
                                  qmax=qmax,
                                  weight=weight,
                                  fid=fid,
                                  toggle_mode_on=toggle_mode_on,
                                  state=state,
                                  completefn=self._complete2D,
                                  update_chisqr=update_chisqr,
                                  exception_handler=self._calc_exception,
                                  source=source)
            self.calc_2D.queue()
        except:
            raise

    def _draw_model1D(self, model, page_id, data,
                      qmin, qmax, smearer=None,
                state=None,
                weight=None,
                fid=None,
                toggle_mode_on=False, update_chisqr=True, source='model',
                enable1D=True):
        """
        Draw model 1D from loaded data1D

        :param data: loaded data
        :param model: the model to plot

        """
        if not enable1D:
            return
        try:
            from .model_thread import Calc1D
            ## If a thread is already started, stop it
            if (self.calc_1D is not None) and self.calc_1D.isrunning():
                self.calc_1D.stop()
                ## stop just raises the flag -- the thread is supposed to 
                ## then kill itself but cannot.  Paul Kienzle came up with
                ## this fix to prevent threads from stepping on each other
                ## which was causing a simple custom plugin model to crash
                ##Sasview.
                ## We still don't know why the fit sometimes lauched a second
                ## thread -- something which should also be investigated.
                ## The thread approach was implemented in order to be able
                ## to lauch a computation in a separate thread from the GUI so
                ## that the GUI can still respond to user input including
                ## a request to stop the computation.
                ## It seems thus that the whole thread approach used here
                ## May need rethinking  
                ##
                ##    -PDB August 12, 2014
                while self.calc_1D.isrunning():
                    time.sleep(0.1)
            self.calc_1D = Calc1D(data=data,
                                  model=model,
                                  page_id=page_id,
                                  qmin=qmin,
                                  qmax=qmax,
                                  smearer=smearer,
                                  state=state,
                                  weight=weight,
                                  fid=fid,
                                  toggle_mode_on=toggle_mode_on,
                                  completefn=self._complete1D,
                                  #updatefn = self._update1D,
                                  update_chisqr=update_chisqr,
                                  exception_handler=self._calc_exception,
                                  source=source)
            self.calc_1D.queue()
        except:
            msg = " Error occurred when drawing %s Model 1D: " % model.name
            msg += " %s" % sys.exc_info()[1]
            wx.PostEvent(self.parent, StatusEvent(status=msg))

    def _cal_chisqr(self, page_id, data, weight, fid=None, index=None):
        """
        Get handy Chisqr using the output from draw1D and 2D,
        instead of calling expansive CalcChisqr in guithread
        """
        try:
            data_copy = deepcopy(data)
        except:
            return
        # default chisqr
        chisqr = None
        #to compute chisq make sure data has valid data
        # return None if data is None
        if not check_data_validity(data_copy) or data_copy is None:
            return chisqr

        # Get data: data I, theory I, and data dI in order
        if data_copy.__class__.__name__ == "Data2D":
            if index is None:
                index = np.ones(len(data_copy.data), dtype=bool)
            if weight is not None:
                data_copy.err_data = weight
            # get rid of zero error points
            index = index & (data_copy.err_data != 0)
            index = index & (np.isfinite(data_copy.data))
            fn = data_copy.data[index]
            theory_data = self.page_finder[page_id].get_theory_data(fid=data_copy.id)
            if theory_data is None:
                return chisqr
            gn = theory_data.data[index]
            en = data_copy.err_data[index]
        else:
            # 1 d theory from model_thread is only in the range of index
            if index is None:
                index = np.ones(len(data_copy.y), dtype=bool)
            if weight is not None:
                data_copy.dy = weight
            if data_copy.dy is None or data_copy.dy == []:
                dy = np.ones(len(data_copy.y))
            else:
                ## Set consistently w/AbstractFitengine:
                # But this should be corrected later.
                dy = deepcopy(data_copy.dy)
                dy[dy == 0] = 1
            fn = data_copy.y[index]

            theory_data = self.page_finder[page_id].get_theory_data(fid=data_copy.id)
            if theory_data is None:
                return chisqr
            gn = theory_data.y
            en = dy[index]

        # residual
        try:
            res = (fn - gn) / en
        except ValueError:
            print("Unmatch lengths %s, %s, %s" % (len(fn), len(gn), len(en)))
            return

        residuals = res[np.isfinite(res)]
        # get chisqr only w/finite
        chisqr = np.average(residuals * residuals)

        self._plot_residuals(page_id=page_id, data=data_copy,
                             fid=fid,
                             weight=weight, index=index)

        return chisqr

    def _plot_residuals(self, page_id, weight, fid=None,
                        data=None, index=None):
        """
        Plot the residuals

        :param data: data
        :param index: index array (bool)
        : Note: this is different from the residuals in cal_chisqr()
        """
        data_copy = deepcopy(data)
        # Get data: data I, theory I, and data dI in order
        if data_copy.__class__.__name__ == "Data2D":
            # build residuals
            residuals = Data2D()
            #residuals.copy_from_datainfo(data)
            # Not for trunk the line below, instead use the line above
            data_copy.clone_without_data(len(data_copy.data), residuals)
            residuals.data = None
            fn = data_copy.data
            theory_data = self.page_finder[page_id].get_theory_data(fid=data_copy.id)
            gn = theory_data.data
            if weight is None:
                en = data_copy.err_data
            else:
                en = weight
            residuals.data = (fn - gn) / en
            residuals.qx_data = data_copy.qx_data
            residuals.qy_data = data_copy.qy_data
            residuals.q_data = data_copy.q_data
            residuals.err_data = np.ones(len(residuals.data))
            residuals.xmin = min(residuals.qx_data)
            residuals.xmax = max(residuals.qx_data)
            residuals.ymin = min(residuals.qy_data)
            residuals.ymax = max(residuals.qy_data)
            residuals.q_data = data_copy.q_data
            residuals.mask = data_copy.mask
            residuals.scale = 'linear'
            # check the lengths
            if len(residuals.data) != len(residuals.q_data):
                return
        else:
            # 1 d theory from model_thread is only in the range of index
            if data_copy.dy is None or data_copy.dy == []:
                dy = np.ones(len(data_copy.y))
            else:
                if weight is None:
                    dy = np.ones(len(data_copy.y))
                ## Set consitently w/AbstractFitengine:
                ## But this should be corrected later.
                else:
                    dy = weight
                dy[dy == 0] = 1
            fn = data_copy.y[index]
            theory_data = self.page_finder[page_id].get_theory_data(fid=data_copy.id)
            gn = theory_data.y
            en = dy[index]
            # build residuals
            residuals = Data1D()
            try:
                residuals.y = (fn - gn) / en
            except:
                msg = "ResidualPlot Error: different # of data points in theory"
                wx.PostEvent(self.parent, StatusEvent(status=msg, info="error"))
                residuals.y = (fn - gn[index]) / en
            residuals.x = data_copy.x[index]
            residuals.dy = np.ones(len(residuals.y))
            residuals.dx = None
            residuals.dxl = None
            residuals.dxw = None
            residuals.ytransform = 'y'
            # For latter scale changes 
            residuals.xaxis('\\rm{Q} ', 'A^{-1}')
            residuals.yaxis('\\rm{Residuals} ', 'normalized')
        theory_name = str(theory_data.name.split()[0])
        new_plot = residuals
        new_plot.name = "Residuals for " + str(theory_name) + "[" + \
                        str(data.name) + "]"
        ## allow to highlight data when plotted
        new_plot.interactive = True
        ## when 2 data have the same id override the 1 st plotted
        new_plot.id = "res" + str(data_copy.id) + str(theory_name)
        ##group_id specify on which panel to plot this data
        group_id = self.page_finder[page_id].get_graph_id()
        if group_id is None:
            group_id = data.group_id
        new_plot.group_id = "res" + str(group_id)
        #new_plot.is_data = True
        ##post data to plot
        title = new_plot.name
        self.page_finder[page_id].set_residuals(residuals=new_plot,
                                                fid=data.id)
        self.parent.update_theory(data_id=data.id, theory=new_plot)
        batch_on = self.fit_panel.get_page_by_id(page_id).batch_on
        if not batch_on:
            wx.PostEvent(self.parent, NewPlotEvent(plot=new_plot, title=title))
