# SPDX-FileCopyrightText: 2025 XLeitstelle Planen und Bauen <xleitstelle@gv.hamburg.de>
# SPDX-FileContributor: Michael Holzapfel <michael.holzapfel@geocledian.com>
#
# SPDX-License-Identifier: EUPL-1.2

from typing import Any, Dict, List, Optional, Tuple

from qgis.core import (
    QgsGeometry,
    QgsLayerTreeLayer,
    QgsMapLayer,
    QgsPointXY,
    QgsProject,
    QgsRectangle,
    QgsVectorLayer,
)
from qgis.gui import QgisInterface
from qgis.PyQt.QtCore import (
    QAbstractTableModel,
    QModelIndex,
    QSortFilterProxyModel,
    Qt,
    QVariant,
)
from qgis.utils import iface

from xmas_plugin.topo_check import TopoTableView, get_layer_group_hierarchy
from xmas_plugin.util.geom import transform_to_project_crs
from xmas_plugin.util.metadata import PLUGIN_DIR_NAME


class BoundsTableModel(QAbstractTableModel):
    def __init__(
        self,
        data: List[Dict[str, Any]],
        columns: List[str],
        parent: Optional[Any] = None,
    ) -> None:
        super().__init__(parent)
        self._data = data
        self._columns = columns

    def rowCount(self, parent: QModelIndex = QModelIndex()) -> int:
        return len(self._data)

    def columnCount(self, parent: QModelIndex = QModelIndex()) -> int:
        return len(self._columns)

    def data(self, index: QModelIndex, role: int = Qt.DisplayRole) -> Any:
        if index.isValid() and role == Qt.UserRole:
            item = self._data[index.row()]
            return item
        if not index.isValid() or role != Qt.DisplayRole:
            return QVariant()
        try:
            if index.column() == 0:
                return str(self._data[index.row()]["feature"].attribute("id"))
            elif index.column() == 1:
                return str(self._data[index.row()]["layer"].name())
            elif index.column() == 2:
                return str(self._data[index.row()]["Typ"])
            else:
                return QVariant()
        except RuntimeError:
            # TODO: logger message that probably a layer was deleted
            # "wrapped C/C++ object of type QgsVectorLayer has been deleted"
            return QVariant()

    def headerData(
        self, section: int, orientation: Qt.Orientation, role: int = Qt.DisplayRole
    ) -> Any:
        if role != Qt.DisplayRole:
            return QVariant()
        if orientation == Qt.Horizontal:
            return self._columns[section]
        return section + 1

    def repopulate(self, new_data) -> None:
        # Notify views that model is about to reset
        self.beginResetModel()
        self._data = new_data
        # Notify views that model reset is done
        self.endResetModel()


class BoundsTableView(TopoTableView):
    def __init__(self, iface: QgisInterface, parent=None):
        super().__init__(parent)
        self.iface = iface

        self.gap_features: List[Tuple[QgsGeometry, QgsVectorLayer]] = []

        self.model = BoundsTableModel([], ["Feature", "Layer", "Typ"])
        self.proxymodel = QSortFilterProxyModel()
        self.proxymodel.setSourceModel(self.model)
        self.setModel(self.proxymodel)
        self.setSortingEnabled(True)

        self.clicked.connect(self.on_click)
        self.proxymodel.modelReset.connect(self.resizeColumnsToContents)

    def repopulate(self, new_data) -> None:
        self.model.repopulate(new_data)

    def on_click(self, index: QModelIndex) -> None:
        def _select_layer(layer: QgsVectorLayer) -> None:
            """
            Highlights/selects a layer in the QGis Layers Dock,
            if the layer is part of a (nested) group QGis should expand
            thos groups automatically
            :param layer:
            :return:
            """
            if not layer:
                return

            root = QgsProject.instance().layerTreeRoot()
            layer_node = root.findLayer(layer.id())
            if layer_node is None:
                return

            self.iface.layerTreeView().setCurrentLayer(layer)
            return

        def _select_gap_layers(
            gap_geom: QgsGeometry,
            gap_features: List[Tuple[QgsGeometry, QgsVectorLayer]],
        ) -> None:
            touching_layers = set()
            for gap, layer in gap_features:
                if gap.geometry().touches(gap_geom):
                    touching_layers.add(layer)
            for layer in touching_layers:
                _select_layer(layer)

        # collect geometry from data
        # noinspection PyUnresolvedReferences
        src_index: QModelIndex = index.model().mapToSource(index)
        row_data = self.model.data(src_index, Qt.UserRole)

        if index.column() == 0:
            # highlight feature

            geom = transform_to_project_crs(
                row_data["feature"].geometry(), self.plan_crs
            )

            # get layer-tree-group that current faeture/layer belongs to,
            # (working with groups from layer tree view instead of collecting layers by plan_id because
            # during split-operation there is a possibility of collisions
            # -> same plan_id for ORG, INSIDE, OUTSIDE plans
            groups = get_layer_group_hierarchy(row_data["layer"])
            root_group = groups[0]
            root_group_layers: List[QgsLayerTreeLayer] = root_group.findLayers()

            # get boundary reference geoemtry (feature in either plan or section layer)
            boundary_ref_geom: QgsGeometry = None
            match row_data["Typ"]:
                case "Plan":
                    # loop over every layer unitl a layer with type plan is found
                    for _tl in root_group_layers:
                        _l: QgsVectorLayer = _tl.layer()
                        if (
                            _l.customProperties().value(f"{PLUGIN_DIR_NAME}/layer_type")
                            == "plan"
                        ):
                            _l: QgsVectorLayer
                            if _l.featureCount() != 1:
                                raise RuntimeError(
                                    "XMAS_PLUGIN: plan-layer has not exactly one feature"
                                )
                            boundary_ref_geom = next(_l.getFeatures()).geometry()
                            break
                case "Bereich":
                    # find out which section layer is the reference for the current layer
                    for _layer_tree_group in groups[::-1]:
                        _layers: List[QgsMapLayer | QgsVectorLayer] = [
                            _tree_layer.layer()
                            for _tree_layer in _layer_tree_group.findLayers()
                        ]
                        _layer_types = [
                            _l.customProperties().value(f"{PLUGIN_DIR_NAME}/layer_type")
                            for _l in _layers
                        ]
                        if "section" not in _layer_types:
                            continue
                        else:
                            if _layer_types.count("section") != 1:
                                raise RuntimeError(
                                    "XMAS_PLUGIN: not exactly one single section layer"
                                )
                            _l: QgsVectorLayer = _layers[_layer_types.index("section")]
                            if _l.featureCount() != 1:
                                raise RuntimeError(
                                    "XMAS_PLUGIN: section-layer has not exactly one feature"
                                )
                            boundary_ref_geom = next(_l.getFeatures()).geometry()
                            break
                case _:
                    pass

            if not boundary_ref_geom:
                raise RuntimeError(
                    "XMAS_PLUGIN: could not get boundary reference geometry"
                )

            # loop over all vertices in current error geometry and find points outside reference geometry
            outside_vertex_geoms = []
            for _v in geom.vertices():
                point_geom = QgsGeometry.fromPointXY(QgsPointXY(_v))
                if point_geom.within(boundary_ref_geom) or point_geom.touches(
                    boundary_ref_geom
                ):
                    continue
                outside_vertex_geoms.append(point_geom)

            iface.mapCanvas().flashGeometries(
                [geom], crs=self.plan_crs, flashes=5, duration=500
            )
            iface.mapCanvas().flashGeometries(
                outside_vertex_geoms, crs=self.plan_crs, flashes=5, duration=500
            )

            # zoom to geometry
            _extent = geom.boundingBox()
            buffer_percentage = 0.4
            width = _extent.xMaximum() - _extent.xMinimum()
            height = _extent.yMaximum() - _extent.yMinimum()
            extent = QgsRectangle(
                _extent.xMinimum() - buffer_percentage * width,
                _extent.yMinimum() - buffer_percentage * height,
                _extent.xMaximum() + buffer_percentage * width,
                _extent.yMaximum() + buffer_percentage * height,
            )
            canvas = self.iface.mapCanvas()
            canvas.setExtent(extent)
            canvas.refresh()

        elif index.column() == 1:
            # highlight layer
            _select_layer(row_data["layer"])
        else:
            return
