# 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 logging
import re
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.PyQt.QtWidgets import QAction, QMenu
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

logger = logging.getLogger(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:
                ret_val = str(self._data[index.row()]["feature"].attribute("id"))
                if (
                    self._data[index.row()]["feature"]
                    .geometry()
                    .constGet()
                    .hasCurvedSegments()
                ):
                    ret_val = f"⚠∿ {ret_val}"  # TODO: also check if in boundary violation geom (plan/section) has curved segments
                return ret_val
            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:
            logger.debug("topocheck - table bounds: {e}, probably a layer was 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)

        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(
                "Features filtern",
                self,
                triggered=lambda: self.filter_overlap_features(index),
            )
        )

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

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

        # get feature data
        data = index.model().data(index, Qt.UserRole)
        feature = data["feature"]
        layer = data["layer"]
        type = data["Typ"]
        violated_layer: QgsVectorLayer | None = None

        # get plan or sectino layer:
        groups = get_layer_group_hierarchy(layer)
        for lt_layer in groups[0].findLayers():
            _layer = lt_layer.layer()
            if (
                type == "Bereich"
                and _layer.customProperties().value(f"{PLUGIN_DIR_NAME}/layer_type")
                == "section"
            ):
                violated_layer = _layer
                break
            if (
                type == "Plan"
                and _layer.customProperties().value(f"{PLUGIN_DIR_NAME}/layer_type")
                == "plan"
            ):
                violated_layer = _layer
                break

        # remove all filters
        self.clear_subset_filters()

        # set substring filter for gap-layers
        filter_str = f" {self.subset_start_token} AND \"id\" = '{feature.attribute('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)
        layer.triggerRepaint()
        iface.layerTreeView().refreshLayerSymbology(layer.id())

        # show overlaping layers and hide all other layers
        boundary_layers = [layer, violated_layer]
        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 boundary_layers:
                        QgsProject.instance().layerTreeRoot().findLayer(
                            layer.id()
                        ).setItemVisibilityChecked(True)
                    else:
                        QgsProject.instance().layerTreeRoot().findLayer(
                            layer.id()
                        ).setItemVisibilityChecked(False)

        iface.mapCanvas().refresh()

    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"])
        elif index.column() == 2:
            # activate violated boundary layer
            violation_type = row_data["Typ"]
            cur_layer: QgsVectorLayer = row_data["layer"]
            layer_groups = get_layer_group_hierarchy(cur_layer)
            for l_tl in layer_groups[0].findLayers():
                cur_layer = l_tl.layer()
                xmas_layer_type = cur_layer.customProperties().value(
                    f"{PLUGIN_DIR_NAME}/layer_type"
                )
                if violation_type == "Plan" and xmas_layer_type == "plan":
                    _select_layer(cur_layer)
                    break
                elif violation_type == "Bereich" and xmas_layer_type == "section":
                    _select_layer(cur_layer)
                    break
        else:
            return
