from typing import Callable, List

import pyqtgraph as pg
import pyqtgraph.parametertree as ptree
import pyqtgraph.parametertree.parameterTypes as pTypes
from qgis.PyQt.QtCore import QObject, Qt, pyqtSignal
from qgis.PyQt.QtGui import QColor, QPen
from qgis.PyQt.QtWidgets import QFileDialog, QWidget

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.config import json_utils
from openlog.gui.assay_visualization.config.config_saver import ConfigSaver
from openlog.gui.assay_visualization.cross_symbology.line_shading import (
    LineCrossSymbologyHandler,
)
from openlog.gui.pyqtgraph.BoldBoolParameter import (
    BoldBoolParameter,
    BoldBoolParameterItem,
)
from openlog.gui.pyqtgraph.ColorRampPreviewParameter import ColorRampPreviewParameter
from openlog.gui.pyqtgraph.SaveLoadActionParameter import SaveLoadActionParameter
from openlog.toolbelt import PlgLogger


class CheckablePenParameter(BoldBoolParameter):
    styles = {
        "SolidLine": Qt.PenStyle.SolidLine,
        "DashLine": Qt.PenStyle.DashLine,
        "DotLine": Qt.PenStyle.DotLine,
        "DashDotLine": Qt.PenStyle.DashDotLine,
        "DashDotDotLine": Qt.PenStyle.DashDotDotLine,
        "CustomDashLine": Qt.PenStyle.CustomDashLine,
    }

    def __init__(self, **opts):
        super().__init__(**opts)
        self.color_param = pTypes.ColorParameter(name="Color", default="black")
        self.width_param = pTypes.SimpleParameter(
            name="Width", type="int", min=1, default=2
        )
        self.style_param = pTypes.ListParameter(
            name="Style",
            default="SolidLine",
            limits=[
                "SolidLine",
                "DashLine",
                "DotLine",
                "DashDotLine",
                "DashDotDotLine",
                "CustomDashLine",
            ],
        )
        self.addChild(self.color_param)
        self.addChild(self.width_param)
        self.addChild(self.style_param)
        self.color_param.sigValueChanged.connect(
            lambda: self.sigValueChanged.emit(self, True)
        )
        self.width_param.sigValueChanged.connect(
            lambda: self.sigValueChanged.emit(self, True)
        )
        self.style_param.sigValueChanged.connect(
            lambda: self.sigValueChanged.emit(self, True)
        )

    def get_pen(self) -> QPen:
        """
        Return defined QPen.
        """
        pen = QPen()
        pen.setCapStyle(Qt.PenCapStyle.SquareCap)
        pen.setCosmetic(True)
        pen.setJoinStyle(Qt.PenJoinStyle.BevelJoin)
        pen.setStyle(self.styles.get(self.style_param.value()))
        pen.setWidth(self.width_param.value())
        pen.setColor(self.color_param.value())
        # if unchecked, no pen
        if not self.value():
            pen.setStyle(Qt.PenStyle.NoPen)
        return pen

    def init_from_pen(self, pen: QPen) -> None:
        self.color_param.setValue(pen.color())
        self.width_param.setValue(pen.width())
        pen_style_str = self._get_pen_style_str(pen)
        if pen_style_str is not None:
            self.style_param.setValue(pen_style_str)

    def _get_pen_style_str(self, pen: QPen) -> str:
        """
        Return str corresponding to Qt.PenStyle.
        """
        style = pen.style()
        for k, v in self.styles.items():
            if v == style:
                return k


class AssayColumnVisualizationConfig:
    class Sender(QObject):
        signal = pyqtSignal(object)
        config_signal = pyqtSignal(object, object)

    # signal emitted when selected from right-click
    plot_selected_signal = Sender()
    # signal for configuration switch
    switch_config = Sender()
    # signal when unit is converted
    unit_converted = Sender()
    # configuration saver
    saverClass = ConfigSaver

    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.log = PlgLogger().log

        self.assay_iface = None
        self.display_plugin_name = None

        self.domain = 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
        self.inspector_pos = None

        # DiagramsMaker classes and instances dict : id(str) : class
        self.diagram_makers_cls = {}
        self.diagram_makers = {}

        # 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

        # save symbology
        self.save_load_group = ptree.Parameter.create(name="Style", type="group")
        self.save_load_group.setOpts(expanded=False)
        self.save_btn = SaveLoadActionParameter(
            icon=":images/themes/default/mActionFileSaveAs.svg", name="Save"
        )
        self.load_btn = SaveLoadActionParameter(
            icon=":images/themes/default/mActionFileOpen.svg", name="Load"
        )
        self.save_load_group.addChild(self.save_btn)
        self.save_load_group.addChild(self.load_btn)

        self.save_btn.sig_file_action.connect(self._save_to_file)
        self.load_btn.sig_file_action.connect(self._load_from_file)
        self.save_btn.sig_db_action.connect(self._save_to_db)
        self.load_btn.sig_db_action.connect(self._load_from_db)

        # 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 = []

        # Default not working with pyqtgraph for pen attributes
        self.pen_params = CheckablePenParameter(name="Line", default=True)
        self.pen_params.setOpts(expanded=False)
        name = (
            f"{column.display_name} ({column.unit})"
            if column.unit
            else column.display_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="Unit", type="group")
        self.unit_group.setOpts(expanded=False)
        self.unit_parameter = pTypes.SimpleParameter(
            name="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="Plot options", type="group"
        )
        self.transformation_group.setOpts(expanded=False)

        # Plot title options
        self.title_collar_param = ptree.Parameter.create(
            name="Collar name",
            type="bool",
            value=True,
            default=True,
        )
        self.title_assay_param = ptree.Parameter.create(
            name="Series name",
            type="bool",
            value=True,
            default=True,
        )
        self.title_column_param = ptree.Parameter.create(
            name="Column name",
            type="bool",
            value=True,
            default=True,
        )
        self.transformation_group.addChild(self.title_collar_param)
        self.transformation_group.addChild(self.title_assay_param)
        self.transformation_group.addChild(self.title_column_param)

        self.title_collar_param.sigValueChanged.connect(self.update_title)
        self.title_assay_param.sigValueChanged.connect(self.update_title)
        self.title_column_param.sigValueChanged.connect(self.update_title)

        # Minimap option
        self.minimap_param = ptree.Parameter.create(
            name="Minimap",
            type="bool",
            value=False,
            default=False,
        )
        self.minimap_param.sigValueChanged.connect(self._display_minimap)
        self.transformation_group.addChild(self.minimap_param)

    def connect_slot_to_visibility_param(self, slot: Callable) -> None:
        """
        Connect visibility_param.sigValueChange to slot.
        """
        self.visibility_param.sigValueChanged.connect(slot)

    def disconnect_slot_to_visibility_param(self, slot: Callable) -> None:
        """
        Disconnect visibility_param.sigValueChange to slot.
        """
        try:
            self.visibility_param.sigValueChanged.disconnect(slot)
        except Exception:
            pass

    def after_plot_item_creation(self) -> None:
        """
        Method executed after plot item have been created.
        """
        return

    def set_hole_id(self, hole_id: str) -> None:
        """
        Set hole_id.
        """
        self.hole_id = hole_id

    def update_title(self):
        """
        Update plot title.
        """
        if self.plot_widget:
            self.plot_widget._update_title()

    def enable_diagrams(self):
        """
        Display existing diagrams in PlotWidgetContainer.
        """
        for diagram_maker in self.diagram_makers.values():
            diagram_maker.attach_diagrams()

    def set_assay_iface(self, assay_iface):
        """
        Set AssayInterface and enable/disable Database I/O for symbology.
        """
        self.assay_iface = assay_iface
        if not self.assay_iface.can_save_symbology_in_db():
            self.save_btn.itemClass.db_action.setEnabled(False)
            self.load_btn.itemClass.db_action.setEnabled(False)

    def _load_symbology(self):

        try:
            self.saverClass().load_from_string(self, self.column.symbology)
        except Exception:
            pass

    def _save_to_db(self):
        """
        Save current symbology parameters in database.
        """
        self.saverClass().save_to_db(self)

    def _load_from_db(self):
        """
        Load symbology parameters from database connection.
        """
        self.saverClass().load_from_db(self)

    def _save_to_file(self):
        """
        Save current symbology parameters in a JSON file.
        """
        filename_suggestion = "visualization_configuration.json"
        if self.column:
            filename_suggestion = (
                f"visualization_{self.assay_name}_{self.column.name}.json"
            )
        filename, _ = QFileDialog.getSaveFileName(
            None,
            "Select file",
            filename_suggestion,
            "JSON file(*.json)",
        )
        if filename:
            self.saverClass().save_to_file(self, filename)

    def _load_from_file(self):
        """
        Load symbology parameters from JSON file.
        """
        filename, _ = QFileDialog.getOpenFileName(
            None,
            "Select file",
            "",
            "JSON file(*.json)",
        )
        if filename:
            self.saverClass().load_from_file(self, filename)

    def to_dict(self) -> dict:
        """
        Called for saving viewer state.
        """
        # symbology
        d = self.saverClass()._serialize(self)
        d["visibility"] = self.visibility_param.value()
        if self.plot_widget:
            d["inspector_pos"] = self.plot_widget.inspector.value()
        else:
            d["inspector_pos"] = None

        return d

    def from_json(self, data: dict) -> None:
        """
        Called for loading viewer state.
        """
        self.saverClass()._deserialize(self, data)
        self.visibility_param.setValue(data.get("visibility"))
        if data.get("inspector_pos"):
            self.inspector_pos = data.get("inspector_pos")

    def disable_diagrams(self):
        """
        Remove all diagrams from displaying.
        Diagrams are still stored.
        """
        for diagram_maker in self.diagram_makers.values():
            diagram_maker.dettach_diagrams()

    def display_diagrams(self):
        """
        Slot for creating and displaying 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 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.get_pen())
            self.plot_item.setVisible(self.visibility_param.value())

    def set_plot_widget(self, widget: pg.PlotWidget) -> None:
        """
        Set plot widget.
        """
        self.plot_widget = widget

        # instantiate diagram makers
        for maker_id, cls in self.diagram_makers_cls.items():
            self.diagram_makers[maker_id] = cls(self.plot_widget)

    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 _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:
                # set range manually because sometimes it is not conserved
                range_ = self.plot_widget.viewRange()
                self.plot_widget.setVisible(self.visibility_param.value())
                self.plot_widget.setRange(xRange=range_[0], yRange=range_[1])

    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
        self.unit_converted.signal.emit(self)

    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.get_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)
        params.addChild(self.save_load_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 get_copiable_config(self, other_config):
        """
        If other_config is switched version of self.
        Useful only for numerical assays.
        """
        return self

    def get_default_config(self):
        """
        Return default config according to assay extent.
        """
        return self

    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.init_from_pen(other.pen_params.get_pen())
            self.pen_params.setValue(other.pen_params.value())
        # 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())
        # Title parameters
        self.title_collar_param.setValue(other.title_collar_param.value())
        self.title_assay_param.setValue(other.title_assay_param.value())
        self.title_column_param.setValue(other.title_column_param.value())


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

    shadingIcon = str(
        DIR_PLUGIN_ROOT / "resources" / "images" / "icon_manage_symbology.svg"
    )

    def __init__(self, column: AssayColumn):
        super().__init__(column)
        # store min/max values
        self.min_value = None
        self.max_value = None
        self.color_ramp_handler = LineCrossSymbologyHandler(self, self.shadingIcon)
        self.color_ramp_group = self.color_ramp_handler.get_pyqtgraph_param()
        self.color_ramp_group.sigValueChanged.connect(self._update_plot_item_color_ramp)

    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_handler.copy_lut_from_config(other.color_ramp_handler)
        self.color_ramp_group.setValue(other.color_ramp_group.value())
        self.color_ramp_handler.legend_param.setValue(
            other.color_ramp_handler.legend_param.value()
        )
        self.color_ramp_handler.binning_param.setValue(
            other.color_ramp_handler.binning_param.value()
        )
        # trigger signal
        self.color_ramp_group.sigValueChanged.emit(
            self.color_ramp_group, self.color_ramp_group.value()
        )
        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 get_min_max_values(self) -> None:
        """
        To be implemented in subclass
        """
        raise NotImplementedError
