# 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

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

from qgis.core import (
    Qgis,
    QgsFeature,
    QgsGeometry,
    QgsProject,
    QgsRectangle,
    QgsSpatialIndex,
    QgsVectorLayer,
)
from qgis.gui import QgisInterface
from qgis.PyQt.QtCore import (
    QAbstractTableModel,
    QModelIndex,
    QSortFilterProxyModel,
    Qt,
    QVariant,
)
from qgis.PyQt.QtWidgets import QAbstractItemView, QAction, QMenu
from qgis.utils import iface

from xmas_plugin.topo_check import TopoTableView
from xmas_plugin.util.geom import transform_to_project_crs


class GapTableModel(QAbstractTableModel):
    def __init__(
        self, data: List[QgsGeometry], 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()
        # return f'Lücke {index.row()+1}: {self._data[index.row()].area():0.2f} m²'
        return f"{self._data[index.row()].area():0.2f} m²"

    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 GapTableView(TopoTableView):
    def __init__(self, iface: QgisInterface, parent=None):
        super().__init__(parent)
        self.iface = iface

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

        # strings used to construct and remove subset-filters on layers
        self.subset_slug = "topo-gap filter"
        self.subset_start_token = f"/* {self.subset_slug} start */"
        self.subset_end_token = f"/* {self.subset_slug} end */"

        self.model = GapTableModel([], ["Fläche"])
        self.proxymodel = QSortFilterProxyModel()
        self.proxymodel.setSourceModel(self.model)
        self.setModel(self.proxymodel)
        self.setSortingEnabled(True)

        self.setContextMenuPolicy(Qt.CustomContextMenu)
        self.customContextMenuRequested.connect(self.show_context_menu)
        self.proxymodel.modelReset.connect(self.resizeColumnsToContents)

    def show_context_menu(self, position):
        # Get cell under cursor (view coordinates)
        index = self.indexAt(position)
        if not index.isValid():
            return

        # Build menu
        menu = QMenu(self)
        menu.addAction(
            QAction("Zoom", self, triggered=lambda: self.zoom_gap_geom(index))
        )
        menu.addAction(
            QAction("Hervorheben", self, triggered=lambda: self.flash_gap_geom(index))
        )
        menu.addAction(
            QAction(
                "Features filtern",
                self,
                triggered=lambda: self.filter_gap_features(index),
            )
        )
        menu.addAction(
            QAction(
                "Ebenen aktivieren",
                self,
                triggered=lambda: self.activate_gap_layers(index),
            )
        )
        menu.addAction(
            QAction(
                "Features selektieren",
                self,
                triggered=lambda: self.select_gap_features(index),
            )
        )

        # Show at cursor position (map to global screen coordinates)
        menu.exec_(self.viewport().mapToGlobal(position))
        return

    def flash_gap_geom(self, index: QModelIndex):
        geom = self.get_geom_from_index(index)
        iface.mapCanvas().flashGeometries(
            [geom], crs=self.plan_crs, flashes=5, duration=500
        )
        iface.mapCanvas().flashGeometries(
            [QgsGeometry(x) for x in geom.vertices()],
            crs=self.plan_crs,
            flashes=5,
            duration=500,
        )

    def zoom_gap_geom(self, index: QModelIndex):
        geom = self.get_geom_from_index(index)
        _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()
        return

    def select_gap_features(self, index: QModelIndex):
        geom = self.get_geom_from_index(index)
        touching_features = self.get_gap_touching_features(list(geom.vertices()))

        # clear all selections
        for layer in iface.mapCanvas().layers():
            if hasattr(layer, "removeSelection"):
                layer.removeSelection()

        # add touching features to selection
        for _feature, _layer in touching_features:
            _layer.selectByIds([_feature.id()], Qgis.SelectBehavior.AddToSelection)

    def filter_gap_features(self, index: QModelIndex):
        if not self.plan_id:
            return

        geom = self.get_geom_from_index(index)
        touching_features = self.get_gap_touching_features(list(geom.vertices()))

        # transpose touching_features to layer-dict
        feature_id_dict: Dict[QgsVectorLayer, List[str]] = {}
        for _f, _l in touching_features:
            feature_id_dict.setdefault(_l, []).append(_f.attribute("id"))

        # remove all filters
        self.clear_subset_filters()

        # set substring filter for gap-layers
        for _l, _ids in feature_id_dict.items():
            quoted_ids = [f"'{x}'" for x in _ids]
            _ids_to_str = (
                f"({','.join(quoted_ids)})"  # convert tuple to sql list # noqa
            )
            filter_str = f' {self.subset_start_token} AND "id" IN {_ids_to_str} {self.subset_end_token}'
            current_subsetstr = _l.dataProvider().subsetString()
            if self.subset_slug in current_subsetstr:
                current_subsetstr = re.sub(
                    pattern=rf" {re.escape(self.subset_start_token)}.*?"
                    rf"{re.escape(self.subset_end_token)}",
                    repl="",
                    string=current_subsetstr,
                    flags=re.DOTALL,
                )
            _l.dataProvider().setSubsetString(f"{current_subsetstr}{filter_str}", True)
            _l.triggerRepaint()
            iface.layerTreeView().refreshLayerSymbology(_l.id())

        # show touching layers and hide all other layers
        gap_feature_layers = feature_id_dict.keys()
        for layer in QgsProject.instance().mapLayers().values():
            if isinstance(layer, QgsVectorLayer):
                if (
                    layer.customProperties().value("xmas_plugin/plan_id")
                    == self.plan_id
                ):
                    if layer in gap_feature_layers:
                        QgsProject.instance().layerTreeRoot().findLayer(
                            layer.id()
                        ).setItemVisibilityChecked(True)
                    else:
                        QgsProject.instance().layerTreeRoot().findLayer(
                            layer.id()
                        ).setItemVisibilityChecked(False)

        iface.mapCanvas().refresh()

    def activate_gap_layers(self, index: QModelIndex):
        """
        make layers that contain touching features editable and hide all other layers
        :param index:
        :return:
        """
        if not self.plan_id:
            return
        geom = self.get_geom_from_index(index)
        touching_features = self.get_gap_touching_features(list(geom.vertices()))
        touching_layers: Set[QgsVectorLayer] = set([x[1] for x in touching_features])

        view = iface.layerTreeView()
        view.clearSelection()

        # activate gap layers and turn other layers off
        for layer in QgsProject.instance().mapLayers().values():
            if isinstance(layer, QgsVectorLayer):
                if (
                    layer.customProperties().value("xmas_plugin/plan_id")
                    == self.plan_id
                ):
                    if layer in touching_layers:
                        QgsProject.instance().layerTreeRoot().findLayer(
                            layer.id()
                        ).setItemVisibilityChecked(True)
                    else:
                        QgsProject.instance().layerTreeRoot().findLayer(
                            layer.id()
                        ).setItemVisibilityChecked(False)

        # make layers editable
        for layer in QgsProject.instance().mapLayers().values():
            if isinstance(layer, QgsVectorLayer):
                if (
                    layer.customProperties().value("xmas_plugin/plan_id")
                    == self.plan_id
                ):
                    if layer in touching_layers:
                        layer.startEditing()

        # select gap layers in layer tree view
        view.setSelectionMode(QAbstractItemView.MultiSelection)

        for layer in touching_layers:
            if not layer:
                continue
            view.setCurrentLayer(layer)
        view.setSelectionMode(QAbstractItemView.ExtendedSelection)

    def clear_subset_filters(self) -> None:
        # no current plan available -> remove subset filters for all plans/layers in project
        if not self.plan_id:
            for layer in QgsProject.instance().mapLayers().values():
                if isinstance(layer, QgsVectorLayer):
                    if layer.customProperties().contains("xmas_plugin/plan_id"):
                        current_subsetstr = layer.dataProvider().subsetString()
                        if self.subset_slug in current_subsetstr:
                            unfiltered_subsetstr = re.sub(
                                pattern=rf" {re.escape(self.subset_start_token)}.*?"
                                rf"{re.escape(self.subset_end_token)}",
                                repl="",
                                string=current_subsetstr,
                                flags=re.DOTALL,
                            )
                            layer.dataProvider().setSubsetString(
                                unfiltered_subsetstr, True
                            )
                            layer.triggerRepaint()
            return

        # current plan available -> remove subset filters for current plan layers only
        for layer in QgsProject.instance().mapLayers().values():
            if isinstance(layer, QgsVectorLayer):
                if (
                    layer.customProperties().value("xmas_plugin/plan_id")
                    == self.plan_id
                ):
                    current_subsetstr = layer.dataProvider().subsetString()
                    if self.subset_slug in current_subsetstr:
                        unfiltered_subsetstr = re.sub(
                            pattern=rf" {re.escape(self.subset_start_token)}.*?"
                            rf"{re.escape(self.subset_end_token)}",
                            repl="",
                            string=current_subsetstr,
                            flags=re.DOTALL,
                        )
                        layer.dataProvider().setSubsetString(unfiltered_subsetstr, True)
                        layer.triggerRepaint()

        # show all layers of plan
        for layer in QgsProject.instance().mapLayers().values():
            if layer.customProperties().value("xmas_plugin/plan_id") == self.plan_id:
                layer_node = QgsProject.instance().layerTreeRoot().findLayer(layer.id())
                if layer_node:
                    layer_node.setItemVisibilityChecked(True)

        # clear layer tree selection
        view = iface.layerTreeView()
        view.clearSelection()

    def get_geom_from_index(self, index: QModelIndex) -> QgsGeometry:
        src_index: QModelIndex = index.model().mapToSource(
            index
        )  # translate proxymodel-index to underlying model
        _geom: QgsGeometry = self.model.data(src_index, Qt.UserRole)
        geom = transform_to_project_crs(
            _geom, self.plan_crs
        )  # creates a copy of original geometry
        return geom

    def get_gap_touching_features(self, gap_vertices):
        """
        get all features that share a vertex with a gap geometry
        :param gap_features:
        :return:
        """

        if not gap_vertices or not self.gap_features:
            return []

        point_index = QgsSpatialIndex()
        features = []
        for i, p in enumerate(gap_vertices):
            _feat = QgsFeature(i)
            _feat.setGeometry(QgsGeometry(p))
            features.append(_feat)
        point_index.addFeatures(features)

        results = []

        for feat, layer in self.gap_features:
            if not feat or not feat.hasGeometry():
                continue

            feat_geom = feat.geometry()
            feat_bbox = feat_geom.boundingBox()

            candidate_point_ids = point_index.intersects(feat_bbox)

            if not candidate_point_ids:
                continue

            for point_id in candidate_point_ids:
                vertex = gap_vertices[point_id]
                if vertex in feat.geometry().vertices():
                    results.append((feat, layer))
                    break

        # add plan/sections layers
        boundary_features = self.get_plan_and_sections()
        for _f, _l in boundary_features:
            if point_index.intersects(_f.geometry().constGet().boundingBox()):
                results.append((_f, _l))

        return results

    def get_plan_and_sections(self) -> List[Tuple[QgsFeature, QgsVectorLayer]]:
        res: List[Tuple[QgsFeature, QgsVectorLayer]] = []
        for layer in QgsProject.instance().mapLayers().values():
            if layer.customProperties().value(
                "xmas_plugin/plan_id"
            ) == self.plan_id and layer.customProperties().value(
                "xmas_plugin/layer_type"
            ) in ["plan", "section"]:
                for _f in layer.getFeatures():
                    res.append((_f, layer))
        return res

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