from typing import List

import pyqtgraph as pg
import pyqtgraph.parametertree as ptree
import pyqtgraph.parametertree.parameterTypes as pTypes
from qgis.PyQt.QtGui import QColor, QPen
from qgis.PyQt.QtWidgets import QWidget

from openlog.core import pint_utilities
from openlog.datamodel.assay.generic_assay import (
    AssayColumn,
    AssayDomainType,
    GenericAssay,
)
from openlog.gui.assay_visualization.config import json_utils
from openlog.toolbelt import PlgLogger, PlgTranslator


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

        To use a plugin for data visualization, display_plugin_name attribute must be defined

        Configuration supported :
        - visibility_param (bool) : assay column visibility (default : True)
        - pen_params (QPen) : assay column pen (default : Black

        Args:
            column: (AssayColumn) assay column
        """

        # translation
        self.tr = PlgTranslator().tr
        self.log = PlgLogger().log

        self.display_plugin_name = None

        self.column = column
        self.hole_id = ""
        self.hole_display_name = ""
        self.assay_name = ""
        self.assay_display_name = ""
        self.plot_item = None
        self.assay = None
        self.plot_widget = None
        self.plot_stacked = None

        # generic attributes (to be assigned at init for subclasses)
        self.is_splittable = False  # symbology can be projected in cross-section ?
        self.is_discrete = False
        self.is_minmax_registered = False  # have color ramp bounds ?
        self.is_switchable_config = False  # can switch to another config from self ?
        self.is_categorical = False

        # min and max depth
        self.min_depth = None
        self.max_depth = None

        # children configs : list of configs associated with a hole_id (only used for global configs, i.e. without hole_id)
        self.child_configs = []

        pen = QPen(QColor("black"))
        pen.setWidth(2)
        pen.setCosmetic(
            True
        )  # Need to add cosmetic option or graph is not correctly displayed

        # Default not working with pyqtgraph for pen attributes
        self.pen_params = pTypes.PenParameter(name=self.tr("Line"), value=pen)
        self.pen_params.setOpts(expanded=False)
        name = f"{column.name} ({column.unit})" if column.unit else column.name
        self.visibility_param = ptree.Parameter.create(
            name=name, type="bool", value=True, default=True
        )
        # Connection to parameter changes
        self.pen_params.sigValueChanged.connect(self._pen_updated)
        self.visibility_param.sigValueChanged.connect(self._visibility_changed)

        self._display_unit_param = True

        self.unit_group = ptree.Parameter.create(name=self.tr("Unit"), type="group")
        self.unit_group.setOpts(expanded=False)
        self.unit_parameter = pTypes.SimpleParameter(
            name=self.tr("Conversion"),
            type="str",
            value=column.unit,
            default=column.unit,
        )
        self.unit_group.addChild(self.unit_parameter)
        self._conversion_unit = column.unit
        self.unit_parameter.sigValueChanged.connect(self._update_conversion_unit)

        self.transformation_group = ptree.Parameter.create(
            name=self.tr("Plot options"), type="group"
        )
        self.transformation_group.setOpts(expanded=False)
        self.minimap_param = ptree.Parameter.create(
            name=self.tr("Minimap"),
            type="bool",
            value=False,
            default=False,
        )
        self.minimap_param.sigValueChanged.connect(self._display_minimap)
        self.transformation_group.addChild(self.minimap_param)

    def disable_diagrams(self):
        """
        Disable all diagrams.
        """
        return

    def set_children_configs(self, config_list: list):
        """
        List containing children configs (with hole_id).
        """
        self.child_configs = config_list

    def _display_minimap(self):
        if self.plot_widget:
            self.plot_widget.plotItem.display_minimap(self.minimap_param.value())

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

        Args:
            data: json data

        """
        self.hole_id = data["hole_id"]
        self.hole_display_name = data["hole_display_name"]
        self.assay_name = data["assay_name"]
        self.assay_display_name = data["assay_display_name"]
        self.visibility_param.setValue(data["is_visible"])
        if self.pen_params:
            self.pen_params.setValue(json_utils.pen_from_json(data["pen"]))
        self._conversion_unit = data["unit_conversion"]
        self.unit_parameter.setValue(data["unit_conversion"])

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

        Returns:
            dict: configuration as dict

        """
        result = {}
        result["hole_id"] = self.hole_id
        result["hole_display_name"] = self.hole_display_name
        result["assay_name"] = self.assay_name
        result["assay_display_name"] = self.assay_display_name
        result["is_visible"] = self.visibility_param.value()
        if self.pen_params:
            result["pen"] = json_utils.pen_to_dict(self.pen_params.value())
        result["unit_conversion"] = self.unit_parameter.value()

        return result

    def clear_item(self):
        if self.plot_item:
            del self.plot_item
            self.plot_item = None

    def set_minmax_depth(self, assay: GenericAssay):

        if (
            assay
            and assay.assay_definition.domain == AssayDomainType.DEPTH
            and self.hole_id != ""
            and not assay.use_assay_column_plugin_reader
        ):
            x, _ = assay.get_all_values(self.column.name)
            x = x.ravel().astype(float)
            if len(x) > 0:
                self.min_depth = x.min()
                self.max_depth = x.max()

    def set_assay(self, assay: GenericAssay) -> None:
        self.assay = assay
        self.set_minmax_depth(assay)

    def enable_unit_param_display(self, enable: bool) -> None:
        """
        Define if unit param should be display

        Args:
            enable: bool
        """
        self._display_unit_param = enable

    def update_name_for_stacked_config(self) -> None:
        """
        Update name for stacked config display

        """

        name = f"{self.assay_display_name};{self.column.name};{self.hole_display_name}"
        name = f"{name} ({self.column.unit})" if self.column.unit else name
        self.visibility_param.setName(name)

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

        Args:
            plot_item: pg.PlotDataItem
        """
        self.plot_item = plot_item
        if self.plot_item:
            # Define current parameters
            if self.pen_params:
                self.plot_item.setPen(self.pen_params.pen)
            self.plot_item.setVisible(self.visibility_param.value())

    def get_current_plot_widget(self) -> pg.PlotWidget:
        """
        Get current plot widget used to display plot item

        Returns:
            pg.PlotWidget: current plot widget, None is none used

        """
        if self.plot_widget and self.plot_widget.isVisible():
            return self.plot_widget
        if self.plot_stacked and self.plot_stacked.isVisible():
            return self.plot_stacked
        return None

    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
        """
        if self.plot_widget and self.plot_item:
            self.plot_widget.removeItem(self.plot_item)
            plot_stacked.addItem(self.plot_item)
            self.plot_widget.setVisible(False)

    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
        """
        if self.plot_widget and self.plot_item:
            plot_stacked.removeItem(self.plot_item)
            self.plot_widget.addItem(self.plot_item)
            self.plot_widget.setVisible(self.visibility_param.value())

    def _visibility_changed(self) -> None:
        """
        Update plot visibility when parameter is changed

        """
        if self.plot_item:
            self.plot_item.setVisible(self.visibility_param.value())

            if self.plot_widget and not self.plot_stacked.isVisible():
                self.plot_widget.setVisible(self.visibility_param.value())

    def set_conversion_unit(self, unit: str) -> None:
        """
        Define iconversion unit

        Args:
            unit: str
        """

        # _update_conversion_unit slot will be called
        self.unit_parameter.setValue(unit)

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

        # Check user value
        if not pint_utilities.can_convert(new_conversion_unit, self._conversion_unit):
            self.unit_parameter.setValue(self._conversion_unit)
            return

        # Update plot item
        if new_conversion_unit != self._conversion_unit:
            self._update_plot_item_unit(self._conversion_unit, new_conversion_unit)

        self._conversion_unit = new_conversion_unit

    def _update_plot_item_unit(self, from_unit: str, to_unit: str) -> None:
        # Specific implementation for extended or discrete configuration
        pass

    def _pen_updated(self) -> None:
        """
        Update plot pen when parameter is changed

        """
        if self.plot_item and self.pen_params:
            self.plot_item.setPen(self.pen_params.pen)

    def get_pyqtgraph_param(self) -> ptree.Parameter:
        """
        Get pyqtgraph param to display in pyqtgraph ParameterTree

        Returns: ptree.Parameter containing all configuration params

        """
        params = self.visibility_param
        params.clearChildren()
        self.add_children_to_root_param(params)
        return params

    def add_children_to_root_param(self, params: ptree.Parameter):
        if pint_utilities.is_pint_unit(self.column.unit) and self._display_unit_param:
            params.addChild(self.unit_group)

        if self.pen_params:
            params.addChild(self.pen_params)
        if self.transformation_group:
            params.addChild(self.transformation_group)

    def create_configuration_widgets(self, parent: None) -> List[QWidget]:
        """
        Create widgets needed for configuration not available as pyqtgraph param

        Args:
            parent: parent QWidget

        Returns: [QWidget] list of QWidget for specific configuration

        """
        return []

    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
        """
        if self.pen_params and other.pen_params:
            self.pen_params.setValue(other.pen_params.pen)
        self.visibility_param.setValue(other.visibility_param.value())
        self.unit_parameter.setValue(other.unit_parameter.value())
        self._display_unit_param = other._display_unit_param
        self.minimap_param.setValue(other.minimap_param.value())


class NumericalAssayColumnVisualizationConfig(AssayColumnVisualizationConfig):
    """
    Support for color ramp parameter.
    """

    def __init__(self, column: AssayColumn):
        super().__init__(column)

        self.color_ramp_group = ptree.Parameter.create(
            name=self.tr("Color ramp"), type="group"
        )
        self.color_ramp_group.setOpts(expanded=False)
        list_of_maps = pg.colormap.listMaps()
        list_of_maps = sorted(list_of_maps, key=lambda x: x.swapcase())

        self.color_ramp_name_param = pTypes.ListParameter(
            name=self.tr("Name"),
            type="str",
            value="PAL-relaxed_bright",
            default="PAL-relaxed_bright",
            limits=list_of_maps,
        )
        self.color_ramp_group.addChild(self.color_ramp_name_param)

        self.color_ramp_parameter = pTypes.ColorMapParameter(
            name=self.tr("Scale"),
            value=pg.colormap.get(self.color_ramp_name_param.value()),
        )

        self.color_ramp_group.addChild(self.color_ramp_parameter)

        # Use None value for min/max to be able to check if value is defined
        self.min_color_ramp_param = pTypes.SimpleParameter(
            name=self.tr("Min"), type="float", value=None
        )
        self.color_ramp_group.addChild(self.min_color_ramp_param)
        self.max_color_ramp_param = pTypes.SimpleParameter(
            name=self.tr("Max"), type="float", value=None
        )
        self.color_ramp_group.addChild(self.max_color_ramp_param)
        self.pen_color_ramp_use_param = ptree.Parameter.create(
            name=self.tr("Enable"),
            type="bool",
            value=False,
            default=False,
        )
        self.color_ramp_group.addChild(self.pen_color_ramp_use_param)

        self.pen_color_ramp_use_param.sigValueChanged.connect(
            self._update_plot_item_color_ramp
        )
        self.color_ramp_name_param.sigValueChanged.connect(
            self._update_color_ramp_from_name
        )
        self.min_color_ramp_param.sigValueChanged.connect(
            self._update_plot_item_color_ramp
        )
        self.max_color_ramp_param.sigValueChanged.connect(
            self._update_plot_item_color_ramp
        )
        self.color_ramp_parameter.sigValueChanged.connect(
            self._update_plot_item_color_ramp
        )

    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 _update_plot_item_color_ramp(self, plot_item) -> None:
        """
        Update plot item color ramp from current parameter

        """
        raise NotImplementedError

    def _copy_color_ramp_from_config(self, other) -> None:
        """
        Copy color ramp parameters from a config to another.
        Should be called in `.copy_to_config` subclass method
        """
        self.color_ramp_name_param.setValue(other.color_ramp_name_param.value())
        self.color_ramp_parameter.setValue(other.color_ramp_parameter.value())

        # Copy only valid min/max

        if other.min_color_ramp_param.value() is not None:

            self.min_color_ramp_param.setValue(
                other.min_color_ramp_param.value(), self._update_plot_item_color_ramp
            )

        if other.max_color_ramp_param.value() is not None:

            self.max_color_ramp_param.setValue(
                other.max_color_ramp_param.value(), self._update_plot_item_color_ramp
            )

        self.pen_color_ramp_use_param.setValue(other.pen_color_ramp_use_param.value())
        self._update_color_ramp_from_name()
        self._update_plot_item_color_ramp()

    def _copy_color_ramp_params_to_child_config(self):
        """
        To be implemented in subclass
        """
        raise NotImplementedError

    def _update_plot_item_color_ramp(self) -> None:
        """
        To be implemented in subclass
        """
        raise NotImplementedError

    def define_color_ramp_min_max_from_plot_item(self, force: bool = False) -> None:
        """
        To be implemented in subclass
        """
        raise NotImplementedError
