from typing import Any

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

from openlog.__about__ import DIR_PLUGIN_ROOT
from openlog.core import pint_utilities
from openlog.datamodel.assay.generic_assay import (
    AssayColumn,
    AssayDomainType,
    GenericAssay,
)
from openlog.gui.assay_visualization.assay_bar_graph_item import AssayBarGraphItem
from openlog.gui.assay_visualization.config import json_utils
from openlog.gui.assay_visualization.config.assay_column_visualization_config import (
    AssayColumnVisualizationConfig,
    NumericalAssayColumnVisualizationConfig,
)
from openlog.gui.assay_visualization.extended.bar_symbology import BarSymbology
from openlog.gui.assay_visualization.extended.bar_symbology_dialog import (
    BarSymbologyDialog,
)
from openlog.gui.assay_visualization.extended.text_bar_graph_items import (
    TextBarGraphItems,
)
from openlog.gui.assay_visualization.plot_item_factory import AssayPlotItemFactory
from openlog.gui.pyqtgraph.CustomActionParameter import CustomActionParameter


class ExtendedAssayColumnVisualizationConfig(AssayColumnVisualizationConfig):
    def __init__(self, column: str):
        """
        Store visualization configuration for an extended assay column.
        Can also access plot item if configuration created from visualization widget

        Configuration supported :
        - visibility_param (bool) : assay column visibility (default : True)
        - pen_params (QPen) : assay column pen (default : black)
        - bar_color_param (QColor): assay column bar color (default : white)

        Args:
            column: (str) assay column name
        """
        super().__init__(column)
        self.pen_params.setName(self.tr("Bar pen"))
        self.bar_color_param = pTypes.ColorParameter(
            name=self.tr("Bar fill"), value="white", default="white"
        )

    def from_json(self, data: dict) -> None:
        """
        Define ExtendedAssayColumnVisualizationConfig from json data (see to_dict)

        Args:
            data: json data

        """
        super().from_json(data)

        # Bar color
        self.bar_color_param.setValue(json_utils.color_from_json(data["bar_color"]))

    def to_dict(self) -> dict:
        """
        Convert configuration to dict (use for json export)

        Returns:
            dict: configuration as dict

        """
        result = super().to_dict()

        # Bar color
        result["bar_color"] = json_utils.color_to_dict(self.bar_color_param.value())

        return result

    def set_plot_item(self, plot_item: pg.PlotDataItem) -> None:
        """
        Define plot item containing current assay data

        Args:
            plot_item: pg.PlotDataItem
        """
        super().set_plot_item(plot_item)
        if self.plot_item:
            # Define current parameters
            self.plot_item.setBrush(self.bar_color_param.value())

            # Connection to parameter changes
            self.bar_color_param.sigValueChanged.connect(
                lambda params, changes: self.plot_item.setBrush(changes)
            )

    def add_children_to_root_param(self, params: ptree.Parameter):
        super().add_children_to_root_param(params)
        params.addChild(self.bar_color_param)

    def copy_from_config(self, other) -> None:
        """
        Copy configuration from another configuration.
        If a plot item is associated it will be updated

        Args:
            other: configuration to be copy
        """
        super().copy_from_config(other)
        self.pen_params.setValue(other.pen_params.pen)
        self.bar_color_param.setValue(other.bar_color_param.value())


class ExtendedNumericalAssayColumnVisualizationConfig(
    ExtendedAssayColumnVisualizationConfig, NumericalAssayColumnVisualizationConfig
):
    def __init__(self, column: str, discrete_configuration: Any = None):
        """
        Store visualization configuration for an extended numerical assay column.
        Can also access plot item if configuration created from visualization widget

        Configuration supported :
        - visibility_param (bool) : assay_name column visibility (default : True)
        - pen_params (QPen) : assay_name column pen (default : black)
        - bar_color_param (QColor): assay_name column bar color (default : white)

        Args:
            column: (str) assay_name column name
        """
        super().__init__(column)
        self.as_discrete = False
        self.discrete_configuration = discrete_configuration
        self.btn = CustomActionParameter(
            name=self.tr(""),
            type="action",
            width=60,
            icon=str(DIR_PLUGIN_ROOT / "resources" / "images" / "icon_line_chart.svg"),
            tooltip="Switch to line chart",
        )
        self.btn.sigActivated.connect(self._switch_to_discrete)

        # Connect to discrete config unit param change to update current unit
        if self.discrete_configuration:
            self.discrete_configuration.unit_parameter.sigValueChanged.connect(
                self._discrete_unit_updated
            )

            # synchronization if visibility change in extended_config attribute
            self.discrete_configuration.visibility_param.sigValueChanged.connect(
                self._synchronize_visibility_param
            )

    def _update_plot_item_color_ramp(self) -> None:
        """
        Update plot item color ramp from current parameter

        """
        if isinstance(self.plot_item, AssayBarGraphItem):
            cm = self.color_ramp_parameter.value()

            if cm and self.pen_color_ramp_use_param.value():
                values = (
                    self.plot_item.opts["width"]
                    if self.plot_item.opts["x1"] is None
                    else self.plot_item.opts["height"]
                )
                min_ = self.min_color_ramp_param.value()
                max_ = self.max_color_ramp_param.value()
                # min/max scaling to have 0-1 range
                mapper = cm.map((values - min_) / (max_ - min_))

                self.plot_item.setOpts(brushes=mapper)
            else:
                pen = self.pen_params.pen
                color = self.bar_color_param.value()
                # remove brushes to have brush working
                self.plot_item.setOpts(brushes=None)
                self.plot_item.setOpts(brush=color)
                self.plot_item.setOpts(pen=pen)

    def _update_color_ramp_from_name(self) -> None:
        """
        Update displayed color ramp from chosen name

        """
        cm = pg.colormap.get(self.color_ramp_name_param.value())
        self.color_ramp_parameter.setValue(cm)

    def set_plot_item(self, plot_item: pg.PlotDataItem) -> None:
        """
        Define plot item containing current assay data

        Args:
            plot_item: pg.PlotDataItem
        """
        super().set_plot_item(plot_item)
        if self.plot_item:
            # Define current parameters
            self.plot_item.setBrush(self.bar_color_param.value())

            # Connection to parameter changes
            self.bar_color_param.sigValueChanged.connect(
                lambda params, changes: self.plot_item.setBrush(changes)
            )

            self.define_color_ramp_min_max_from_plot_item()
            self._update_plot_item_color_ramp()

    def define_color_ramp_min_max_from_plot_item(self, force: bool = False) -> None:
        """
        Define color ramp min/max from plot item data

        """
        if not isinstance(self.plot_item, AssayBarGraphItem):
            return

        if self.plot_item.opts is not None:
            # depth
            if (
                self.plot_item.opts["x1"] is None
                and len(self.plot_item.opts["width"]) > 0
            ):
                min_ = min(self.plot_item.opts["width"])
                max_ = max(self.plot_item.opts["width"])
            # time
            elif (
                self.plot_item.opts["y1"] is None
                and len(self.plot_item.opts["height"]) > 0
            ):
                min_ = min(self.plot_item.opts["height"])
                max_ = max(self.plot_item.opts["height"])
            else:
                min_ = max_ = None

            self.min_color_ramp_param.setDefault(min_)
            self.max_color_ramp_param.setDefault(max_)

            # Update current min/max only if not defined
            if (not self.min_color_ramp_param.value() or force) and min_:
                self.min_color_ramp_param.setValue(
                    min_, self._update_plot_item_color_ramp
                )

            if (not self.max_color_ramp_param.value() or force) and max_:
                self.max_color_ramp_param.setValue(
                    max_, self._update_plot_item_color_ramp
                )

    def _synchronize_visibility_param(self):
        self.visibility_param.setValue(
            self.discrete_configuration.visibility_param.value()
        )

    def from_json(self, data: dict) -> None:
        """
        Define ExtendedNumericalAssayColumnVisualizationConfig from json data (see to_dict)

        Args:
            data: json data

        """
        super().from_json(data)

        if "discrete_config" in data:
            self.discrete_configuration.from_json(data["discrete_config"])
        if "as_discrete" in data:
            self.as_discrete = data["as_discrete"]

    def to_dict(self) -> dict:
        """
        Convert configuration to dict (use for json export)

        Returns:
            dict: configuration as dict

        """
        result = super().to_dict()

        if self.discrete_configuration:
            result["discrete_config"] = self.discrete_configuration.to_dict()
            result["as_discrete"] = self.as_discrete

        return result

    def enable_unit_param_display(self, enable: bool) -> None:
        super().enable_unit_param_display(enable)
        if self.discrete_configuration:
            self.discrete_configuration.enable_unit_param_display(enable)

    def set_assay(self, assay: GenericAssay) -> None:
        super().set_assay(assay)
        if self.discrete_configuration:
            self.discrete_configuration.set_assay(assay)

    def add_children_to_root_param(self, params: ptree.Parameter):
        if self.discrete_configuration:
            if self.as_discrete:
                self.btn.setOpts(
                    icon=str(
                        DIR_PLUGIN_ROOT / "resources" / "images" / "histogram.svg"
                    ),
                    tooltip="Switch to bar chart",
                )
                params.addChild(self.btn)
                self.discrete_configuration.add_children_to_root_param(params)
            else:
                self.btn.setOpts(
                    icon=str(DIR_PLUGIN_ROOT / "resources" / "images" / "curve.svg"),
                    tooltip="Switch to line chart",
                )
                params.addChild(self.btn)
                super().add_children_to_root_param(params)
                params.addChild(self.color_ramp_group)
        else:
            super().add_children_to_root_param(params)
            params.addChild(self.color_ramp_group)

    def copy_from_config(self, other) -> None:
        """
        Copy configuration from another configuration.
        If a plot item is associated it will be updated

        Args:
            other: configuration to be copy
        """
        super().copy_from_config(other)
        self._copy_color_ramp_from_config(other)

        if self.discrete_configuration:
            self.discrete_configuration.copy_from_config(other.discrete_configuration)
            if self.as_discrete != other.as_discrete:
                self._switch_to_discrete()

    def propagate_hole_and_assay_properties(self) -> None:
        self.discrete_configuration.hole_id = self.hole_id
        self.discrete_configuration.hole_display_name = self.hole_display_name
        self.discrete_configuration.assay_name = self.assay_name
        self.discrete_configuration.assay_display_name = self.assay_display_name

    def _copy_color_ramp_params_to_child_config(self):
        self.discrete_configuration.color_ramp_name_param.setValue(
            self.color_ramp_name_param.value()
        )
        self.discrete_configuration.color_ramp_parameter.setValue(
            self.color_ramp_parameter.value(),
            self.discrete_configuration._update_plot_item_color_ramp,
        )
        self.discrete_configuration.min_color_ramp_param.setValue(
            self.min_color_ramp_param.value(),
            self.discrete_configuration._update_plot_item_color_ramp,
        )
        self.discrete_configuration.max_color_ramp_param.setValue(
            self.max_color_ramp_param.value(),
            self.discrete_configuration._update_plot_item_color_ramp,
        )
        self.discrete_configuration.pen_color_ramp_use_param.setValue(
            self.pen_color_ramp_use_param.value(),
            self.discrete_configuration._update_plot_item_color_ramp,
        )

    def _switch_to_discrete(self) -> None:
        plot_widget = self.get_current_plot_widget()
        self.as_discrete = not self.as_discrete
        self._copy_color_ramp_params_to_child_config()

        if self.assay and plot_widget:

            if self.plot_item:
                plot_widget.removeItem(self.plot_item)
            if not self.as_discrete:
                plot_widget.removeItem(self.discrete_configuration.plot_item)
                self.plot_item = (
                    AssayPlotItemFactory.create_extended_numerical_plot_items(
                        plot_widget, self.assay, self.column.name, self
                    )
                )
            else:
                self.propagate_hole_and_assay_properties()
                self.plot_item = AssayPlotItemFactory.create_discrete_plot_item_from_extended_numerical(
                    plot_widget,
                    self.assay,
                    self.column.name,
                    self.discrete_configuration,
                )
                self.discrete_configuration._set_log_mode()

            self.plot_item.setVisible(self.visibility_param.value())

        # Update parameters
        self.visibility_param.clearChildren()
        self.add_children_to_root_param(self.visibility_param)

    def _discrete_unit_updated(self) -> None:
        self.unit_parameter.setValue(self.discrete_configuration.unit_parameter.value())

    def _update_plot_item_unit(self, from_unit: str, to_unit: str) -> None:
        # Update discrete configuration
        if self.discrete_configuration:
            self.discrete_configuration.unit_parameter.setValue(to_unit)

        # Update plot widget unit
        if self.plot_widget:
            self.plot_widget.update_displayed_unit(to_unit)

        if not self.plot_item:
            return

        if self.as_discrete:
            return

        self.log(
            f"Conversion for assay {self.assay_name} column {self.column.name} and collar {self.hole_display_name} from {from_unit} to {to_unit}"
        )

        if self.assay.assay_definition.domain == AssayDomainType.DEPTH:
            y_val = pint_utilities.unit_conversion(
                from_unit, to_unit, self.plot_item.opts.get("width")
            )
            self.plot_item.setOpts(width=y_val)
        else:
            y_val = pint_utilities.unit_conversion(
                from_unit, to_unit, self.plot_item.opts.get("height")
            )
            self.plot_item.setOpts(height=y_val)


class ExtendedNominalAssayColumnVisualizationConfig(
    ExtendedAssayColumnVisualizationConfig
):
    def __init__(self, column: str):
        """
        Store visualization configuration for an extended nominal assay column.
        Can also access plot item if configuration created from visualization widget

        Configuration supported :
        - visibility_param (bool) : assay column visibility (default : True)
        - pen_params (QPen) : assay column pen (default : black)
        - bar_color_param (QColor): assay column bar color (default : white)
        - text_color_param (QColor): assay column text color (default : black)

        Args:
            column: (str) assay column name
        """
        super().__init__(column)
        self.text_color_param = pTypes.ColorParameter(
            name=self.tr("Text color"), value="black", default="black"
        )

    def from_json(self, data: dict) -> None:
        """
        Define ExtendedNominalAssayColumnVisualizationConfig from json data (see to_dict)

        Args:
            data: json data

        """
        super().from_json(data)

        # Text
        self.text_color_param.setValue(json_utils.color_from_json(data["text_color"]))

    def to_dict(self) -> dict:
        """
        Convert configuration to dict (use for json export)

        Returns:
            dict: configuration as dict

        """
        result = super().to_dict()

        # Text
        result["text_color"] = json_utils.color_to_dict(self.text_color_param.value())

        return result

    def set_plot_item(self, plot_item: pg.PlotDataItem) -> None:
        """
        Define plot item containing current assay data

        Args:
            plot_item: pg.PlotDataItem
        """
        super().set_plot_item(plot_item)
        if self.plot_item:
            # Define current parameters
            self.plot_item.setTextColor(self.text_color_param.value())

            # Connection to parameter changes
            self.text_color_param.sigValueChanged.connect(
                lambda params, changes: self.plot_item.setTextColor(changes)
            )

    def add_children_to_root_param(self, params: ptree.Parameter):
        super().add_children_to_root_param(params)
        params.addChild(self.text_color_param)

    def copy_from_config(self, other) -> None:
        """
        Copy configuration from another configuration.
        If a plot item is associated it will be updated

        Args:
            other: configuration to be copy
        """
        super().copy_from_config(other)
        self.text_color_param.setValue(other.text_color_param.value())


class ExtendedCategoricalAssayColumnVisualizationConfig(AssayColumnVisualizationConfig):
    def __init__(self, column: AssayColumn):
        """
        Store visualization configuration for an extended assay categorical column.
        Can also access plot item if configuration created from visualization widget

        Configuration supported :
        - visibility_param (bool) : assay column visibility (default : True)
        - pen_params (QPen) : assay column pen (default : Black)
        - bar_symbology : symbology for each category
        - text_visibility (bool) : assay column category visibility (default : True)
        - text_color_param (QColor): assay column category text color (default : black)

        Args:
            column: (AssayColumn) assay column
        """
        super().__init__(column)
        self.text_bar_graph_item = None
        self.pen_params.setName(self.tr("Bar pen"))
        self.bar_symbology = BarSymbology()
        self.bar_symbology.pattern_col = column.name
        self.bar_symbology.color_col = column.name
        self.bar_symbology.scale_col = column.name
        self.text_visibility = ptree.Parameter.create(
            name=self.tr("Display category"), type="bool", value=True, default=True
        )
        self.text_color_param = pTypes.ColorParameter(
            name=self.tr("Text color"), value="black", default="black"
        )
        self.define_symbology_button = CustomActionParameter(
            name=self.tr(""),
            type="action",
            width=60,
            icon=str(
                DIR_PLUGIN_ROOT / "resources" / "images" / "icon_manage_symbology.svg"
            ),
            tooltip="Set symbology",
        )
        self.define_symbology_button.sigActivated.connect(
            self._display_symbology_dialog
        )

    def from_json(self, data: dict) -> None:
        """
        Define ExtendedCategoricalAssayColumnVisualizationConfig from json data (see to_dict)

        Args:
            data: json data

        """
        super().from_json(data)

        # Bar symbology
        self.bar_symbology = BarSymbology.from_json(data["bar_symbology"])

        # Text
        self.text_visibility.setValue(data["display_category"])
        self.text_color_param.setValue(json_utils.color_from_json(data["text_color"]))

    def to_dict(self) -> dict:
        """
        Convert configuration to dict (use for json export)

        Returns:
            dict: configuration as dict

        """
        result = super().to_dict()

        # Bar symbology
        result["bar_symbology"] = self.bar_symbology.to_dict()

        # Text
        result["display_category"] = self.text_visibility.value()
        result["text_color"] = json_utils.color_to_dict(self.text_color_param.value())

        return result

    def set_plot_item(self, plot_item: pg.PlotDataItem) -> None:
        """
        Define plot item containing current assay data

        Args:
            plot_item: pg.PlotDataItem
        """
        super().set_plot_item(plot_item)
        if self.plot_item:
            self.bar_symbology = plot_item.symbology_config

    def stack_widget(self, plot_stacked: pg.PlotWidget) -> None:
        """
        Add current plot item to a new plot widget and remove them from current plot widget

        Args:
            plot_stacked (pg.PlotWidget): plot widget where item are added
        """
        super().stack_widget(plot_stacked)
        if self.plot_widget and self.text_bar_graph_item:
            self.plot_widget.removeItem(self.text_bar_graph_item)
            plot_stacked.addItem(self.text_bar_graph_item)

    def unstack_widget(self, plot_stacked: pg.PlotWidget) -> None:
        """
        Remove current plot item from a stacked plot widget and add them to current plot widget

        Args:
            plot_stacked (pg.PlotWidget): plot widget where items are available
        """
        super().unstack_widget(plot_stacked)
        if self.plot_widget and self.text_bar_graph_item:
            plot_stacked.removeItem(self.text_bar_graph_item)
            self.plot_widget.addItem(self.text_bar_graph_item)

    def set_category_text_bar_graph_item(
        self, text_bar_graph_item: TextBarGraphItems
    ) -> None:
        """
        Define plot item containing category text for current assay data

        Args:
            text_bar_graph_item: TextBarGraphItems
        """
        self.text_bar_graph_item = text_bar_graph_item
        if self.text_bar_graph_item:
            # Define current parameters
            self.text_bar_graph_item.setPen(self.pen_params.value())
            self.text_bar_graph_item.setTextColor(self.text_color_param.value())

            # Connection to parameter changes
            self.pen_params.sigValueChanged.connect(
                lambda params, changes: self.text_bar_graph_item.setPen(changes)
            )

            self.text_color_param.sigValueChanged.connect(
                lambda params, changes: self.text_bar_graph_item.setTextColor(changes)
            )

            self.text_visibility.sigValueChanged.connect(
                lambda params, changes: self.text_bar_graph_item.setVisible(changes)
            )

    def add_children_to_root_param(self, params: ptree.Parameter):
        super().add_children_to_root_param(params)
        params.addChild(self.text_visibility)
        params.addChild(self.text_color_param)
        params.addChild(self.define_symbology_button)

    def copy_from_config(self, other) -> None:
        """
        Copy configuration from another configuration.
        If a plot item is associated it will be updated

        Args:
            other: configuration to be copy
        """
        super().copy_from_config(other)
        self.pen_params.setValue(other.pen_params.pen)

        self.bar_symbology.merge(other.bar_symbology, replace=True)

        self.text_visibility.setValue(other.text_visibility.value())
        self.text_color_param.setValue(other.text_color_param.value())

        if self.plot_item:
            self.plot_item.set_symbology(self.bar_symbology)
            self.plot_item.drawPicture()

    def _display_symbology_dialog(self):
        dialog = BarSymbologyDialog()
        dialog.set_assay(self.assay)
        dialog.set_symbology(self.bar_symbology)
        if dialog.exec():
            self.bar_symbology = dialog.get_symbology()
            if self.plot_item:
                self.plot_item.set_symbology(self.bar_symbology)
                self.plot_item.drawPicture()


class ExtendedImageryColumnVisualizationConfig(AssayColumnVisualizationConfig):
    def __init__(self, column: str):
        """
        Store visualization configuration for an extended imagery assay column.
        Can also access plot item if configuration created from visualization widget

        Configuration supported :
        - visibility_param (bool) : assay column visibility (default : True)
        - max_ratio_param (float): maximum ratio
        - min_width_px_param (int): minimum image width in pixels

        Args:
            column: (str) assay column name
        """
        super().__init__(column)

        # Remove pen parameter
        self.pen_params = None

        self.min_width_px_param = pTypes.SimpleParameter(
            name=self.tr("Min. width (px)"),
            type="int",
            value=150,
            default=150,
            min=50,
            max=300,
        )
        self.max_stretch_factor_param = pTypes.SimpleParameter(
            name=self.tr("Max. stretch factor"),
            type="int",
            value=1,
            default=1,
            min=1,
            max=20,
        )

    def from_json(self, data: dict) -> None:
        """
        Define class ExtendedImageryColumnVisualizationConfig from json data (see to_dict)

        Args:
            data: json data

        """
        super().from_json(data)

        self.min_width_px_param.setValue(data["min_width_px"])
        self.max_stretch_factor_param.setValue(data["max_stretch_factor"])

    def to_dict(self) -> dict:
        """
        Convert configuration to dict (use for json export)

        Returns:
            dict: configuration as dict

        """
        result = super().to_dict()

        result["min_width_px"] = self.min_width_px_param.value()
        result["max_stretch_factor"] = self.max_stretch_factor_param.value()

        return result

    def set_plot_item(self, plot_item: pg.PlotDataItem) -> None:
        """
        Define plot item containing current assay data

        Args:
            plot_item: pg.PlotDataItem
        """
        super().set_plot_item(plot_item)
        if self.plot_item:
            # Define current parameters
            self.plot_item.set_min_width(self.min_width_px_param.value())
            self.plot_item.set_max_stretch_factor(self.max_stretch_factor_param.value())

            # Connection to parameter changes
            self.min_width_px_param.sigValueChanged.connect(
                lambda params, changes: self.plot_item.set_min_width(changes)
            )
            self.max_stretch_factor_param.sigValueChanged.connect(
                lambda params, changes: self.plot_item.set_max_stretch_factor(changes)
            )

    def add_children_to_root_param(self, params: ptree.Parameter):
        super().add_children_to_root_param(params)
        params.addChild(self.min_width_px_param)
        params.addChild(self.max_stretch_factor_param)

    def copy_from_config(self, other) -> None:
        """
        Copy configuration from another configuration.
        If a plot item is associated it will be updated

        Args:
            other: configuration to be copy
        """
        super().copy_from_config(other)
        self.min_width_px_param.setValue(other.min_width_px_param.value())
        self.max_stretch_factor_param.setValue(other.max_stretch_factor_param.value())
