from qgis.core import Qgis
from qgis.core import QgsFeature
from qgis.core import QgsFeatureRequest
from qgis.core import QgsProject
from qgis.core import QgsValueMapFieldFormatter
from qgis.core import QgsVectorLayer
from qgis.core import QgsWkbTypes
from qgis.gui import QgsMapToolIdentify
from qgis.gui import QgsRubberBand
from qgis.PyQt.QtCore import pyqtSignal
from qgis.PyQt.QtCore import pyqtSlot
from qgis.PyQt.QtCore import QEvent
from qgis.PyQt.QtCore import QMetaObject
from qgis.PyQt.QtCore import QSize
from qgis.PyQt.QtCore import Qt
from qgis.PyQt.QtGui import QColor
from qgis.PyQt.QtWidgets import QAbstractItemView
from qgis.PyQt.QtWidgets import QAction
from qgis.PyQt.QtWidgets import QActionGroup
from qgis.PyQt.QtWidgets import QCheckBox
from qgis.PyQt.QtWidgets import QColorDialog
from qgis.PyQt.QtWidgets import QComboBox
from qgis.PyQt.QtWidgets import QDockWidget
from qgis.PyQt.QtWidgets import QHBoxLayout
from qgis.PyQt.QtWidgets import QMenu
from qgis.PyQt.QtWidgets import QMessageBox
from qgis.PyQt.QtWidgets import QSizePolicy
from qgis.PyQt.QtWidgets import QSpacerItem
from qgis.PyQt.QtWidgets import QSplitter
from qgis.PyQt.QtWidgets import QTableView
from qgis.PyQt.QtWidgets import QTabWidget
from qgis.PyQt.QtWidgets import QToolButton
from qgis.PyQt.QtWidgets import QVBoxLayout
from qgis.PyQt.QtWidgets import QWidget
from threedi_results_analysis.datasource.threedi_results import normalized_object_type
from threedi_results_analysis.threedi_plugin_model import ThreeDiGridItem
from threedi_results_analysis.threedi_plugin_model import ThreeDiPluginModel
from threedi_results_analysis.threedi_plugin_model import ThreeDiResultItem
from threedi_results_analysis.tool_graph.graph_model import LocationTimeseriesModel
from threedi_results_analysis.utils.constants import TOOLBOX_MESSAGE_TITLE
from threedi_results_analysis.utils.user_messages import messagebar_message
from threedi_results_analysis.utils.user_messages import statusbar_message
from threedi_results_analysis.utils.utils import generate_parameter_config
from threedi_results_analysis.utils.widgets import PenStyleWidget
from typing import List

import logging
import pyqtgraph as pg


logger = logging.getLogger(__name__)

pg.setConfigOption("background", "w")
pg.setConfigOption("foreground", "k")

# Layer providers that we can use for the graph
VALID_PROVIDERS = ["spatialite", "memory", "ogr"]
# providers which don't have a primary key
PROVIDERS_WITHOUT_PRIMARY_KEY = ["memory", "ogr"]

FLOWLINE_OR_PUMP = 'flowline_or_pump'
NODE_OR_CELL = 'node_or_cell'


def is_threedi_layer(vector_layer: QgsVectorLayer) -> bool:
    """
    Checks whether a layer has been generated by Rana Results Analysis.

    It is an extensive check, trying to be backwards compatible with older tools.
    """
    if not vector_layer:
        return False

    provider = vector_layer.dataProvider()
    valid_object_type = normalized_object_type(vector_layer.name())

    if provider.name() in ["spatialite", "memory", "ogr"] and valid_object_type:
        return True
    elif vector_layer.objectName() in ("flowline", "node", "pump_linestring", "cell", "pump"):
        return True

    return False


class GraphPlot(pg.PlotWidget):
    """Graph element"""

    def __init__(self, parent=None):
        super().__init__(parent)
        self.showGrid(True, True, 0.5)
        self.current_parameter = None
        self.location_model = None
        self.result_model = None
        self.parent = parent
        self.absolute = False
        self.current_time_units = "hrs"
        self.setLabel("bottom", "Time", self.current_time_units)
        # Auto SI prefix scaling doesn't work properly with m3, m2 etc.
        self.getAxis("left").enableAutoSIPrefix(False)

    def on_close(self):
        """
        unloading widget and remove all required stuff
        :return:
        """
        if self.location_model:
            self.location_model.dataChanged.disconnect(self.location_data_changed)
            self.location_model.rowsInserted.disconnect(self.on_insert_locations)
            self.location_model.rowsAboutToBeRemoved.disconnect(
                self.on_remove_locations
            )
            self.location_model = None

    def closeEvent(self, event):
        """
        overwrite of QDockWidget class to emit signal
        :param event: QEvent
        """
        self.on_close()
        event.accept()

    def set_location_model(self, model):
        self.location_model = model
        self.location_model.dataChanged.connect(self.location_data_changed)
        self.location_model.rowsInserted.connect(self.on_insert_locations)
        self.location_model.rowsAboutToBeRemoved.connect(self.on_remove_locations)

    def set_result_model(self, model: ThreeDiPluginModel):
        self.result_model = model

    def set_absolute(self, absolute: bool):
        # Remove and re-add to set correct absoluteness
        for item in self.location_model.rows:
            self.removeItem(
                item.plots(self.current_parameter["parameters"], time_units=self.current_time_units, absolute=self.absolute)
            )

            if item.active.value:
                self.addItem(
                    item.plots(self.current_parameter["parameters"], time_units=self.current_time_units, absolute=absolute)
                )

        self.absolute = absolute
        self.plotItem.vb.menu.viewAll.triggered.emit()

    def on_insert_locations(self, parent, start, end):
        """
        add list of items to graph. based on Qt addRows model trigger
        :param parent: parent of event (Qt parameter)
        :param start: first row nr
        :param end: last row nr
        """
        for i in range(start, end + 1):
            item = self.location_model.rows[i]
            self.addItem(
                item.plots(
                    self.current_parameter["parameters"],
                    absolute=self.absolute,
                    time_units=self.current_time_units,
                )
            )
        self.plotItem.vb.menu.viewAll.triggered.emit()

    def on_remove_locations(self, index, start, end):
        """
        remove items from graph. based on Qt model removeRows
        trigger
        :param index: Qt Index (not used)
        :param start: first row nr
        :param end: last row nr
        """
        for i in range(start, end + 1):
            item = self.location_model.rows[i]
            self.removeItem(
                        item.plots(self.current_parameter["parameters"], time_units=self.current_time_units, absolute=self.absolute)
                    )

        self.plotItem.vb.menu.viewAll.triggered.emit()

    def location_data_changed(self, index):
        """
        change graphs based on changes in locations
        :param index: index of changed field
        """
        item = self.location_model.rows[index.row()]

        if self.location_model.columns[index.column()].name == "active":
            if item.active.value:
                self.show_timeseries(index.row())
            else:
                self.hide_timeseries(index.row())

        elif self.location_model.columns[index.column()].name == "hover":
            width = 2
            if item.hover.value:
                width = 5
            item.plots(self.current_parameter["parameters"], time_units=self.current_time_units, absolute=self.absolute).setPen(
                color=item.color.qvalue, width=width, style=item.result.value._pattern)

        elif self.location_model.columns[index.column()].name == "color":
            item.plots(self.current_parameter["parameters"], time_units=self.current_time_units, absolute=self.absolute).setPen(
                color=item.color.qvalue, width=2, style=item.result.value._pattern)

    def hide_timeseries(self, location_nr):
        """
        hide timeseries of location in graph
        :param row_nr: integer, row number of location
        """

        plot = self.location_model.rows[location_nr].plots(
            self.current_parameter["parameters"], time_units=self.current_time_units, absolute=self.absolute
        )
        self.removeItem(plot)

    def show_timeseries(self, location_nr):
        """
        show timeseries of location in graph
        :param row_nr: integer, row number of location
        """

        plot = self.location_model.rows[location_nr].plots(
            self.current_parameter["parameters"], time_units=self.current_time_units, absolute=self.absolute
        )
        self.addItem(plot)

    def set_parameter(self, parameter, time_units):
        """
        on selection of parameter (in combobox), change timeseries in graphs
        :param parameter: parameter identification string
        :param time_units: current time units string
        """

        if self.current_parameter == parameter and self.current_time_units == time_units:
            return

        old_parameter = self.current_parameter
        old_time_units = self.current_time_units
        self.current_parameter = parameter
        self.current_time_units = time_units

        for item in self.location_model.rows:
            if not item.active.value:
                continue

            self.removeItem(
                item.plots(old_parameter["parameters"], time_units=old_time_units, absolute=self.absolute)
            )
            self.addItem(
                item.plots(self.current_parameter["parameters"], time_units=self.current_time_units, absolute=self.absolute)
            )

        self.setLabel(
            "left", self.current_parameter["name"], self.current_parameter["unit"]
        )


class LocationTimeseriesTable(QTableView):

    hoverExitRow = pyqtSignal(int)
    hoverExitAllRows = pyqtSignal()  # exit the whole widget
    hoverEnterRow = pyqtSignal(int, str, ThreeDiResultItem)
    deleteRequested = pyqtSignal(list)

    def __init__(self, parent=None):
        super().__init__(parent)
        self.setStyleSheet("QTreeView::item:hover{background-color:#FFFF00;}")
        self.setMouseTracking(True)
        self.verticalHeader().hide()
        self.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
        self.model = None

        self._last_hovered_row = None
        self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
        self.customContextMenuRequested.connect(self.customMenuRequested)
        self.viewport().installEventFilter(self)

    def customMenuRequested(self, pos):
        selectionModel = self.selectionModel()
        menu = QMenu(self)
        action_delete = QAction("Delete", self)
        action_delete.triggered.connect(lambda _, sel_inx=selectionModel.selectedRows(): self.deleteRequested.emit(sel_inx))
        menu.addAction(action_delete)
        menu.popup(self.viewport().mapToGlobal(pos))

    def on_close(self):
        """
        unloading widget and remove all required stuff
        :return:
        """
        self.setMouseTracking(False)
        self.viewport().removeEventFilter(self)

    def closeEvent(self, event):
        """
        overwrite of QDockWidget class to emit signal
        :param event: QEvent
        """
        self.on_close()
        event.accept()

    def eventFilter(self, widget, event):
        if widget is self.viewport():
            if event.type() == QEvent.Type.MouseButtonDblClick:

                if event.button() == Qt.MouseButton.RightButton:
                    return True
                # map mouse position to index
                column = self.indexAt(event.pos()).column()
                if self.model.columns[column].name == "color":
                    row = self.indexAt(event.pos()).row()
                    item = self.model.rows[row]
                    selected_color = QColorDialog.getColor()
                    if not selected_color.isValid():  # User pressed cancel
                        return True

                    item.color.value = (selected_color.red(), selected_color.green(), selected_color.blue())

                return QTableView.eventFilter(self, widget, event)

            elif event.type() == QEvent.Type.MouseMove:
                row = self.indexAt(event.pos()).row()
                if row == 0 and self.model and row > self.model.rowCount():
                    row = None

            elif event.type() == QEvent.Type.Leave:
                row = None
                self.hoverExitAllRows.emit()
            else:
                row = self._last_hovered_row

            if row != self._last_hovered_row:
                if self._last_hovered_row is not None:
                    try:
                        self.hover_exit(self._last_hovered_row)
                    except IndexError:
                        logger.warning(
                            "Hover row index %s out of range" % self._last_hovered_row
                        )
                    # self.hoverExitRow.emit(self._last_hovered_row)
                # self.hoverEnterRow.emit(row)
                if row is not None:
                    try:
                        self.hover_enter(row)
                    except IndexError:
                        logger.warning("Hover row index %s out of range" % row)
                self._last_hovered_row = row
                pass
        return QTableView.eventFilter(self, widget, event)

    def hover_exit(self, row_nr):
        if row_nr >= 0:
            item = self.model.rows[row_nr]
            item.hover.value = False

    def hover_enter(self, row_nr):
        if row_nr >= 0:
            item = self.model.rows[row_nr]
            self.hoverEnterRow.emit(item.object_id.value, item.object_type.value, item.result.value)
            item.hover.value = True

    def setModel(self, model):
        super().setModel(model)
        self.model = model
        self.model.dataChanged.connect(self._update_table_widgets)
        self.model.rowsInserted.connect(self._update_table_widgets)
        self.model.rowsAboutToBeRemoved.connect(self._update_table_widgets)
        self.setVisible(False)
        self.resizeColumnsToContents()
        self.horizontalHeader().setStretchLastSection(True)
        self.setVisible(True)
        self.model.set_column_sizes_on_view(self)
        # Columns checkbox can be set small always
        self.setColumnWidth(0, 20)  # checkbox

    def _update_table_widgets(self):
        """The PenStyle widget is not part of the model, but explicitely added/overlayed to the table"""
        for i in range(self.model.rowCount()):
            item = self.model.rows[i]
            index = self.model.index(i, 1)
            pen_color = QColor(item.color.value[0], item.color.value[1], item.color.value[2])
            # If index widget A is replaced with index widget B, index widget A will be deleted.
            patternWidget = PenStyleWidget(item.result.value._pattern, pen_color, 2, self)
            # patternWidget.setAutoFillBackground(True)
            patternWidget.setPalette(self.palette())
            self.setIndexWidget(index, patternWidget)


class GraphWidget(QWidget):
    def __init__(
        self,
        parent=None,
        model: ThreeDiPluginModel = None,
        parameter_config=[],
        name="",
        geometry_type=QgsWkbTypes.Type.Point,
    ):
        super().__init__(parent)

        self.name = name
        self.model = model
        self.parent = parent
        self.geometry_type = geometry_type

        self.setup_ui()

        self.location_model = LocationTimeseriesModel(self.model)
        self.graph_plot.set_location_model(self.location_model)
        self.graph_plot.set_result_model(self.model)
        self.location_timeseries_table.setModel(self.location_model)
        self._updateHiddenColumns(self.showFullLegendCheckbox.checkState())

        # set listeners
        self.parameter_combo_box.currentIndexChanged.connect(self.parameter_change)
        self.ts_units_combo_box.currentIndexChanged.connect(self.time_units_change)
        self.showFullLegendCheckbox.stateChanged.connect(self._updateHiddenColumns)
        self.location_timeseries_table.deleteRequested.connect(self._removeRows)

        # init parameter selection
        self.set_parameter_list(parameter_config)

        self.marker = QgsRubberBand(self.parent.iface.mapCanvas())
        self.marker.setColor(Qt.GlobalColor.red)
        self.marker.setWidth(2)

    def _removeRows(self, index_list):
        # Also here, remove in decreasing order to keep table idx valid
        row_list = [index.row() for index in index_list]
        row_list.sort(reverse=True)
        for row in row_list:
            self.location_model.removeRows(row, 1)

    def _updateHiddenColumns(self, state):
        if state == Qt.CheckState.Unchecked:
            for i in range(3, 7):
                self.location_timeseries_table.setColumnHidden(i, True)
        else:
            for i in range(7):
                self.location_timeseries_table.setColumnHidden(i, False)

    def refresh_table(self):
        # trigger all listeners by emiting dataChanged signal
        logger.info("Refreshing table")
        self.location_model.beginResetModel()
        self.location_model.endResetModel()
        self.location_timeseries_table._update_table_widgets()

    @pyqtSlot(ThreeDiResultItem)
    def result_removed(self, result_item: ThreeDiResultItem):
        # Remove corresponding plots that refer to this item
        item_idx_to_remove = []
        for count, item in enumerate(self.location_model.rows):
            if item.result.value is result_item:
                item_idx_to_remove.append(count)

        # We delete them descending to keep the row idx consistent
        for item_idx in reversed(item_idx_to_remove):
            self.location_model.removeRows(item_idx, 1)

        # In case there are no more other results in results model, we clean up the parameter combobox
        if len(self.model.get_results(checked_only=False)) == 1:
            self.parameter_combo_box.clear()

    def set_parameter_list(self, parameter_config):
        self.parameter_combo_box.clear()

        if not parameter_config:
            return

        self.parameters = dict([(p["name"], p) for p in parameter_config])

        params = sorted([p["name"] for p in parameter_config])

        Q_CUM = 'Net cumulative discharge'
        active = {'Waterlevel', Q_CUM}
        if Q_CUM not in params:
            active.add('Discharge')
        active_idx = None

        for idx, param in enumerate(params):
            self.parameter_combo_box.addItem(param)
            if param in active:
                active_idx = idx

        self.parameter_combo_box.setCurrentIndex(active_idx or 0)

    def on_close(self):
        """
        unloading widget and remove all required stuff
        :return:
        """
        self.parameter_combo_box.currentIndexChanged.disconnect(self.parameter_change)

    def closeEvent(self, event):
        """
        overwrite of QDockWidget class to emit signal
        :param event: QEvent
        """
        self.on_close()
        event.accept()

    def highlight_feature(self, obj_id, obj_type, result_item: ThreeDiResultItem):

        for table_name, layer_id in result_item.parent().layer_ids.items():

            if obj_type == table_name:
                # query layer for object
                filt = u'"id" = {0}'.format(obj_id)
                request = QgsFeatureRequest().setFilterExpression(filt)
                lyr = QgsProject.instance().mapLayer(layer_id)
                features = lyr.getFeatures(request)
                for feature in features:
                    self.marker.setToGeometry(feature.geometry(), lyr)

    def unhighlight_all_features(self):
        """Remove the highlights from all layers"""
        self.marker.reset()

    def setup_ui(self):

        mainLayout = QHBoxLayout(self)
        self.setLayout(mainLayout)

        splitterWidget = QSplitter(self)

        # add plot
        self.graph_plot = GraphPlot(self)
        sizePolicy = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
        sizePolicy.setHorizontalStretch(1)
        sizePolicy.setVerticalStretch(1)
        sizePolicy.setHeightForWidth(self.graph_plot.sizePolicy().hasHeightForWidth())
        self.graph_plot.setSizePolicy(sizePolicy)
        self.graph_plot.setMinimumSize(QSize(250, 250))
        splitterWidget.addWidget(self.graph_plot)

        # add widget for timeseries table and other controls
        legendWidget = QWidget(self)
        vLayoutTable = QVBoxLayout(self)
        legendWidget.setLayout(vLayoutTable)

        # add comboboxes
        self.ts_units_combo_box = QComboBox(self)
        self.ts_units_combo_box.insertItems(0, ["hrs", "mins", "s"])
        self.parameter_combo_box = QComboBox(self)
        vLayoutTable.addWidget(self.parameter_combo_box)
        vLayoutTable.addWidget(self.ts_units_combo_box)

        # add timeseries table
        self.location_timeseries_table = LocationTimeseriesTable(self)
        self.location_timeseries_table.hoverEnterRow.connect(self.highlight_feature)
        self.location_timeseries_table.hoverExitAllRows.connect(self.unhighlight_all_features)
        sizePolicy = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(0)
        sizePolicy.setHeightForWidth(self.location_timeseries_table.sizePolicy().hasHeightForWidth())
        self.location_timeseries_table.setSizePolicy(sizePolicy)
        self.location_timeseries_table.setMinimumSize(QSize(250, 0))
        vLayoutTable.addWidget(self.location_timeseries_table)

        # add button below table
        hLayoutButtons = QHBoxLayout(self)
        hLayoutButtons.addItem(QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum))

        self.showFullLegendCheckbox = QCheckBox("Show full legend", self)
        self.showFullLegendCheckbox.setCheckState(Qt.CheckState.Unchecked)
        hLayoutButtons.addWidget(self.showFullLegendCheckbox)

        vLayoutTable.addLayout(hLayoutButtons)

        splitterWidget.addWidget(legendWidget)

        mainLayout.addWidget(splitterWidget)

    def parameter_change(self, nr):
        """
        set current selected parameter and trigger refresh of graphs
        :param nr: nr of selected option of combobox
        :return:
        """
        if nr == -1:
            return  # Combobox cleared

        self.current_parameter = self.parameters[self.parameter_combo_box.currentText()]
        time_units = self.ts_units_combo_box.currentText()
        self.graph_plot.setLabel("bottom", "Time", time_units)
        self.graph_plot.set_parameter(self.current_parameter, time_units)
        self.graph_plot.plotItem.vb.menu.viewAll.triggered.emit()

    def time_units_change(self):
        parameter_idx = self.parameter_combo_box.currentIndex()
        self.parameter_change(parameter_idx)

    def get_feature_index(self, layer, feature):
        """
        get the id of the selected id feature
        :param layer: selected Qgis layer to be added
        :param feature: selected Qgis feature to be added
        :return: idx (integer)
        We can't do ``feature.id()``, so we have to pick something that we
        have agreed on. For now we have hardcoded the 'id' field as the
        default, but that doesn't mean it's always the case in the future
        when more layers are added!
        """
        idx = feature.id()
        if layer.dataProvider().name() in PROVIDERS_WITHOUT_PRIMARY_KEY:
            idx = feature["id"]
        return idx

    def get_object_name(self, layer, feature):
        """
        get the object_name (display_name / type)  of the selected id feature
        :param layer: selected Qgis layer to be added
        :param feature: selected Qgis feature to be added
        :return: object_name (string)
        To get a object_name we use the following logic:
        - get the 'display_name' column if available;
        - if not: get the 'type' column if available;
        - if not: get the 'line_type' column if available;
        - if not: get the 'node_type' column if available;
        - if not: object_name = 'N/A'
        """
        for column_nr, field in enumerate(layer.fields()):
            if "display_name" in field.name():
                return feature[column_nr]
        for column_nr, field in enumerate(layer.fields()):
            if field.name() == "type":
                return feature[column_nr]

        # Apply ValueMap field formatter
        for column_nr, field in enumerate(layer.fields()):
            if field.name() == "line_type":
                config = layer.editorWidgetSetup(column_nr).config()
                return QgsValueMapFieldFormatter().representValue(layer, column_nr, config, None, feature[column_nr])
        for column_nr, field in enumerate(layer.fields()):
            if field.name() == "node_type":
                config = layer.editorWidgetSetup(column_nr).config()
                return QgsValueMapFieldFormatter().representValue(layer, column_nr, config, None, feature[column_nr])

        logger.warning("Layer has no 'display_name', it's probably a result "
                       "layer, but putting a placeholder object name just "
                       "for safety."
                       )

        return "N/A"

    def add_objects(self, layer: QgsVectorLayer, features: List[QgsFeature]) -> bool:
        """
        :param layer: layer of features
        :param features: Qgis layer features to be added
        :return: boolean: new objects are added
        """

        if not is_threedi_layer(layer):
            msg = """Please select results from either the 'flowlines', 'nodes', 'cells' or
            'pumplines' layer."""
            messagebar_message(TOOLBOX_MESSAGE_TITLE, msg, Qgis.MessageLevel.Warning, 5.0)
            return False

        if len(self.model.get_results(checked_only=False)) == 0:
            logger.warning("No results loaded")
            return False

        # Retrieve summary of existing items in model (!= graph plots)
        existing_items = [
            f"{item.object_type.value}_{str(item.object_id.value)}_{item.result.value.id}" for item in self.location_model.rows
        ]

        # Determine new items
        new_items = []
        for feature in features:
            new_idx = self.get_feature_index(layer, feature)
            new_object_name = self.get_object_name(layer, feature)

            result_items = self.model.get_results(checked_only=False)
            for result_item in result_items:
                # Check whether this result belongs to the selected grid
                if layer.id() not in result_item.parent().layer_ids.values():
                    continue

                # Check whether a pump isn't already plotted as pump_linestring or vice versa (QGIS doesn't know they are the same thing)
                if layer.objectName() == "pump_linestring":
                    if ("pump_" + str(new_idx) + "_" + result_item.id) in existing_items:
                        logger.error("Pump already plotted as node item")
                        continue
                elif layer.objectName() == "pump":
                    if ("pump_linestring_" + str(new_idx) + "_" + result_item.id) in existing_items:
                        logger.error("Pump already plotted as line item")
                        continue

                # Check whether a node isn't already plotted as cell or vice versa (QGIS doesn't know they are the same thing)
                if layer.objectName() == "cell":
                    if ("node_" + str(new_idx) + "_" + result_item.id) in existing_items:
                        logger.error("Cell already plotted as node item")
                        continue
                elif layer.objectName() == "node":
                    if ("cell_" + str(new_idx) + "_" + result_item.id) in existing_items:
                        # Assert whether this is a 2D node
                        assert new_object_name.startswith("2D")
                        logger.error("Node already plotted as cell item")
                        continue

                if (layer.objectName() + "_" + str(new_idx) + "_" + result_item.id) not in existing_items:
                    item = {
                        "object_type": layer.objectName(),
                        "object_id": new_idx,
                        "object_name": new_object_name,
                        "object_label": f"{result_item.parent().text()} | {result_item.text()} | ID: {new_idx}",
                        "result": result_item,
                        "color": self.location_model.get_color(new_idx, layer.id()),
                    }
                    new_items.append(item)

        # Small usability tweak, if we are adding a pump flowline, set a specific parameter
        if not existing_items and "pump" in layer.objectName() and new_items:
            # Find the corresponding name for q_cum, set parameter (and set combobox)
            pump_params = [p for p in self.parameters.values() if p["parameters"] == "q_pump"]
            if pump_params:
                self.graph_plot.set_parameter(pump_params[0], self.ts_units_combo_box.currentText())
                combo_idx = self.parameter_combo_box.findText(pump_params[0]["name"])
                assert combo_idx != -1
                # Prevent the combobox to trigger other signals (and set the parameter again)
                self.parameter_combo_box.blockSignals(True)
                self.parameter_combo_box.setCurrentIndex(combo_idx)
                self.parameter_combo_box.blockSignals(False)

        if len(new_items) > 20:
            msg = (
                "%i new objects selected. Adding those to the plot can "
                "take a while. Do you want to continue?" % len(new_items)
            )
            reply = QMessageBox.question(
                self, "Add objects", msg, QMessageBox.StandardButton.Yes, QMessageBox.StandardButton.No
            )

            if reply == QMessageBox.StandardButton.No:
                return False

        self.location_model.insertRows(new_items)
        msg = "%i new objects added to plot " % len(new_items)
        skipped_items = len(features) - len(new_items)
        if skipped_items > 0:
            msg += "(skipped %s already present objects)" % skipped_items

        statusbar_message(msg)
        return True

    def remove_objects_table(self):
        """
        removes selected objects from table
        :return:
        """
        selection_model = self.location_timeseries_table.selectionModel()
        # get unique rows in selected fields
        rows = set([index.row() for index in selection_model.selectedIndexes()])
        for row in reversed(sorted(rows)):
            self.location_model.removeRows(row, 1)


class GraphDockWidget(QDockWidget):
    """Main Dock Widget for showing Rana simulation results in Graphs"""

    closingWidget = pyqtSignal(int)

    def __init__(self, iface, nr, model: ThreeDiPluginModel):
        super().__init__()

        self.iface = iface
        self.nr = nr
        self.model = model

        self.setup_ui()

        parameter_config = self._get_active_parameter_config()

        # add graph widgets
        self.q_graph_widget = GraphWidget(
            self,
            self.model,
            parameter_config["q"],
            "Flowlines && pumps",
            QgsWkbTypes.Type.LineString,
        )
        self.h_graph_widget = GraphWidget(
            self,
            self.model,
            parameter_config["h"],
            "Nodes && cells",
            QgsWkbTypes.Type.Point,
        )
        self.graphTabWidget.addTab(self.q_graph_widget, self.q_graph_widget.name)
        self.graphTabWidget.addTab(self.h_graph_widget, self.h_graph_widget.name)

        # add listeners
        self.addFlowlinePumpButton.clicked.connect(self.add_flowline_pump_button_clicked)
        self.addNodeCellButton.clicked.connect(self.add_node_cell_button_clicked)

        # add map tools
        self.map_tool_add_flowline_pump = AddFlowlinePumpMapTool(
            widget=self, canvas=self.iface.mapCanvas(),
        )
        self.map_tool_add_flowline_pump.setButton(self.addFlowlinePumpButton)
        self.map_tool_add_flowline_pump.setCursor(Qt.CursorShape.CrossCursor)
        self.map_tool_add_node_cell = AddNodeCellMapTool(
            widget=self, canvas=self.iface.mapCanvas(),
        )
        self.map_tool_add_node_cell.setButton(self.addNodeCellButton)
        self.map_tool_add_node_cell.setCursor(Qt.CursorShape.CrossCursor)

        # In case this dock widget becomes (in)visible, we disable the route tools
        self.visibilityChanged.connect(self.unset_map_tools)

    def on_close(self):
        """
        unloading widget and remove all required stuff
        :return:
        """
        self.addFlowlinePumpButton.clicked.disconnect(self.add_flowline_pump_button_clicked)
        self.addNodeCellButton.clicked.disconnect(self.add_node_cell_button_clicked)

        self.map_tool_add_flowline_pump = None
        self.map_tool_add_node_cell = None

        # self.q_graph_widget.close()
        # self.h_graph_widget.close()

    def closeEvent(self, event):
        """
        overwrite of QDockWidget class to emit signal
        :param event: QEvent
        """
        self.on_close()
        self.closingWidget.emit(self.nr)
        event.accept()

    def _get_active_parameter_config(self, result_item_ignored: ThreeDiResultItem = None):
        """
        Generates a parameter dict based on results, takes union of parameters from results.
        """
        q_vars = []
        h_vars = []

        for result in self.model.get_results(checked_only=False):
            if result is result_item_ignored:  # about to be deleted
                continue

            threedi_result = result.threedi_result
            available_subgrid_vars = threedi_result.available_subgrid_map_vars
            available_agg_vars = threedi_result.available_aggregation_vars[:]  # a copy
            available_wq_vars = threedi_result.available_water_quality_vars[:]  # a copy
            available_sca_vars = threedi_result.available_structure_control_actions_vars[:]  # a copy
            available_debug_vars = threedi_result.available_debug_vars[:]
            if not available_agg_vars:
                messagebar_message("Warning", "No aggregation netCDF was found.", level=1, duration=5)

            parameter_config = generate_parameter_config(
                available_subgrid_vars, agg_vars=available_agg_vars, wq_vars=available_wq_vars, sca_vars=available_sca_vars, debug_vars=available_debug_vars
            )

            def _union(a: List, b: List):
                if not a:
                    return b

                for param in b:
                    # Check whether a contains param with same name, if so, don't add
                    if not [x for x in a if x["name"] == param["name"]]:
                        a.append(param)

                return a

            q_vars = _union(q_vars, parameter_config["q"])
            h_vars = _union(h_vars, parameter_config["h"])

        return {"q": q_vars, "h": h_vars}

    def result_added(self, _: ThreeDiResultItem):
        parameter_config = self._get_active_parameter_config()
        self.q_graph_widget.set_parameter_list(parameter_config["q"])
        self.h_graph_widget.set_parameter_list(parameter_config["h"])

    def result_removed(self, result_item: ThreeDiResultItem):
        parameter_config = self._get_active_parameter_config(result_item)
        self.q_graph_widget.result_removed(result_item)
        self.h_graph_widget.result_removed(result_item)
        self.q_graph_widget.set_parameter_list(parameter_config["q"])
        self.h_graph_widget.set_parameter_list(parameter_config["h"])

    def result_changed(self, _: ThreeDiResultItem):
        self.q_graph_widget.refresh_table()
        self.h_graph_widget.refresh_table()

    def grid_changed(self, result_item: ThreeDiGridItem):
        self.q_graph_widget.refresh_table()
        self.h_graph_widget.refresh_table()

    def on_btnAbsoluteState(self, state):
        """Toggle ``absolute`` state of the GraphPlots."""
        checked = (state == Qt.CheckState.Checked)
        self.q_graph_widget.graph_plot.set_absolute(checked)
        self.h_graph_widget.graph_plot.set_absolute(checked)

    def setup_ui(self):

        self.setObjectName("dock_widget")
        self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose)

        self.dockWidgetContent = QWidget(self)
        self.dockWidgetContent.setObjectName("dockWidgetContent")

        self.mainVLayout = QVBoxLayout(self.dockWidgetContent)
        self.dockWidgetContent.setLayout(self.mainVLayout)

        self.buttonBarHLayout = QHBoxLayout(self)

        self.buttonBarHLayout.setSpacing(10)

        selection_flowline_pump_menu = QMenu(self)
        action_group = QActionGroup(self)
        action_group.setExclusive(True)
        self.flowline_single_pick = selection_flowline_pump_menu.addAction("Pick single flowline/pump")
        self.flowline_single_pick.setCheckable(True)
        self.flowline_single_pick.setChecked(True)
        self.flowline_single_pick.toggled.connect(self._changeFlowlineSelectionMode)
        action_group.addAction(self.flowline_single_pick)
        selected_pick = selection_flowline_pump_menu.addAction("Add all selected flowlines/pump")
        selected_pick.setCheckable(True)
        action_group.addAction(selected_pick)

        selection_node_cell_menu = QMenu(self)
        action_group = QActionGroup(self)
        action_group.setExclusive(True)
        self.node_single_pick = selection_node_cell_menu.addAction("Pick single node/cell")
        self.node_single_pick.setCheckable(True)
        self.node_single_pick.setChecked(True)
        self.node_single_pick.toggled.connect(self._changeNodeCellSelectionMode)
        action_group.addAction(self.node_single_pick)
        selected_pick = selection_node_cell_menu.addAction("Add all selected nodes/cells")
        selected_pick.setCheckable(True)
        action_group.addAction(selected_pick)

        self.addFlowlinePumpButton = QToolButton(parent=self.dockWidgetContent)
        self.addFlowlinePumpButton.setCheckable(True)
        self.addFlowlinePumpButton.setText("Pick flowlines/pumps")
        self.addFlowlinePumpButton.setPopupMode(QToolButton.ToolButtonPopupMode.MenuButtonPopup)
        self.addFlowlinePumpButton.setMenu(selection_flowline_pump_menu)
        self.buttonBarHLayout.addWidget(self.addFlowlinePumpButton)

        self.addNodeCellButton = QToolButton(parent=self.dockWidgetContent)
        self.addNodeCellButton.setText("Pick nodes/cells")
        self.addNodeCellButton.setCheckable(True)
        self.addNodeCellButton.setPopupMode(QToolButton.ToolButtonPopupMode.MenuButtonPopup)
        self.addNodeCellButton.setMenu(selection_node_cell_menu)
        self.buttonBarHLayout.addWidget(self.addNodeCellButton)

        self.absoluteCheckbox = QCheckBox("Absolute", parent=self.dockWidgetContent)
        self.absoluteCheckbox.setChecked(False)
        self.absoluteCheckbox.stateChanged.connect(self.on_btnAbsoluteState)
        self.buttonBarHLayout.addWidget(self.absoluteCheckbox)

        spacerItem = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
        self.buttonBarHLayout.addItem(spacerItem)

        self.mainVLayout.addItem(self.buttonBarHLayout)

        # add tabWidget for graphWidgets
        self.graphTabWidget = QTabWidget(self.dockWidgetContent)
        sizePolicy = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
        sizePolicy.setHorizontalStretch(6)
        sizePolicy.setVerticalStretch(0)
        sizePolicy.setHeightForWidth(
            self.graphTabWidget.sizePolicy().hasHeightForWidth()
        )
        self.graphTabWidget.setSizePolicy(sizePolicy)
        self.graphTabWidget.setObjectName("graphTabWidget")
        self.mainVLayout.addWidget(self.graphTabWidget)

        # add dockwidget
        self.setWidget(self.dockWidgetContent)
        self.setWindowTitle("Time series %i" % self.nr)
        QMetaObject.connectSlotsByName(self)

    def _changeFlowlineSelectionMode(self, single_pick_selected: bool) -> None:
        if not single_pick_selected:
            if self.iface.mapCanvas().mapTool() is self.map_tool_add_flowline_pump:
                self.iface.mapCanvas().unsetMapTool(self.map_tool_add_flowline_pump)
            self.addFlowlinePumpButton.setCheckable(False)
            self.addFlowlinePumpButton.setText("Add flowlines/pumps")
        else:
            self.addFlowlinePumpButton.setCheckable(True)
            self.addFlowlinePumpButton.setText("Pick flowlines/pumps")

    def _changeNodeCellSelectionMode(self, single_pick_selected: bool) -> None:
        if not single_pick_selected:
            if self.iface.mapCanvas().mapTool() is self.map_tool_add_node_cell:
                self.iface.mapCanvas().unsetMapTool(self.map_tool_add_node_cell)
            self.addNodeCellButton.setCheckable(False)
            self.addNodeCellButton.setText("Add nodes/cells")
        else:
            self.addNodeCellButton.setCheckable(True)
            self.addNodeCellButton.setText("Pick nodes/cells")

    def add_flowline_pump_button_clicked(self):
        if self.flowline_single_pick.isChecked():
            self.iface.mapCanvas().setMapTool(self.map_tool_add_flowline_pump)
        else:
            current_layer = self.iface.mapCanvas().currentLayer()
            if not current_layer or current_layer.objectName() not in ['flowline', 'pump_linestring', 'pump']:
                logger.error("Select features from flowline or pump layer first.")
                return

            self.add_results([QgsMapToolIdentify.IdentifyResult(current_layer, f, dict()) for f in current_layer.selectedFeatures()], feature_type=FLOWLINE_OR_PUMP)

    def add_node_cell_button_clicked(self):
        if self.node_single_pick.isChecked():
            self.iface.mapCanvas().setMapTool(self.map_tool_add_node_cell)
        else:
            current_layer = self.iface.mapCanvas().currentLayer()

            if not current_layer or current_layer.objectName() not in ['node', 'cell']:
                logger.error("Select features from node or cell layer first.")
                return

            self.add_results([QgsMapToolIdentify.IdentifyResult(current_layer, f, dict()) for f in current_layer.selectedFeatures()], feature_type=NODE_OR_CELL)

    def unset_map_tools(self):
        if self.iface.mapCanvas().mapTool() is self.map_tool_add_node_cell:
            self.iface.mapCanvas().unsetMapTool(self.map_tool_add_node_cell)
        elif self.iface.mapCanvas().mapTool() is self.map_tool_add_flowline_pump:
            self.iface.mapCanvas().unsetMapTool(self.map_tool_add_flowline_pump)

    def add_results(self, results, feature_type, single_feature_per_layer=False):
        """
        Add results for features of specific types.
        """
        if feature_type == FLOWLINE_OR_PUMP:
            layer_keys = ['flowline', 'pump_linestring', 'pump']
            graph_widget = self.q_graph_widget
        elif feature_type == NODE_OR_CELL:
            layer_keys = ['node', 'cell']
            graph_widget = self.h_graph_widget
        item = self.model.invisibleRootItem()

        relevant_grid_layer_ids = []
        for layer_key in layer_keys:
            for i in range(item.rowCount()):
                if layer_key in item.child(i).layer_ids:
                    relevant_grid_layer_ids.append(item.child(i).layer_ids[layer_key])

        layers_added = set()
        for result in results:
            layer_id = result.mLayer.id()
            if layer_id not in relevant_grid_layer_ids:
                continue
            if single_feature_per_layer and layer_id in layers_added:
                continue
            graph_widget.add_objects(result.mLayer, [result.mFeature])
            layers_added.add(layer_id)

        if layers_added:
            tab_index = self.graphTabWidget.indexOf(graph_widget)
            self.graphTabWidget.setCurrentIndex(tab_index)

            graph_widget.graph_plot.plotItem.vb.menu.viewAll.triggered.emit()


class BaseAddMapTool(QgsMapToolIdentify):
    def __init__(self, widget, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.widget = widget

    def canvasReleaseEvent(self, event):
        results = self.identify(
            x=int(event.pos().x()),
            y=int(event.pos().y()),
            layerList=self.parent().layers(),
        )
        self.widget.add_results(
            results=results, feature_type=self.feature_type, single_feature_per_layer=True
        )


class AddFlowlinePumpMapTool(BaseAddMapTool):
    feature_type = FLOWLINE_OR_PUMP


class AddNodeCellMapTool(BaseAddMapTool):
    feature_type = NODE_OR_CELL
