import os

import numpy as np
from qgis.core import (
    QgsApplication,
    QgsCoordinateReferenceSystem,
    QgsCoordinateTransform,
    QgsExpression,
    QgsExpressionContext,
    QgsFeature,
    QgsField,
    QgsFields,
    QgsGeometry,
    QgsMapLayer,
    QgsMemoryProviderUtils,
    QgsPointXY,
    QgsProject,
    QgsVectorLayer,
    QgsWkbTypes,
)
from qgis.gui import QgsMapToolEmitPoint
from qgis.PyQt import QtGui, uic
from qgis.PyQt.QtCore import QVariant
from qgis.PyQt.QtWidgets import QDialog, QHeaderView, QShortcut
from qgis.utils import iface

from openlog.core.affine_projection import create_derived_projection_wkt
from openlog.datamodel.connection.openlog_connection import OpenLogConnection
from openlog.gui.local_grid.local_grid_layer_model import LocalGridLayerModel


class LocalGridCreationDialog(QDialog):
    def __init__(self, openlog_connection: OpenLogConnection, parent=None) -> None:
        """
        QDialog to create collar

        Args:
            openlog_connection: OpenLogConnection interface for data import
            parent: QWidget parent
        """
        super(LocalGridCreationDialog, self).__init__(parent)
        uic.loadUi(
            os.path.join(os.path.dirname(__file__), "local_grid_creation_dialog.ui"),
            self,
        )

        self._openlog_connection = openlog_connection

        # translation

        self.setWindowTitle("Local grid creation")

        # Create temporary layer for origin display
        fields = QgsFields()
        fields.append(QgsField("color", QVariant.String))
        fields.append(QgsField("name", QVariant.String))
        self._layer = QgsMemoryProviderUtils.createMemoryLayer(
            name="Local grid origin point",
            fields=fields,
            geometryType=QgsWkbTypes.LineString,
            crs=iface.mapCanvas().mapSettings().destinationCrs(),
        )

        current_dir = os.path.dirname(os.path.realpath(__file__))
        self._layer.loadNamedStyle(current_dir + "/origin_layer_style.qml")

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

        self._model = (
            LocalGridLayerModel()
        )  # No parent so we can remove layer when widget is deleted
        self.local_grid_table_view.setModel(self._model)
        self.local_grid_table_view.horizontalHeader().setSectionResizeMode(
            QHeaderView.Stretch
        )

        # Shortcut for point remove
        self.shortcut_close = QShortcut(QtGui.QKeySequence("Del"), self)
        self.shortcut_close.activated.connect(self._shortcut_del)

        # Define crs from reference point model layer
        crs = self._model._layer.crs()
        self.crs_selection_widget.setCrs(crs)
        self.crs_selection_widget.crsChanged.connect(self._crs_updated)

        # Origin position definition signals update
        self.x_origin_spinbox.valueChanged.connect(self._update_origin_point_layer_val)
        self.y_origin_spinbox.valueChanged.connect(self._update_origin_point_layer_val)
        self.rotation_spinbox.valueChanged.connect(self._update_origin_point_layer_val)
        self.x_scale_spinbox.valueChanged.connect(self._update_origin_point_layer_val)
        self.invert_x_checkbox.stateChanged.connect(self._update_origin_point_layer_val)
        self.y_scale_spinbox.valueChanged.connect(self._update_origin_point_layer_val)
        self.invert_y_checkbox.stateChanged.connect(self._update_origin_point_layer_val)

        # Map tool to select origin position
        self.map_tool = QgsMapToolEmitPoint(iface.mapCanvas())
        self.map_tool.canvasClicked.connect(self._canvas_clicked)

        # Wkt and projection creation
        self.create_wkt_button.clicked.connect(self._create_wkt)
        self.add_projection_button.clicked.connect(self._add_projection)

        self.reference_point_button.setChecked(True)
        self.reference_point_button.clicked.connect(self._method_changed)
        self.origin_direction_button.clicked.connect(self._method_changed)

        self.add_projection_button.setEnabled(False)
        self._crs_updated()
        self._method_changed()

    def get_derived_crs(self) -> QgsCoordinateReferenceSystem:
        """
        Get derived crs from current wkt string

        Returns: (QgsCoordinateReferenceSystem) crs from wkt string

        """
        crs = QgsCoordinateReferenceSystem()
        crs.createFromWkt(self.result_wkt.toPlainText())
        return crs

    def remove_temp_layer(self) -> None:
        """
        Remove temporary layers

        Must be called when widget 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)

        self._model.remove_temp_layer()

    def _crs_updated(self) -> None:
        """
        Slot to update temporarary layer and origin crs from crs selected by user

        """
        crs = self.crs_selection_widget.crs()

        # Affine transformation available only for projected crs
        if crs.isGeographic():
            self.create_wkt_button.setEnabled(False)
            self.create_wkt_button.setToolTip(
                "You must choose a PROJECTED CRS as the base (ie not latitude, longitude)"
            )
        else:
            self.create_wkt_button.setEnabled(True)
            self.create_wkt_button.setToolTip("")

        # Update origin geometry
        geom = self._get_origin_geometry()
        geom.transform(
            QgsCoordinateTransform(self._layer.crs(), crs, QgsProject.instance())
        )

        self.x_origin_spinbox.blockSignals(True)
        self.y_origin_spinbox.blockSignals(True)
        self.x_origin_spinbox.setValue(geom.asPoint().x())
        self.y_origin_spinbox.setValue(geom.asPoint().y())
        self.x_origin_spinbox.blockSignals(False)
        self.y_origin_spinbox.blockSignals(False)

        # Reproject temporary layers
        self._reproject_layer(crs, self._model._layer)
        self._reproject_layer(crs, self._layer)

    def _shortcut_del(self) -> None:
        """
        Remove selected points from table

        """
        # selected cell
        while self.local_grid_table_view.selectionModel().selectedIndexes():
            self._model.removeRow(
                self.local_grid_table_view.selectionModel().selectedIndexes()[0].row()
            )

    def _create_wkt(self) -> None:
        """
        Create wtk string and check if projection is valid to enable add button

        """
        wkt = self._compute_wkt()
        self.result_wkt.setText(wkt)

        crs = self.get_derived_crs()
        valid = crs.isValid()
        self.add_projection_button.setEnabled(valid)
        self.add_projection_button.setToolTip("" if valid else "Invalid WKT")

    def _compute_wkt(self) -> str:
        """
        Compute wkt string from current source and destination array and selected base CRS

        Returns: (str) computed wkt string

        """
        dst, src = self._get_src_dst_arrays()
        base_crs = self.crs_selection_widget.crs()
        wkt = create_derived_projection_wkt(
            self.projection_name_edit.text(), base_crs, src, dst
        )
        crs = QgsCoordinateReferenceSystem()
        crs.createFromWkt(wkt)
        if crs.isValid():
            wkt = crs.toWkt(QgsCoordinateReferenceSystem.WKT_PREFERRED, True)
        return wkt

    def _get_origin_geometry(self) -> QgsGeometry:
        """
        Returns origin geometry

        Returns: (QgsGeometry) origin geometry

        """
        return QgsGeometry.fromPointXY(
            QgsPointXY(self.x_origin_spinbox.value(), self.y_origin_spinbox.value())
        )

    def _get_src_dst_arrays(self) -> (np.array, np.array):
        """
        Get source and destination numpy.array for affine transformation calculation.

        For reference points, use data from model.

        For origin and rotation, create array from projected point.

        Returns: (np.array, np.array) source,destination numpy.array

        """
        if self.reference_point_button.isChecked():
            src = self._model.get_src_array()
            dst = self._model.get_dst_array()
        else:
            feature = QgsFeature()
            feature.setGeometry(self._get_origin_geometry())
            length = 1000.0

            # Create reference point from origin
            north_arrow = self._create_projected_point(
                feature, length, self.rotation_spinbox.value()
            )
            east_arrow = self._create_projected_point(
                feature, length, self.rotation_spinbox.value() + 90.0
            )

            src = np.array(
                [
                    [self.x_origin_spinbox.value(), self.y_origin_spinbox.value(), 0],
                    [north_arrow.x(), north_arrow.y(), 0],
                    [east_arrow.x(), east_arrow.y(), 0],
                ]
            )

            # Create destination point with scaling and inversion
            y_factor = -1.0 if self.invert_y_checkbox.isChecked() else 1.0
            x_factor = -1.0 if self.invert_x_checkbox.isChecked() else 1.0
            dst = np.array(
                [
                    [0, 0, 0],
                    [0, y_factor * length * self.y_scale_spinbox.value(), 0],
                    [x_factor * length * self.x_scale_spinbox.value(), 0, 0],
                ]
            )
        return dst, src

    def _add_projection(self) -> None:
        """
        Add derived crs to user crs

        """
        crs = self.get_derived_crs()
        if crs.isValid():
            QgsApplication.coordinateReferenceSystemRegistry().addUserCrs(
                crs,
                self.projection_name_edit.text(),
                QgsCoordinateReferenceSystem.FormatWkt,
            )

    def _method_changed(self) -> None:
        """
        Change displayed stack page in case of definition method update.

        Also set temporary layers used as visible/invisible

        """
        if self.origin_direction_button.isChecked():
            iface.mapCanvas().setMapTool(self.map_tool)
            self.stackedWidget.setCurrentWidget(self.origin_direction_page)
            self._set_layer_visibility(self._model._layer, False)
            self._set_layer_hidden(self._model._layer, True)
            self._set_layer_visibility(self._layer, True)
        else:
            iface.mapCanvas().unsetMapTool(self.map_tool)
            iface.actionAddFeature().trigger()
            self.stackedWidget.setCurrentWidget(self.reference_points_page)
            self._set_layer_visibility(self._model._layer, True)
            self._set_layer_hidden(self._model._layer, False)
            self._set_layer_visibility(self._layer, False)

    @staticmethod
    def _set_layer_hidden(layer: QgsMapLayer, hidden: bool) -> None:
        model = iface.layerTreeView().layerTreeModel()
        ltv = iface.layerTreeView()
        root = QgsProject.instance().layerTreeRoot()
        node = root.findLayer(layer)
        index = model.node2index(node)

        ltv.setRowHidden(index.row(), index.parent(), hidden)
        node.setCustomProperty("nodeHidden", hidden)

    @staticmethod
    def _set_layer_visibility(layer: QgsMapLayer, visible: bool) -> None:
        root = QgsProject.instance().layerTreeRoot()
        node = root.findLayer(layer)
        node.setItemVisibilityChecked(visible)

    def _update_origin_point_layer_val(self) -> None:
        self._update_origin_point_layer(
            self.x_origin_spinbox.value(),
            self.y_origin_spinbox.value(),
            self.rotation_spinbox.value(),
            self.x_scale_spinbox.value(),
            self.invert_x_checkbox.isChecked(),
            self.y_scale_spinbox.value(),
            self.invert_y_checkbox.isChecked(),
        )

    def _update_origin_point_layer(
        self,
        x: float,
        y: float,
        rotation: float,
        x_scale: float = 1.0,
        invert_x: bool = False,
        y_scale: float = 1.0,
        invert_y: bool = False,
    ) -> None:
        # self._layer.dataProvider().truncate()
        for feat in self._layer.getFeatures():
            self._layer.deleteFeature(feat.id())

        feature = QgsFeature()
        feature.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(x, y)))

        length = 1000.0
        self._layer.addFeatures(
            [
                self._create_projected_line(
                    feature,
                    length * y_scale,
                    rotation,
                    invert_y,
                    "green",
                    "North",
                ),
                self._create_projected_line(
                    feature,
                    length * x_scale,
                    rotation + 90.0,
                    invert_x,
                    "red",
                    "East",
                ),
            ]
        )

        self._layer.triggerRepaint()

    def _create_projected_point(
        self, feature: QgsFeature, length: float, rotation: float
    ) -> QgsPointXY:
        expression_str = f"""project ($geometry,
                             {length},
                              radians({rotation})
                              )
                           """
        context = QgsExpressionContext()
        expression = QgsExpression(expression_str)
        context.setFeature(feature)
        value = expression.evaluate(context)
        return value.asPoint()

    def _create_projected_line(
        self,
        feature: QgsFeature,
        length: float,
        rotation: float,
        invert: bool,
        color: str,
        name: str,
    ) -> QgsFeature:
        val_proj = -length if invert else length
        expression_str = f"""extend(
                                    make_line(
                                    $geometry,
                                     project ($geometry,
                                     {val_proj},
                                      radians({rotation})
                                      )
                                    ),
                                   0,
                                   0
                                )"""
        context = QgsExpressionContext()
        expression = QgsExpression(expression_str)
        context.setFeature(feature)
        value = expression.evaluate(context)
        new_feature = QgsFeature()
        new_feature.setGeometry(value)

        new_feature.setFields(self._layer.fields())
        new_feature.setAttribute("color", color)
        new_feature.setAttribute("name", name)
        return new_feature

    def _canvas_clicked(self, canvasPoint: QgsPointXY) -> None:
        base_crs = self.crs_selection_widget.crs()
        self.x_origin_spinbox.blockSignals(True)
        self.y_origin_spinbox.blockSignals(True)
        geom = QgsGeometry.fromPointXY(canvasPoint)
        geom.transform(
            QgsCoordinateTransform(
                iface.mapCanvas().mapSettings().destinationCrs(),
                base_crs,
                QgsProject.instance(),
            )
        )
        self.x_origin_spinbox.setValue(geom.asPoint().x())
        self.y_origin_spinbox.setValue(geom.asPoint().y())
        self.x_origin_spinbox.blockSignals(False)
        self.y_origin_spinbox.blockSignals(False)
        self._update_origin_point_layer_val()

    @staticmethod
    def _reproject_layer(dst_crs: QgsCoordinateReferenceSystem, layer: QgsVectorLayer):
        """
        Reproject layer feature and update crs

        Args:
            dst_crs: (QgsCoordinateReferenceSystem) destination crs
            layer: (QgsVectorLayer) layer to be reprojected
        """
        src_crs = layer.crs()
        for feat in layer.getFeatures():
            # you need to store the geometry in a variable to reproject it; you can not reproject without doing so
            geom = feat.geometry()
            # transform the geometry
            geom.transform(
                QgsCoordinateTransform(src_crs, dst_crs, QgsProject.instance())
            )
            # now update the features geometry
            feat.setGeometry(geom)
            # dont forget to update the feature, otherwise the changes will have no effect
            layer.updateFeature(feat)
        # finally simply set the layers crs to the new one
        layer.setCrs(dst_crs)
