from dataclasses import dataclass, field
from typing import List, Union

import pyqtgraph as pg
import pyqtgraph.parametertree as ptree
import pyqtgraph.parametertree.parameterTypes as pTypes

from openlog.core import pint_utilities
from openlog.gui.assay_visualization.config.assay_column_visualization_config import (
    AssayColumnVisualizationConfig,
)
from openlog.gui.assay_visualization.config.assay_visualization_config import (
    AssayVisualizationConfig,
)
from openlog.gui.assay_visualization.stacked.stacked_utils import (
    config_list_default_unit,
    is_config_list_valid_for_stack,
)


@dataclass
class StackedConfiguration:
    """
    StackedConfiguration
    Contains list of AssayColumnVisualizationConfig
    Can also access plot and plot item if configuration created from visualization widget

    """

    def __init__(self) -> None:

        self.is_visible: bool = True
        self.plot: pg.PlotWidget = None
        self.name: str = ""
        self.config_list: List[AssayColumnVisualizationConfig] = []
        self.unit_parameter = pTypes.SimpleParameter(
            name="Conversion",
            type="str",
            value="",
            default="",
        )
        self.unit_parameter.sigValueChanged.connect(self._update_conversion_unit)
        self._conversion_unit = ""
        self.optionnal_global_params = ["Log", "X grid", "Y grid"]
        self.global_param = None
        from openlog.plugins.manager import get_plugin_manager

        self.mixed_units_validator = (
            get_plugin_manager().get_heterogenous_plots_plugin().validator
        )
        self.plot_widget_factory = (
            get_plugin_manager().get_heterogenous_plots_plugin().widget_factory
        )

        self.are_mixed_units = False

    def display_stacked_minimap(self):
        """
        Slot to call AssayPlotItem.display_minimap() method.
        """
        if self.global_param is None:
            self.set_global_param()

        display = self.global_param.child("Minimap").value()
        self.plot.plotItem.minimap.set_stacked_items(self.plot.plotItem.items)
        self.plot.plotItem.display_minimap(display)

    def update_global_param(self) -> None:
        """
        Update global plot options parameter.
        Paramaters are hidden or shown depending of discrete/extended.
        """

        self.global_param.child("Minimap").sigValueChanged.connect(
            self.display_stacked_minimap
        )

        only_discrete = True
        for config in self.config_list:
            if config.is_discrete and config.is_switchable_config:
                if config.as_extended:
                    only_discrete = False
                    break

            if config.is_switchable_config and not config.is_discrete:
                if not config.as_discrete:
                    only_discrete = False
                    break

        # hide individual transformation groups
        for config in self.config_list:
            config.transformation_group.hide()
            if config.is_discrete and config.is_switchable_config:
                if config.as_extended:
                    config.extended_config.transformation_group.hide()

            if config.is_switchable_config and not config.is_discrete:
                if config.as_discrete:
                    config.discrete_configuration.transformation_group.hide()

        # synchronize log, x grid and y grid + show/hide global options parameters

        if only_discrete:

            for name in self.optionnal_global_params:
                self.global_param.child(name).sigValueChanged.connect(
                    self.synchronize_discrete_param
                )

            for name in self.optionnal_global_params:
                self.global_param.child(name).setValue(False)
                self.global_param.child(name).show()
        else:

            for name in self.optionnal_global_params:
                self.global_param.child(name).setValue(False)
                self.global_param.child(name).hide()

        self.display_stacked_minimap()

    def set_global_param(self) -> None:
        """
        Global plot options : minimap, logarithm, x and y grids.
        If only discrete plots, all parameters are visible.
        If not, only minimap.

        """

        parameters = ["Minimap"] + self.optionnal_global_params
        if self.global_param is None:

            if len(self.config_list) > 0:

                param = ptree.Parameter.create(name="Plot options")
                for elt in parameters:
                    p = ptree.Parameter.create(
                        name=elt, type="bool", value=False, default=False
                    )

                    param.addChild(p)

                self.global_param = param

        # connect switch button signal to update_global_param
        for config in self.config_list:

            config.btn.sigActivated.connect(self.update_global_param)
            # connect to alternative config
            if config.is_discrete:
                config.extended_config.btn.sigActivated.connect(
                    self.update_global_param
                )
            else:
                config.discrete_configuration.btn.sigActivated.connect(
                    self.update_global_param
                )

    def synchronize_discrete_param(self):
        """
        Slot for synchronization between global param and each config's parameters (log, grids)
        """
        # get optionnal params
        params = [children.name() for children in self.global_param.children()]
        for config in self.config_list:
            if config.is_switchable_config and config.is_discrete:
                if "Log" in params:
                    config.log_param.setValue(self.global_param.child("Log").value())
                if "X grid" in params:
                    config.grid_x_param.setValue(
                        self.global_param.child("X grid").value()
                    )
                if "Y grid" in params:
                    config.grid_y_param.setValue(
                        self.global_param.child("Y grid").value()
                    )

            if config.is_switchable_config and not config.is_discrete:
                if config.discrete_configuration is not None:
                    if "Log" in params:
                        config.discrete_configuration.log_param.setValue(
                            self.global_param.child("Log").value()
                        )
                    if "X grid" in params:
                        config.discrete_configuration.grid_x_param.setValue(
                            self.global_param.child("X grid").value()
                        )
                    if "Y grid" in params:
                        config.discrete_configuration.grid_y_param.setValue(
                            self.global_param.child("Y grid").value()
                        )

        self.display_stacked_minimap()

    def set_plot(self, plot: pg.PlotWidget) -> None:
        """
        Define plot containing current stacked data

        Args:
            plot: pg.PlotWidget
        """
        self.plot = plot

    def create_plot_widget(self, domain) -> pg.PlotWidget:
        """
        Create plot widget.
        """
        return self.plot_widget_factory(self, domain, self.config_list)

    def is_valid(self) -> bool:
        """
        Check if StackedConfiguration is valid :
        - only one unit used
        - only one series type used
        - only valid series type used

        Returns: True if StackedConfiguration is valid, False otherwise

        """
        self.are_mixed_units = self.mixed_units_validator(self.config_list)
        return is_config_list_valid_for_stack(self.config_list) or self.are_mixed_units

    def get_pyqtgraph_params(self) -> List[ptree.Parameter]:
        """
        Get pyqtgraph param to display in pyqtgraph ParameterTree

        Returns: List[ptree.Parameter] containing all configuration params

        """
        params = []

        if pint_utilities.is_pint_unit(self._conversion_unit):
            params.append(self.unit_parameter)

        return params

    def _update_conversion_unit(self) -> None:
        new_conversion_unit = self.unit_parameter.value()

        # Check if conversion possible, if not restore previous value
        if not pint_utilities.can_convert(new_conversion_unit, self._conversion_unit):
            self.unit_parameter.setValue(self._conversion_unit)
            return
        elif new_conversion_unit != self._conversion_unit:
            # Propagate unit change to assay column
            for config in self.config_list:
                config.set_conversion_unit(new_conversion_unit)

            self._conversion_unit = new_conversion_unit

            # Update stack plot widget unit
            if self.plot:
                self.plot.update_displayed_unit(self._conversion_unit)

    def add_column_config(self, config: AssayColumnVisualizationConfig) -> None:
        """
        Add assay column configuration

        Args:
            config: AssayColumnVisualizationConfig
        """

        # Check if config already available
        if not self.config_list.count(config):
            self.config_list.append(config)

        # get units of all configs
        default_unit = config_list_default_unit(self.config_list)

        # enable each original config unit parameter
        for col, unit in zip(self.config_list, default_unit):
            col.enable_unit_param_display(unit != "")

    def refresh_unit_param(self):
        """
        Hide or show Unit group in each config.
        If heterogenous plot, each config get its own unit parameter.
        If homogenous plot, one global unit param.
        """
        # each config
        units = config_list_default_unit(self.config_list)
        for config, unit in zip(self.config_list, units):
            config.enable_unit_param_display(unit != "" and len(list(set(units))) > 1)

        if len(list(set(units))) == 1 and units[0] != "":
            self._conversion_unit = units[0]
            self.unit_parameter.setDefault(units[0])
            if not self.unit_parameter.value():
                self.unit_parameter.setValue(units[0])

    def copy_from_config(
        self,
        other: Union[AssayVisualizationConfig, AssayColumnVisualizationConfig],
        only_visible: bool = False,
    ) -> None:
        """
        Copy configuration from another configuration.

        Args:
            other(Union[AssayVisualizationConfig, AssayColumnVisualizationConfig]): configuration to be copy
            only_visible : bool : True if only visible configuration must be used, False for all (default False)
        """
        if only_visible and not self.is_visible:
            return
        if isinstance(other, AssayVisualizationConfig):
            for col in other.column_config.values():
                self.copy_from_column_config(col, only_visible=only_visible)
        if isinstance(other, AssayColumnVisualizationConfig):
            self.copy_from_column_config(other)

    def copy_from_column_config(
        self,
        other: AssayColumnVisualizationConfig,
        only_visible: bool = False,
    ) -> None:
        """
        Copy column configuration from another configuration.

        Args:
            other(AssayColumnVisualizationConfig): configuration to be copy
            only_visible : bool : True if only visible configuration must be used, False for all (default False)
        """
        for col in self.config_list:
            # Update configuration if same assay and column
            if (
                col.assay_name == other.assay_name
                and col.column.name == other.column.name
            ):
                if only_visible and not col.visibility_param.value():
                    continue
                col.copy_from_config(other)


@dataclass
class StackedConfigurationList:
    def __init__(self):
        """
        Container of StackedConfiguration.
        List can be accessed with list attributes.

        Define several method for list modification and query

        """
        self.list = []

    def get_stacked_config_from_assay(
        self, assay_name: str
    ) -> List[StackedConfiguration]:
        """
        Return StackedConfiguration related to an assay or a column.
        """
        result = []
        for config in self.list:
            assay_names = [c.assay_name for c in config.config_list]
            column_names = [c.column.name for c in config.config_list]
            if assay_name in assay_names + column_names:
                result.append(config)

        return result

    def remove_config(self, config: StackedConfiguration) -> None:
        """
        Remove configuration from list
        If plot is associated to configuration, plot is also deleted

        Args:
            config: (StackedConfiguration) configuration
        """

        if config.plot is not None:
            # Update visibility even if plot is deleted later
            # so we can count number of visible plot if main thread as not process event
            config.plot.setVisible(False)
            config.plot.deleteLater()
        self.list.remove(config)

    def replace_config(self, config, new_config) -> None:
        """
        Replace old config by new config inside a StackedConfiguration instance
        """
        for stacked_config in self.list:
            if config in stacked_config.config_list:
                index = stacked_config.config_list.index(config)
                new_config.update_name_for_stacked_config()
                stacked_config.config_list[index] = new_config
                continue
