from qgis.core import (
    QgsCoordinateTransform,
    QgsFeature,
    QgsFields,
    QgsGeometry,
    QgsMemoryProviderUtils,
    QgsPoint,
    QgsPointXY,
    QgsProject,
    QgsRasterLayer,
    QgsWkbTypes,
)
from qgis.PyQt import QtCore
from qgis.PyQt.QtCore import QModelIndex, QObject, QVariant
from qgis.PyQt.QtGui import QStandardItem, QStandardItemModel
from qgis.utils import iface
from xplordb.datamodel.collar import Collar

from openlog.toolbelt import PlgLogger


class CollarCreationLayerModel(QStandardItemModel):
    HOLE_ID_COL = 0
    X_COL = 1
    Y_COL = 2
    Z_COL = 3
    EOH_COL = 4

    def __init__(self, parent: QObject = None):
        """
        QStandardItemModel for collar layer creation.

        Use a temporary memory layer for collar position definition.

        Layer features are synchronized with model values

        Args:
            parent: QObject parent
        """
        super(CollarCreationLayerModel, self).__init__(parent)
        self.setHorizontalHeaderLabels(
            [
                self.tr("HoleID"),
                self.tr("Easting"),
                self.tr("Northing"),
                self.tr("Elevation"),
                self.tr("EOH"),
            ]
        )
        self.log = PlgLogger().log

        # Hole ID prefix and index start for automatic hole_id definition
        self._hole_id_prefix = "HOLE_"
        self._index_start = 0
        self._length_numbering = 1

        # Create temporary layer
        self._layer = QgsMemoryProviderUtils.createMemoryLayer(
            name=self.tr("Collar creation"),
            fields=QgsFields(),
            geometryType=QgsWkbTypes.PointZ,
            crs=iface.mapCanvas().mapSettings().destinationCrs(),
        )

        # Add to project : must be removed by widget using model before delete
        QgsProject().instance().addMapLayer(self._layer)

        # Start edition for immediate collar add
        self._layer.startEditing()
        iface.actionAddFeature().trigger()

        # Synchronize layer modification for model
        self._featureAdded_connect = lambda fid: self._feature_added(fid)
        self._layer.featureAdded.connect(self._featureAdded_connect)

        self._featuresDeleted_connect = lambda fids: self._features_deleted(fids)
        self._layer.featuresDeleted.connect(self._featuresDeleted_connect)

        self._geometryChanged_connect = lambda fid, geometry: self._geometry_changed(
            fid, geometry
        )
        self._layer.geometryChanged.connect(self._geometryChanged_connect)

        # Synchronize model change for layer
        self._itemChanged_connect = lambda item: self._item_changed(item)
        self.itemChanged.connect(self._itemChanged_connect)

        self._dtm_layer = QgsRasterLayer()

    def set_hole_id_prefix(self, hole_id_prefix: str) -> None:
        """
        Define hole_id prefix for automatic hole id definition

        Args:
            hole_id_prefix: hole id prefix
        """
        self._hole_id_prefix = hole_id_prefix
        self.dataChanged.emit(QModelIndex(), QModelIndex())

    def set_index_start(self, index_start: int) -> None:
        """
        Define index start for automatic hole id definition

        Args:
            index_start: index start
        """
        self._index_start = index_start
        self.dataChanged.emit(QModelIndex(), QModelIndex())

    def set_length_numbering(self, length_numbering: int) -> None:
        """
        Define length numbering for hole number definition

        Args:
            length_numbering: length numbering
        """
        self._length_numbering = length_numbering
        self.dataChanged.emit(QModelIndex(), QModelIndex())

    def remove_temp_layer(self) -> None:
        """
        Remove temp layer

        Must be called when model is not used anymore

        """
        if self._layer:
            #  Changes must be committed for no warning when removing layer
            self._layer.commitChanges(True)
            QgsProject().instance().removeMapLayer(self._layer)

    def get_collar_list(self) -> [Collar]:
        """
        Get collar list (warning no person and dataset are defined)

        Returns: collar list (warning no person and dataset are defined)

        """
        result = []
        for i in range(0, self.rowCount()):
            result.append(self._collar(i))
        return result

    def _item_changed(self, item: QStandardItem) -> None:
        """
        Function called when item is changed in model.

        Used to synchronize temporary memory layer (only x, y, z)

        Args:
            item: QStandardItem changed
        """
        col = item.index().column()
        if col == self.X_COL or col == self.Y_COL or col == self.Z_COL:
            row = item.index().row()
            collar = self._collar(row)
            fid = self.data(self.index(row, self.HOLE_ID_COL), QtCore.Qt.UserRole)
            self._update_layer_feature_geom(fid, collar)

    def _update_layer_feature_geom(self, fid: int, collar: Collar) -> None:
        """
        Update temporary layer geometry from collar definition

        Args:
            fid: id of feature
            collar: collar
        """
        # Layer must be editable or we won't be able to change geometry
        if not self._layer.isEditable():
            self._layer.startEditing()
        iface.setActiveLayer(self._layer)

        # Disconnect geometry changes signal to avoid infinite loop
        self._layer.geometryChanged.disconnect(self._geometryChanged_connect)
        self._layer.beginEditCommand(
            self.tr("Moved point from collar layer creation model")
        )
        self._layer.moveVertexV2(QgsPoint(x=collar.x, y=collar.y, z=collar.z), fid, 0)
        self._layer.endEditCommand()
        self._layer.triggerRepaint()

        # Restore synchronization for geometry change
        self._layer.geometryChanged.connect(self._geometryChanged_connect)

    def removeRow(self, row: int, parent: QModelIndex = QModelIndex()) -> bool:
        """
        Override default implementation of QStandardItemModel to remove feature from associated layer

        Args:
            row: (int) row to remove
            parent: (QModelIndex) parent

        Returns: True if row was removed, False otherwise

        """
        # Layer must be editable or we won't be able to change geometry
        if not self._layer.isEditable():
            self._layer.startEditing()
        iface.setActiveLayer(self._layer)

        fid = self.data(self.index(row, self.HOLE_ID_COL), QtCore.Qt.UserRole)

        # Disconnect geometry changes signal to avoid infinite loop
        self._layer.featuresDeleted.disconnect(self._featuresDeleted_connect)

        self._layer.beginEditCommand(
            self.tr("Deleted point from collar layer creation model")
        )
        self._layer.deleteFeature(fid)
        self._layer.endEditCommand()
        self._layer.triggerRepaint()

        # Restore synchronization for geometry delete
        self._layer.featuresDeleted.connect(self._featuresDeleted_connect)

        return super().removeRow(row, parent)

    def _collar(self, row: int) -> Collar:
        """
        Get collar definition for specific row

        Args:
            row: model row

        Returns: Collar for specific row

        """
        collar = Collar(
            hole_id=self.data(self.index(row, self.HOLE_ID_COL)),
            data_set="",  # Data set must be defined by widget using model
            loaded_by="",  # Loaded by person must be defined by widget using model
            x=self.data(self.index(row, self.X_COL)),
            y=self.data(self.index(row, self.Y_COL)),
            z=self.data(self.index(row, self.Z_COL)),
            srid=iface.mapCanvas().mapSettings().destinationCrs().postgisSrid(),
        )
        return collar

    def _features_deleted(self, fids: [int]) -> None:
        """
        Synchronize feature removal in temporary memory layer in model

        Args:
            fids: list of feature id removed
        """
        for i in reversed(range(0, self.rowCount())):
            fid = self.data(self.index(i, self.HOLE_ID_COL), QtCore.Qt.UserRole)
            if fid in fids:
                super().removeRow(i)

    def _geometry_changed(self, fid: int, geometry: QgsGeometry):
        """
        Synchronize feature geometry changes in temporary memory layer in model

        Args:
            fid: id of feature changed
            geometry: new feature geometry
        """

        # Update only if geometry is defined
        if geometry.constGet():
            for i in range(0, self.rowCount()):
                # Get feature id of row from UserRole
                fid_row = self.data(self.index(i, self.HOLE_ID_COL), QtCore.Qt.UserRole)
                if fid_row == fid:
                    # Disconnect item changes signal to avoid infinite loop
                    self.itemChanged.disconnect(self._itemChanged_connect)
                    # Update data with new geometry
                    self.setData(self.index(i, self.X_COL), geometry.constGet().x())
                    self.setData(self.index(i, self.Y_COL), geometry.constGet().y())
                    self.setData(self.index(i, self.Z_COL), geometry.constGet().z())
                    # Restore synchronization for item change
                    self.itemChanged.connect(self._itemChanged_connect)
                    break

    def _feature_added(self, fid: int):
        """
        Synchronize feature added in temporary memory layer in model

        Args:
            fid: new feature id
        """
        if self._layer:
            self._add_feature(self._layer.getFeature(fid))

    def _add_feature(self, feature: QgsFeature):
        """
        Add row in model from QgsFeature

        Args:
            feature: QgsFeature added
        """
        self.insertRow(self.rowCount())
        row = self.rowCount() - 1

        point = feature.geometry().asPoint()

        # Disconnect item changes signal to avoid infinite loop
        self.itemChanged.disconnect(self._itemChanged_connect)

        self.setData(
            self.index(row, self.HOLE_ID_COL), feature.id(), QtCore.Qt.UserRole
        )

        self.setData(self.index(row, self.X_COL), point.x())
        self.setData(self.index(row, self.Y_COL), point.y())

        # Get value from DTM here
        if self._dtm_layer and self._dtm_layer.isValid():
            z = self._get_z_value_from_dtm(point)
        else:
            z = 0.0

        self.setData(self.index(row, self.Z_COL), z)

        # Restore synchronization for item change
        self.itemChanged.connect(self._itemChanged_connect)

    def _get_z_value_from_dtm(self, point):
        tr = QgsCoordinateTransform(
            self._layer.crs(), self._dtm_layer.crs(), QgsProject.instance()
        )
        point_dtm = tr.transform(point)
        z_val, res = self._dtm_layer.dataProvider().sample(point_dtm, 1)
        if res:
            z = z_val
        else:
            self.log(
                self.tr(
                    "Can't define DTM value for point : {0}/{1}. z value used is 0.0."
                ).format(point.x(), point.y()),
                push=True,
            )
            z = 0.0
        return z

    def set_dtm_layer(self, layer: QgsRasterLayer) -> None:
        self._dtm_layer = layer

        if self._dtm_layer:
            # Disconnect item changes signal to avoid infinite loop
            self.itemChanged.disconnect(self._itemChanged_connect)
            for i in range(0, self.rowCount()):
                x = self.data(self.index(i, self.X_COL))
                y = self.data(self.index(i, self.Y_COL))
                point = QgsPointXY(x, y)
                z = self._get_z_value_from_dtm(point)
                self.setData(self.index(i, self.Z_COL), z)
            # Restore synchronization for item change
            self.itemChanged.connect(self._itemChanged_connect)

    def data(
        self, index: QtCore.QModelIndex, role: int = QtCore.Qt.DisplayRole
    ) -> QVariant:
        """
        Override QStandardItemModel data() for automatic hole_id definition from hole_id_prefix and index_start

        Args:
            index:
            role:

        Returns:

        """
        result = super().data(index, role)

        # Only if no value define and for DisplayRole
        if (
            not result
            and role == QtCore.Qt.DisplayRole
            and index.column() == self.HOLE_ID_COL
        ):
            row = index.row()
            result = self._hole_id_prefix + str(self._index_start + row).zfill(
                self._length_numbering
            )
        return result
