"""
This module contains the interfaces to the different tabs and dialogs in the ClearMap GUI.
"""
import os
from PyQt5.QtWidgets import QWhatsThis
from ClearMap.Utils.exceptions import MissingRequirementException
from ClearMap.gui.gui_utils import create_clearmap_widget
[docs]
class GenericUi:
    """
    The first layer of interface. This is not implemented directly but is the base class
    of GenericTab and GenericDialog, themselves interfaces
    """
    def __init__(self, main_window, name, ui_file_name, widget_class_name):
        """
        Parameters
        ----------
        main_window: ClearMapGui
        name: str
        ui_file_name: str
        widget_class_name: str
        """
        self.main_window = main_window
        self.name = name
        self.ui_file_name = ui_file_name
        self.widget_class_name = widget_class_name
        self.ui = None
        self.params = None
        self.progress_watcher = self.main_window.progress_watcher
[docs]
    def init_ui(self):
        self.ui = create_clearmap_widget(f'{self.ui_file_name}.ui', patch_parent_class=self.widget_class_name)
        self.patch_button_boxes() 
[docs]
    def set_params(self, *args):
        """
        Set the params object which links the UI and the configuration file
        Parameters
        ----------
        args
        Returns
        -------
        """
        raise NotImplementedError() 
[docs]
    def load_config_to_gui(self):
        """
        Set every control on the UI to the value in the params
        Returns
        -------
        """
        self.params.cfg_to_ui() 
[docs]
    def set_progress_watcher(self, watcher):
        pass 
 
[docs]
class GenericDialog(GenericUi):
    """
    Interface to any dialog associated with parameters
    """
    def __init__(self, main_window, name, file_name):
        super().__init__(main_window, name, file_name, 'QDialog')
[docs]
    def init_ui(self):
        super().init_ui()
        self.ui.setWindowTitle(self.name.title()) 
[docs]
    def set_params(self, *args):
        raise NotImplementedError() 
 
[docs]
class GenericTab(GenericUi):
    """
    The interface to all tab managers.
    A tab manager includes a tab widget, the associated parameters and
    potentially a processor object which handles the computations.
    """
    def __init__(self, main_window, name, tab_idx, ui_file_name):
        """
        Parameters
        ----------
        main_window: ClearMapGui
        name: str
        tab_idx: int
        ui_file_name: str
        """
        super().__init__(main_window, name, ui_file_name, 'QTabWidget')
        self.processing_type = None
        self.tab_idx = tab_idx
        self.minimum_width = 200  # REFACTOR:
        self.advanced_control_names = []
[docs]
    def init_ui(self):
        super().init_ui()
        self.ui.setMinimumWidth(self.minimum_width)
        self.main_window.tabWidget.removeTab(self.tab_idx)
        self.main_window.tabWidget.insertTab(self.tab_idx, self.ui, self.name.title())
        if hasattr(self.ui, 'advancedCheckBox'):
            for ctrl_name in self.advanced_control_names:
                ctrl = getattr(self.ui, ctrl_name)
                ctrl.setVisible(False)
            self.ui.advancedCheckBox.stateChanged.connect(self.handle_advanced_checked) 
[docs]
    def handle_advanced_checked(self):
        """
        Activate the *advanced* mode which will display more controls
        Returns
        -------
        """
        checked = self.ui.advancedCheckBox.isChecked()
        for ctrl_name in self.advanced_control_names:
            ctrl = getattr(self.ui, ctrl_name)
            ctrl.setVisible(checked) 
[docs]
    def set_params(self, *args):
        """
        Set the params object which links the UI and the configuration file
        Parameters
        ----------
        args
        Returns
        -------
        """
        raise NotImplementedError() 
[docs]
    def read_configs(self, cfg_path):  # FIXME: REFACTOR: parse_configs
        """
        Read the configuration file associated with the params from the filesystem
        Parameters
        ----------
        cfg_path
        Returns
        -------
        """
        self.params.read_configs(cfg_path) 
[docs]
    def fix_config(self):  # TODO: check if could make part of self.params may not be possible since not set
        """
        Amend the config for the tabs that required live patching the config
        Returns
        -------
        """
        self.params.fix_cfg_file(self.params.config_path) 
[docs]
    def disable(self):
        """
        Disable this tab (UI element)
        Returns
        -------
        """
        self.ui.setEnabled(False) 
[docs]
    def step_exists(self, step_name, file_list):
        """
        Check that prerequisite step step_name has been run and produced
        the outputs in file_list
        Parameters
        ----------
        step_name
        file_list
        Returns
        -------
        """
        if isinstance(file_list, str):
            file_list = [file_list]
        for f_path in file_list:
            if not os.path.exists(f_path):
                self.main_window.print_error_msg(f'Missing {step_name} file {f_path}. '
                                                 f'Please ensure {step_name} is run first.')
                return False
        return True 
[docs]
    def setup_workers(self):
        """
        Setup the optional workers (which handle the computations) associated with this tab
        Returns
        -------
        """
        pass 
[docs]
    def display_whats_this(self, widget):
        """
        Utility function to display the detailed *whatsThis* message
        associated with the control
        Parameters
        ----------
        widget
        Returns
        -------
        """
        QWhatsThis.showText(widget.pos(), widget.whatsThis(), widget) 
[docs]
    def connect_whats_this(self, info_btn, whats_this_ctrl):
        """
        Utility function to bind the info button to the display of
        the detailed *whatsThis* message associated with the control
        Parameters
        ----------
        widget
        Returns
        -------
        """
        info_btn.clicked.connect(lambda: self.display_whats_this(whats_this_ctrl)) 
[docs]
    def wrap_step(self, task_name, func, step_args=None, step_kw_args=None, n_steps=1, abort_func=None, save_cfg=True,
                  nested=True, close_when_done=True, main_thread=False):  # FIXME: saving config should be default
        """
        This function wraps the computations of the tab. It should start a new thread to ensure that
        the UI remains responsive (unless main_thread is set to True).
        It will also start a progress dialog
        Parameters
        ----------
        task_name : str
            The name of the task to be displayed
        func : function
            The function to run
        step_args : list
            The positional arguments to func
        step_kw_args : dict
            The keyword arguments to func
        n_steps : int
            The number of top level steps in the computation. This will be disabled if nested is False.
        abort_func : function
            The function to trigger to abort the execution of the computation (bound to the abort button)
        save_cfg : bool
            Whether to save the configuration to disk before running the computation.
            This is usually the right choice to ensure that the config reloaded by func is up to date.
        nested : bool
            Whether the computation has 2 levels of progress
        close_when_done : bool
            Close the progress dialog when func has finished executing
        main_thread : bool
            Whether to run in the main thread. Default is False to ensure that the UI thread remains
            responsive, a new thread will be spawned.
        Returns
        -------
        """
        if step_args is None:
            step_args = []
        if step_kw_args is None:
            step_kw_args = {}
        if save_cfg:
            self.params.ui_to_cfg()
        if task_name:
            if not nested:
                n_steps = 0
            self.main_window.make_progress_dialog(task_name, n_steps=n_steps, abort=abort_func)
        try:
            if main_thread:
                func(*step_args, **step_kw_args)
            else:
                self.main_window.wrap_in_thread(func, *step_args, **step_kw_args)
        except MissingRequirementException as ex:
            self.main_window.print_error_msg(ex)
            self.main_window.popup(str(ex), base_msg=f'Could not run operation {func.__name__}', print_warning=False)
        finally:
            if self.preprocessor is not None and self.preprocessor.workspace is not None:  # WARNING: hacky
                self.preprocessor.workspace.executor = None
            if close_when_done:
                self.progress_watcher.finish()
            else:
                msg = f'{self.progress_watcher.main_step_name} finished'
                self.main_window.print_status_msg(msg)
                self.main_window.log_progress(f'    : {msg}') 
 
[docs]
class PostProcessingTab(GenericTab):
    """
    Interface to all the tab managers in charge of post processing the data (e.e. typically detecting relevant info in the data).
    One particularity of a post processing tab manager is that it includes the corresponding pre processor.
    A tab manager includes a tab widget, the associated parameters
    and potentially a processor object which handles the computations.
    """
    def __init__(self, main_window, name, tab_idx, ui_file_name):
        super().__init__(main_window, name, tab_idx, ui_file_name)
        self.preprocessor = None
        self.processing_type = 'post'
[docs]
    def set_params(self, sample_params, alignment_params):
        """
        Set the params object which links the UI and the configuration file
        Parameters
        ----------
        args
        Returns
        -------
        """
        raise NotImplementedError() 
[docs]
    def setup_preproc(self, pre_processor):
        """
        Set the PreProcessor object associated with the sample
        Parameters
        ----------
        pre_processor : PreProcessor
        Returns
        -------
        """
        self.preprocessor = pre_processor 
[docs]
    def plot_slicer(self, slicer_prefix, tab, params):
        """
        Display the orthoslicer to pick a subset of 3D data.
        This is typically used to create a small  dataset to test parameters
        for long running operations before analysing the whole sample
        Parameters
        ----------
        slicer_prefix
        tab
        params
        Returns
        -------
        """
        self.main_window.clear_plots()
        # if self.preprocessor.was_registered:
        #     img = mhd_read(self.preprocessor.annotation_file_path)  # FIXME: does not work (probably compressed format)
        # else:
        img = self.preprocessor.workspace.source('resampled')
        self.main_window.ortho_viewer.setup(img, params, parent=self.main_window)
        dvs = self.main_window.ortho_viewer.plot_orthogonal_views()
        ranges = [[params.reverse_scale_axis(v, ax) for v in vals] for ax, vals in zip('xyz', params.slice_tuples)]
        self.main_window.ortho_viewer.update_ranges(ranges)
        self.main_window.setup_plots(dvs, ['x', 'y', 'z'])
        # WARNING: needs to be done after setup
        for axis, ax_max in zip('XYZ', self.preprocessor.raw_stitched_shape):  # FIXME: not always raw stitched
            getattr(tab, f'{slicer_prefix}{axis}RangeMax').setMaximum(ax_max)