# 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, List, Optional, Tuple

from qgis.core import (
    Qgis,
    QgsGeometry,
    QgsProject,
    QgsRectangle,
    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 InvalidsTableModel(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:
        # TODO: show warning when gap geometry has curved segments
        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²'
        if self._columns[index.column()] == "id":
            return self._data[index.row()]["xplan_id"]
        elif self._columns[index.column()] == "Layer":
            return self._data[index.row()]["layer_name"]
        elif self._columns[index.column()] == "Fehler":
            errs = ",".join(set([e["what"] for e in self._data[index.row()]["errors"]]))
            return errs

    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 InvalidsTableView(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]] = []

        self.columns = ["id", "Layer", "Fehler"]
        self.model = InvalidsTableModel([], self.columns)
        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.clicked.connect(self.on_click)
        self.proxymodel.modelReset.connect(self.resizeColumnsToContents)

    def on_click(self, index: QModelIndex) -> None:
        if self.columns[index.column()] == "id":
            self.flash_geom(index, "geom")
        elif self.columns[index.column()] == "Layer":
            self.activate_layer(index, select_only=True)
        elif self.columns[index.column()] == "Fehler":
            self.flash_geom(index, mode="errors")

    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_geom(index)))
        menu.addAction(
            QAction("Hervorheben", self, triggered=lambda: self.flash_geom(index))
        )
        menu.addAction(
            QAction(
                "Feature filtern",
                self,
                triggered=lambda: self.filter_feature(index),
            )
        )
        menu.addAction(
            QAction(
                "Ebene aktivieren",
                self,
                triggered=lambda: self.activate_layer(index),
            )
        )
        menu.addAction(
            QAction(
                "Feature selektieren",
                self,
                triggered=lambda: self.select_feature(index),
            )
        )

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

    def zoom_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 flash_geom(self, index: QModelIndex, mode: str = "all"):
        data = index.model().data(index, Qt.UserRole)
        geom = self.get_geom_from_index(index)

        if mode in ["all", "geom"]:
            iface.mapCanvas().flashGeometries(
                [geom], crs=self.plan_crs, flashes=5, duration=500
            )
        if mode in ["all", "errors"]:
            # get errors where values
            error_geoms = [QgsGeometry.fromWkt(e["where"]) for e in data["errors"]]
            iface.mapCanvas().flashGeometries(
                [g for g in error_geoms],
                crs=self.plan_crs,
                flashes=5,
                duration=500,
            )

    def filter_feature(self, index: QModelIndex):
        if not self.plan_id:
            return
        data = index.model().data(index, Qt.UserRole)

        layer = QgsProject.instance().mapLayer(data["layer_id"])
        filter_str = f" {self.subset_start_token} AND \"id\" = '{data['xplan_id']}' {self.subset_end_token}"
        current_subsetstr = layer.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,
            )
        layer.dataProvider().setSubsetString(f"{current_subsetstr}{filter_str}", True)
        # NOTE: this won't hide features that have changed geoemtrie in edit buffer!
        #       possible workaround would be changing the layer style rules
        layer.triggerRepaint()
        iface.layerTreeView().refreshLayerSymbology(layer.id())

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

        iface.mapCanvas().refresh()

    def activate_layer(self, index: QModelIndex, select_only: bool = False):
        if not self.plan_id:
            return
        data = index.model().data(index, Qt.UserRole)
        cur_layer = QgsProject.instance().mapLayer(data["layer_id"])

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

        # activate layer 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 == cur_layer:
                        QgsProject.instance().layerTreeRoot().findLayer(
                            layer.id()
                        ).setItemVisibilityChecked(True)
                    else:
                        if not select_only:
                            QgsProject.instance().layerTreeRoot().findLayer(
                                layer.id()
                            ).setItemVisibilityChecked(False)

        # make layer 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 == cur_layer:
                        layer.startEditing()

        # select layer in layer tree view
        view.setSelectionMode(QAbstractItemView.MultiSelection)
        view.setCurrentLayer(cur_layer)
        view.setSelectionMode(QAbstractItemView.ExtendedSelection)

    def select_feature(self, index: QModelIndex):
        data = index.model().data(index, Qt.UserRole)
        cur_layer = QgsProject.instance().mapLayer(data["layer_id"])

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

        cur_layer.selectByIds([data["qgis_fid"]], Qgis.SelectBehavior.AddToSelection)

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

    def get_geom_from_index(self, index: QModelIndex) -> QgsGeometry:
        data = index.model().data(index, Qt.UserRole)
        layer = QgsProject.instance().mapLayer(data["layer_id"])
        _geom = layer.getGeometry(data["qgis_fid"])
        geom = transform_to_project_crs(
            _geom, self.plan_crs
        )  # creates a copy of original geometry
        return geom
