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,
)
from openlog.toolbelt.translator import PlgTranslator


@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.tr = PlgTranslator().tr
        self.is_visible: bool = True
        self.plot: pg.PlotWidget = None
        self.name: str = ""
        self.config_list: List[AssayColumnVisualizationConfig] = []
        self.unit_parameter = pTypes.SimpleParameter(
            name=self.tr("Conversion"),
            type="str",
            value="",
            default="",
        )
        self.unit_parameter.sigValueChanged.connect(self._update_conversion_unit)
        self._conversion_unit = ""

        self.global_param = None

    def display_stacked_minimap(self):
        """
        Slot to call AssayPlotItem.display_minimap() method.
        """

        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 ["Log", "X grid", "Y grid"]:
                self.global_param.child(name).sigValueChanged.connect(
                    self.synchronize_discrete_param
                )

            for name in ["Log", "X grid", "Y grid"]:
                self.global_param.child(name).setValue(False)
                self.global_param.child(name).show()
        else:

            for name in ["Log", "X grid", "Y grid"]:
                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", "Log", "X grid", "Y grid"]
        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)

    def synchronize_discrete_param(self):
        """
        Slot for synchronization between global param and each config's parameters (log, grids)
        """
        for config in self.config_list:
            if config.is_switchable_config and config.is_discrete:
                config.log_param.setValue(self.global_param.child("Log").value())
                config.grid_x_param.setValue(self.global_param.child("X grid").value())
                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:
                    config.discrete_configuration.log_param.setValue(
                        self.global_param.child("Log").value()
                    )
                    config.discrete_configuration.grid_x_param.setValue(
                        self.global_param.child("X grid").value()
                    )
                    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 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

        """
        return is_config_list_valid_for_stack(self.config_list)

    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)

        display_unit = False

        # Define conversion unit
        default_unit = config_list_default_unit(self.config_list)
        display_unit = default_unit and pint_utilities.is_pint_unit(default_unit)
        if display_unit:
            self._conversion_unit = default_unit
            self.unit_parameter.setDefault(default_unit)
            if not self.unit_parameter.value():
                self.unit_parameter.setValue(default_unit)

        for col in self.config_list:
            col.enable_unit_param_display(not display_unit)

    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 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)
