import json
from typing import Any

import pyqtgraph as pg
import pyqtgraph.parametertree as ptree
import pyqtgraph.parametertree.parameterTypes as pTypes
from qgis.PyQt.QtWidgets import QFileDialog

from openlog.__about__ import DIR_PLUGIN_ROOT
from openlog.datamodel.assay.generic_assay import AssayColumn
from openlog.gui.assay_visualization.categorical_symbology import CategoricalSymbology
from openlog.gui.assay_visualization.categorical_symbology_dialog import (
    CategoricalSymbologyDialog,
)
from openlog.gui.assay_visualization.config import json_utils
from openlog.gui.assay_visualization.config.assay_column_visualization_config import (
    AssayColumnVisualizationConfig,
)
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.pyqtgraph.CustomActionParameter import CustomActionParameter
from openlog.gui.pyqtgraph.SaveLoadActionParameter import SaveLoadActionParameter
from openlog.plugins.visualization.categorical.saver import (
    DiscreteCategoricalSaver,
    ExtendedCategoricalSaver,
)


class DiscreteCategoricalAssayColumnVisualizationConfig(AssayColumnVisualizationConfig):
    """
    Store visualization configuration for a discrete catgerical assay column.
        Can also access plot item if configuration created from visualization widget

        Configuration supported :

        Args:
            column: (str) assay column name
    """

    saverClass = DiscreteCategoricalSaver

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

        self.is_splittable = True
        self.is_discrete = True
        self.is_minmax_registered = False
        self.is_switchable_config = False
        self.is_categorical = True

        self.as_extended = False
        self.pen_params = None
        self.symbol_group = ptree.Parameter.create(name="Point", type="group")
        self.symbol_group.setOpts(expanded=False)
        self.symbol_parameter = pTypes.ListParameter(
            name="Point",
            type="str",
            value="",
            default="o",
            limits=["o", "s", "x", "d"],
        )
        self.symbol_group.addChild(self.symbol_parameter)
        self.symbol_size_parameter = pTypes.SimpleParameter(
            name="Size", type="int", value=5, default=5, min=1, max=20
        )
        self.symbol_group.addChild(self.symbol_size_parameter)
        self.symbol_color_parameter = pTypes.ColorParameter(
            name="Color", value="blue", default="blue"
        )
        self.symbol_group.addChild(self.symbol_color_parameter)
        self.symbol_parameter.sigValueChanged.connect(self.update_symbol)
        self.symbol_size_parameter.sigValueChanged.connect(self.update_symbol)
        self.symbol_color_parameter.sigValueChanged.connect(self.update_symbol)

        self.point_symbology = CategoricalSymbology()
        self.point_symbology.color_col = column.name

        self.define_symbology_button = CustomActionParameter(
            name="",
            type="action",
            width=60,
            icon=str(
                DIR_PLUGIN_ROOT / "resources" / "images" / "icon_manage_symbology.svg"
            ),
            tooltip="Categorical symbology",
        )
        self.define_symbology_button.sigActivated.connect(
            self._display_symbology_dialog
        )

        # merge button
        self.merge_btn = SaveLoadActionParameter(
            icon=":images/themes/default/mActionFileOpen.svg", name="Merge"
        )
        self.save_load_group.addChild(self.merge_btn)
        self.merge_btn.sig_db_action.connect(self._merge_from_db)
        self.merge_btn.sig_file_action.connect(self._merge_from_file)

    def set_assay_iface(self, assay_iface):

        super().set_assay_iface(assay_iface)
        if not self.assay_iface.can_save_symbology_in_db():
            self.merge_btn.itemClass.db_action.setEnabled(False)

    def _merge_from_db(self):

        d = self.saverClass().get_database_symbology(self)
        cat_dict = d["point_symbology"]
        dialog = CategoricalSymbologyDialog()
        dialog.set_assay(self.assay)
        dialog.set_symbology(self.point_symbology)
        dialog.merge_symbology(cat_dict)
        self.point_symbology = dialog.get_symbology()
        if self.plot_item:
            self.plot_item.set_symbology(self.point_symbology)

    def _merge_from_file(self):

        filename, _ = QFileDialog.getOpenFileName(
            None,
            "Select file",
            "",
            "JSON file(*.json)",
        )
        if filename:
            with open(filename, "r") as f:
                d = json.loads(f.read())
                cat_dict = d["point_symbology"]
                dialog = CategoricalSymbologyDialog()
                dialog.set_assay(self.assay)
                dialog.set_symbology(self.point_symbology)
                dialog.merge_symbology(cat_dict)
                self.point_symbology = dialog.get_symbology()
                if self.plot_item:
                    self.plot_item.set_symbology(self.point_symbology)

    def update_symbol(self):
        if self.plot_item and self.symbol_group:
            self.plot_item.setSymbol(self.symbol_parameter.value())
            self.plot_item.setSize(self.symbol_size_parameter.value())
            self.plot_item.setPen(self.symbol_color_parameter.value())

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

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

    def add_children_to_root_param(self, params: ptree.Parameter):

        params.addChild(self.symbol_group)
        params.addChild(self.define_symbology_button)
        super().add_children_to_root_param(params)

    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.point_symbology.merge(other.point_symbology, replace=True)
        self.symbol_parameter.setValue(other.symbol_parameter.value())
        self.symbol_size_parameter.setValue(other.symbol_size_parameter.value())
        self.symbol_color_parameter.setValue(other.symbol_color_parameter.value())

        if self.plot_item:
            self.plot_item.set_symbology(self.point_symbology)
            self.update_symbol()

    def _display_symbology_dialog(self):
        dialog = CategoricalSymbologyDialog()
        dialog.set_assay(self.assay)
        dialog.set_symbology(self.point_symbology)
        if dialog.exec():
            self.point_symbology = dialog.get_symbology()
            if self.plot_item:
                self.plot_item.set_symbology(self.point_symbology)


class ExtendedCategoricalAssayColumnVisualizationConfig(AssayColumnVisualizationConfig):
    saverClass = ExtendedCategoricalSaver

    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.is_splittable = True
        self.is_discrete = False
        self.is_minmax_registered = False
        self.is_switchable_config = False
        self.is_categorical = True

        self.text_bar_graph_item = None
        self.pen_params.setName("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="Display category", type="bool", value=True, default=True
        )
        self.text_color_param = pTypes.ColorParameter(
            name="Text color", value="black", default="black"
        )
        self.define_symbology_button = CustomActionParameter(
            name="",
            type="action",
            width=60,
            icon=str(
                DIR_PLUGIN_ROOT / "resources" / "images" / "icon_manage_symbology.svg"
            ),
            tooltip="Categorical symbology",
        )
        self.define_symbology_button.sigActivated.connect(
            self._display_symbology_dialog
        )

        # merge button
        self.merge_btn = SaveLoadActionParameter(
            icon=":images/themes/default/mActionFileOpen.svg", name="Merge"
        )
        self.save_load_group.addChild(self.merge_btn)
        self.merge_btn.sig_db_action.connect(self._merge_from_db)
        self.merge_btn.sig_file_action.connect(self._merge_from_file)

    def set_assay_iface(self, assay_iface):

        super().set_assay_iface(assay_iface)
        if not self.assay_iface.can_save_symbology_in_db():
            self.merge_btn.itemClass.db_action.setEnabled(False)

    def _merge_from_db(self):

        d = self.saverClass().get_database_symbology(self)
        cat_dict = d["bar_symbology"]
        dialog = BarSymbologyDialog()
        dialog.set_assay(self.assay)
        dialog.set_symbology(self.bar_symbology)
        dialog.merge_symbology(cat_dict)
        self.bar_symbology = dialog.get_symbology()
        if self.plot_item:
            self.plot_item.set_symbology(self.bar_symbology)
            self.plot_item.drawPicture()

    def _merge_from_file(self):

        filename, _ = QFileDialog.getOpenFileName(
            None,
            "Select file",
            "",
            "JSON file(*.json)",
        )
        if filename:
            with open(filename, "r") as f:
                d = json.loads(f.read())
                cat_dict = d["bar_symbology"]
                dialog = BarSymbologyDialog()
                dialog.set_assay(self.assay)
                dialog.set_symbology(self.bar_symbology)
                dialog.merge_symbology(cat_dict)
                self.bar_symbology = dialog.get_symbology()
                if self.plot_item:
                    self.plot_item.set_symbology(self.bar_symbology)
                    self.plot_item.drawPicture()

    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 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.get_pen())
            self.text_bar_graph_item.setTextColor(self.text_color_param.value())

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

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

            self.text_visibility.sigValueChanged.connect(
                self._on_text_visibility_change
            )

    def _on_text_visibility_change(self):
        """
        Update visibility of categories (text).
        """
        visible = self.text_visibility.value()
        self.text_bar_graph_item.setVisible(visible)
        if self.plot_widget:
            xmin = 0
            xmax = 15 if visible else 10
            self.plot_widget.setXRange(xmin, xmax)

    def add_children_to_root_param(self, params: ptree.Parameter):

        params.addChild(self.text_visibility)
        params.addChild(self.text_color_param)
        params.addChild(self.define_symbology_button)
        super().add_children_to_root_param(params)

    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.init_from_pen(other.pen_params.get_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()
            if self.plot_widget:
                self.plot_widget.plotItem.minimap.addItem(self.plot_item)
