# -*- coding: utf-8 -*-
"""
Click-Classify Map Tool
=======================
QgsMapTool that intercepts canvas clicks, identifies the nearest
feature on the active layer, cycles its value through the configured
classification list, writes the value to the field, and refreshes
the layer renderer so the map updates immediately.

Author  : Loc Vu Trung / LVT Center
License : GPL-3.0
"""

from qgis.PyQt.QtCore import Qt
from qgis.PyQt.QtGui import QCursor
from qgis.core import (
    QgsCoordinateTransform,
    QgsFeatureRequest,
    QgsGeometry,
    QgsPointXY,
    QgsProject,
    QgsRectangle,
    QgsWkbTypes,
)
from qgis.gui import QgsMapTool, QgsMapMouseEvent

# pyqtSignal lives in PyQt5 or PyQt6
try:
    from qgis.PyQt.QtCore import pyqtSignal
except ImportError:
    from qgis.PyQt.QtCore import pyqtSignal


class ClickClassifyTool(QgsMapTool):
    """Map tool for click-based feature classification.

    Signals:
        classified(layer_id, fid, new_value, label):
            Emitted after each successful classification write.
        status_changed(message):
            Emitted with a status string for display in the dialog.
    """

    classified = pyqtSignal(str, int, object, str)   # layer_id, fid, value, label
    status_changed = pyqtSignal(str)

    def __init__(self, canvas, config):
        """Initialise the tool.

        Args:
            canvas: QgsMapCanvas instance.
            config (dict): Runtime configuration dict from the dialog:
                {
                  'layer':     QgsVectorLayer,
                  'field':     field_name (str),
                  'label_field': label_field_name or '' (str),
                  'steps':     [
                      {'value': 0, 'label': '', 'color': '#aaaaaa'},
                      {'value': 1, 'label': 'Good', 'color': '#00aa00'},
                      ...
                  ],
                  'auto_save': bool,
                }
        """
        super().__init__(canvas)
        self.config = config
        self.setCursor(QCursor(Qt.CursorShape.CrossCursor))

    # ------------------------------------------------------------------
    # QgsMapTool interface
    # ------------------------------------------------------------------

    def canvasReleaseEvent(self, event: QgsMapMouseEvent):
        """Handle left-click: classify the nearest feature."""
        if event.button() != Qt.MouseButton.LeftButton:
            return

        layer = self.config.get('layer')
        if not layer or not layer.isValid():
            self.status_changed.emit('No valid layer configured.')
            return

        # Locate the nearest feature under the cursor
        fid = self._find_feature(event.mapPoint(), layer)
        if fid is None:
            self.status_changed.emit('No feature found at click location.')
            return

        # Determine next value in the cycle
        field_name = self.config.get('field', '')
        steps = self.config.get('steps', [])
        if not steps or not field_name:
            return

        field_idx = layer.fields().indexFromName(field_name)
        if field_idx < 0:
            self.status_changed.emit(f'Field "{field_name}" not found in layer.')
            return

        # Read current value
        feat = next(layer.getFeatures(QgsFeatureRequest(fid)), None)
        if feat is None:
            return
        current_val = feat[field_name]

        # Find current step index and advance
        step_values = [s['value'] for s in steps]
        try:
            cur_idx = step_values.index(current_val)
        except ValueError:
            cur_idx = -1
        next_idx = (cur_idx + 1) % len(steps)
        next_step = steps[next_idx]

        # Write to layer
        if not layer.isEditable():
            layer.startEditing()

        layer.changeAttributeValue(fid, field_idx, next_step['value'])

        # Write label field if configured
        label_field = self.config.get('label_field', '')
        if label_field:
            lf_idx = layer.fields().indexFromName(label_field)
            if lf_idx >= 0:
                layer.changeAttributeValue(fid, lf_idx, next_step['label'])

        if self.config.get('auto_save', True):
            layer.commitChanges()
            layer.startEditing()

        layer.triggerRepaint()
        self.classified.emit(
            layer.id(), fid, next_step['value'], next_step['label']
        )
        self.status_changed.emit(
            f'Feature {fid} → {next_step["label"] or next_step["value"]}'
        )

    # ------------------------------------------------------------------
    # Internal helpers
    # ------------------------------------------------------------------

    def _find_feature(self, map_point: QgsPointXY, layer):
        """Return the FID of the nearest/containing feature, or None.

        Properly handles CRS differences between the map canvas and the
        vector layer.  Polygon layers use a spatial containment check so
        that any click inside a polygon body is detected regardless of
        how small the vertex-distance tolerance is.
        """
        canvas       = self.canvas()
        map_settings = canvas.mapSettings()
        canvas_crs   = map_settings.destinationCrs()
        layer_crs    = layer.crs()

        # ── 1. Transform click point → layer CRS ─────────────────────
        if canvas_crs != layer_crs:
            ct = QgsCoordinateTransform(
                canvas_crs, layer_crs, QgsProject.instance()
            )
            try:
                layer_point = ct.transform(map_point)
            except Exception:
                layer_point = map_point
        else:
            ct = None
            layer_point = map_point

        click_geom = QgsGeometry.fromPointXY(layer_point)

        # ── 2. Determine a search envelope in layer CRS ───────────────
        # Use 1 % of the visible extent (in layer CRS) as search radius.
        canvas_extent = canvas.extent()
        if ct is not None:
            try:
                layer_extent = ct.transformBoundingBox(canvas_extent)
            except Exception:
                layer_extent = canvas_extent
        else:
            layer_extent = canvas_extent

        search_radius = max(layer_extent.width(), layer_extent.height()) * 0.01
        search_rect   = QgsRectangle.fromCenterAndSize(
            layer_point, search_radius * 2, search_radius * 2
        )

        request = QgsFeatureRequest().setFilterRect(search_rect)
        request.setNoAttributes()

        # ── 3. Polygon layer: containment first, then proximity ───────
        try:
            geom_type = layer.geometryType()
            is_polygon = geom_type in (
                QgsWkbTypes.GeometryType.PolygonGeometry,
                QgsWkbTypes.PolygonGeometry,   # older QGIS enum path
            )
        except AttributeError:
            is_polygon = False

        if is_polygon:
            # Primary pass: exact containment
            for feat in layer.getFeatures(request):
                geom = feat.geometry()
                if not geom.isNull() and geom.contains(click_geom):
                    return feat.id()

            # Secondary pass: nearest edge within 0.3 % of view
            tol = max(layer_extent.width(), layer_extent.height()) * 0.003
            best_fid  = None
            best_dist = float('inf')
            for feat in layer.getFeatures(request):
                geom = feat.geometry()
                if geom.isNull():
                    continue
                dist = geom.distance(click_geom)
                if dist < best_dist and dist <= tol:
                    best_dist = dist
                    best_fid  = feat.id()
            return best_fid

        # ── 4. Point / Line layer: nearest within search radius ───────
        best_fid  = None
        best_dist = float('inf')
        for feat in layer.getFeatures(request):
            geom = feat.geometry()
            if geom.isNull():
                continue
            dist = geom.distance(click_geom)
            if dist < best_dist:
                best_dist = dist
                best_fid  = feat.id()

        return best_fid


    def deactivate(self):
        """Called when the tool is deactivated."""
        super().deactivate()
