import os
import sys

import qgis.processing as processing
from qgis.core import (
    QgsFields,
    QgsMapLayerProxyModel,
    QgsProcessingException,
    QgsProject,
)
from qgis.gui import QgsMapToolExtent
from qgis.PyQt import QtCore, QtGui, uic
from qgis.PyQt.QtGui import QCursor, QGuiApplication
from qgis.PyQt.QtWidgets import QHeaderView, QMessageBox, QShortcut, QWidget
from qgis.utils import iface
from xplordb.datamodel.collar import Collar

from openlog.gui.collar_creation.collar_creation_layer_model import (
    CollarCreationLayerModel,
)
from openlog.processing.rotated_grid_point_creation import (
    RotatedGridPointCreationAlgorihtm,
)
from openlog.toolbelt import PlgLogger


class CollarCreationWidget(QWidget):
    def __init__(self, parent=None) -> None:
        """
        QWidget to display collar for creation.

        Use CollarCreationLayerModel for table definition with temporary layer use

        Args:
            parent: QWidget parent
        """
        super().__init__(parent)

        self.log = PlgLogger().log
        self._initial_tool = iface.mapCanvas().mapTool()

        uic.loadUi(
            os.path.dirname(os.path.realpath(__file__)) + "/collar_creation_widget.ui",
            self,
        )
        self._model = (
            CollarCreationLayerModel()
        )  # No parent so we can remove layer when widget is deleted
        self.collar_table_view.setModel(self._model)
        self.collar_table_view.horizontalHeader().setSectionResizeMode(
            QHeaderView.Stretch
        )

        self.prefix_edit.textChanged.connect(
            lambda text: self._model.set_hole_id_prefix(text)
        )
        self.index_start_spinbox.valueChanged.connect(
            lambda value: self._model.set_index_start(value)
        )
        self.spb_length_numbering.valueChanged.connect(
            lambda value: self._model.set_length_numbering(value)
        )

        self._model.set_index_start(self.index_start_spinbox.value())
        self._model.set_hole_id_prefix(self.prefix_edit.text())
        self._model.set_length_numbering(self.spb_length_numbering.value())

        self._connect_column_update()
        self._connect_row_update()

        self._bulk_creation_tool_extent = QgsMapToolExtent(iface.mapCanvas())
        self._bulk_creation_tool_extent.extentChanged.connect(self._tool_extent_updated)

        self.number_radiobutton.clicked.connect(self._bulk_creation_mode_changed)
        self.distance_radiobutton.clicked.connect(self._bulk_creation_mode_changed)

        self.bulk_extent_groupbox.extentChanged.connect(self._extent_updated)
        self.rotation_spinbox.valueChanged.connect(self._update_bulk_layer)

        self.rows_spinbox.setMinimum(2)
        self.rows_spinbox.setMaximum(10000)

        self.columns_spinbox.setMinimum(2)
        self.columns_spinbox.setMaximum(10000)

        self.horizontal_spacing_spinbox.setMinimum(1.0)
        self.horizontal_spacing_spinbox.setMaximum(sys.float_info.max)

        self.vertical_spacing_spinbox.setMinimum(1.0)
        self.vertical_spacing_spinbox.setMaximum(sys.float_info.max)

        self._savedTool = iface.mapCanvas().mapTool()
        self._bulk_layer = None
        self.bulk_extent_groupbox.setOutputCrs(
            iface.mapCanvas().mapSettings().destinationCrs()
        )
        self.bulk_extent_groupbox.setMapCanvas(iface.mapCanvas())

        self.bulk_groupbox.clicked.connect(self._enable_bulk)
        self.add_bulk_button.setEnabled(False)
        self.add_bulk_button.clicked.connect(self._add_bulk_layer_to_model_layer)

        self.dtm_layer_combobox.layerChanged.connect(self._dtm_layer_changed)
        self.dtm_layer_combobox.setFilters(QgsMapLayerProxyModel.RasterLayer)
        self.dtm_layer_combobox.setAllowEmptyLayer(True, self.tr("None"))

        if self.bulk_groupbox.isChecked():
            self._enable_bulk()

        self._dtm_layer_changed()

        self.shortcut_close = QShortcut(QtGui.QKeySequence("Del"), self)
        self.shortcut_close.activated.connect(self._shortcut_del)

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

        """
        while self.collar_table_view.selectionModel().selectedRows():
            self._model.removeRow(
                self.collar_table_view.selectionModel().selectedRows()[0].row()
            )

    def _dtm_layer_changed(self) -> None:
        """
        Update model dtm layer for Z definition from DTM

        """
        self._model.set_dtm_layer(self.dtm_layer_combobox.currentLayer())

    def _bulk_creation_mode_changed(self) -> None:
        """
        Update bulk layer when creation mode is changed.
        Define horizontal and vertical spacing for row/cols


        """
        number_enabled = self.number_radiobutton.isChecked()
        self.rows_spinbox.setEnabled(number_enabled)
        self.columns_spinbox.setEnabled(number_enabled)
        self.horizontal_spacing_spinbox.setEnabled(not number_enabled)
        self.vertical_spacing_spinbox.setEnabled(not number_enabled)

        # Define spacing if number enabled
        if number_enabled:
            self._update_horizontal_spacing_from_rows()
            self._update_vertical_spacing_from_cols()
        self._update_bulk_layer()

    def _connect_column_update(self) -> None:
        """
        Connect signals for columns update

        """
        self.columns_spinbox.valueChanged.connect(self._nb_column_changed)
        self.horizontal_spacing_spinbox.valueChanged.connect(
            self._horizontal_spacing_changed
        )

    def _disconnect_column_update(self) -> None:
        """
        Disconnect signals for columns update

        """
        self.columns_spinbox.valueChanged.disconnect(self._nb_column_changed)
        self.horizontal_spacing_spinbox.valueChanged.disconnect(
            self._horizontal_spacing_changed
        )

    def _connect_row_update(self) -> None:
        """
        Connect signals for row update

        """
        self.rows_spinbox.valueChanged.connect(self._nb_row_changed)
        self.vertical_spacing_spinbox.valueChanged.connect(
            self._vertical_spacing_changed
        )

    def _disconnect_row_update(self) -> None:
        """
        Disconnect signals for row update

        """
        self.rows_spinbox.valueChanged.disconnect(self._nb_row_changed)
        self.vertical_spacing_spinbox.valueChanged.disconnect(
            self._vertical_spacing_changed
        )

    def _nb_row_changed(self) -> None:
        """
        Update bulk layer and horizontal spacing when number of row is changed

        """
        self._update_horizontal_spacing_from_rows()
        self._update_bulk_layer()

    def _update_horizontal_spacing_from_rows(self) -> None:
        """
        Update horizontal spacing from number of row and current extent

        """
        extent = self.bulk_extent_groupbox.outputExtent()
        if not extent.isNull():
            self._disconnect_row_update()
            self.vertical_spacing_spinbox.setValue(
                RotatedGridPointCreationAlgorihtm.nb_row_to_vertical_spacing(
                    extent, self.rows_spinbox.value()
                )
            )
            self._connect_row_update()

    def _vertical_spacing_changed(self) -> None:
        """
        Update number of column and bulk layer when vertical spacing is changed

        """
        extent = self.bulk_extent_groupbox.outputExtent()
        if not extent.isNull():
            self._disconnect_row_update()
            self.rows_spinbox.setValue(
                RotatedGridPointCreationAlgorihtm.vertical_spacing_to_nb_row(
                    extent, self.vertical_spacing_spinbox.value()
                )
            )
            self._connect_row_update()
        self._update_bulk_layer()

    def _nb_column_changed(self) -> None:
        """
        Update bulk layer and vertical spacing when number of column is changed

        """
        self._update_vertical_spacing_from_cols()
        self._update_bulk_layer()

    def _update_vertical_spacing_from_cols(self):
        """
        Update vertical spacing from number of column and current extent

        """
        extent = self.bulk_extent_groupbox.outputExtent()
        if not extent.isNull():
            self._disconnect_column_update()
            self.horizontal_spacing_spinbox.setValue(
                RotatedGridPointCreationAlgorihtm.nb_col_to_horizontal_spacing(
                    extent, self.columns_spinbox.value()
                )
            )
            self._connect_column_update()

    def _horizontal_spacing_changed(self) -> None:
        """
        Update number of row and bulk layer when horizontal spacing is changed

        """
        extent = self.bulk_extent_groupbox.outputExtent()
        if not extent.isNull():
            self._disconnect_column_update()
            self.columns_spinbox.setValue(
                RotatedGridPointCreationAlgorihtm.horizontal_spacing_to_nb_col(
                    extent, self.horizontal_spacing_spinbox.value()
                )
            )
            self._connect_column_update()
        self._update_bulk_layer()

    def _enable_bulk(self) -> None:
        """
        Enable bulk creation depending on current selected options

        """
        if self.bulk_groupbox.isChecked():
            self.add_bulk_button.setEnabled(True)
            self.add_bulk_button.setText(self.tr("Add collars (0)"))
            self._savedTool = iface.mapCanvas().mapTool()
            iface.mapCanvas().setMapTool(self._bulk_creation_tool_extent)
            self._update_bulk_layer()
        else:
            self.add_bulk_button.setEnabled(False)
            self.add_bulk_button.setText(self.tr("Add collar"))
            if self._bulk_layer:
                QgsProject.instance().removeMapLayer(self._bulk_layer.id())
                self._bulk_layer = None
            iface.mapCanvas().setMapTool(self._savedTool)

    def _add_bulk_layer_to_model_layer(self) -> None:
        """
        Add bulk layer feature with fields removed to model layer

        """
        if self._bulk_layer:
            QGuiApplication.setOverrideCursor(QCursor(QtCore.Qt.WaitCursor))
            features = []
            for feature in self._bulk_layer.getFeatures():
                feature.setFields(QgsFields())
                features.append(feature)
            self._model._layer.addFeatures(features)
            QGuiApplication.restoreOverrideCursor()

    def _tool_extent_updated(self):
        """
        Update selected extent from QgsMapTool

        """
        extent = self._bulk_creation_tool_extent.extent()
        self.bulk_extent_groupbox.setOutputExtentFromUser(
            extent, iface.mapCanvas().mapSettings().destinationCrs()
        )

    def _extent_updated(self) -> None:
        """
        Update bulk layer and horizontal / vertical spacing when extent is updated

        """
        # Define spacing if number enabled
        if self.number_radiobutton.isChecked():
            self._update_horizontal_spacing_from_rows()
            self._update_vertical_spacing_from_cols()
        extent = self.bulk_extent_groupbox.outputExtent()
        if not extent.isNull():
            self._update_bulk_layer()

    def _update_bulk_layer(self) -> None:
        """
        Create temporary bulk layer from current extent and bulk options.
        Check if expected number of point before running algorithm.

        """
        extent = self.bulk_extent_groupbox.outputExtent()
        if not extent.isNull():
            if self._bulk_layer:
                QgsProject.instance().removeMapLayer(self._bulk_layer.id())
                self._bulk_layer = None
                iface.mapCanvas().refresh()

            # Check estimated number of point
            if self.number_radiobutton.isChecked():
                nb_row = self.columns_spinbox.value()
                nb_col = self.rows_spinbox.value()
            else:
                nb_row = RotatedGridPointCreationAlgorihtm.vertical_spacing_to_nb_row(
                    extent, self.vertical_spacing_spinbox.value()
                )
                nb_col = RotatedGridPointCreationAlgorihtm.horizontal_spacing_to_nb_col(
                    extent, self.horizontal_spacing_spinbox.value()
                )
            total = nb_col * nb_row
            if total > 1e5:
                ret = QMessageBox.question(
                    self,
                    self.tr("Warning"),
                    self.tr("{0} collars will be created. Do you confirm ?").format(
                        total
                    ),
                    QMessageBox.Yes | QMessageBox.No,
                    QMessageBox.Yes,
                )
                if ret != QMessageBox.Yes:
                    return

            self.add_bulk_button.setText(self.tr("Add ({0})").format(total))
            params = {
                RotatedGridPointCreationAlgorihtm.EXTENT: extent,
                RotatedGridPointCreationAlgorihtm.CRS: iface.mapCanvas()
                .mapSettings()
                .destinationCrs(),
                RotatedGridPointCreationAlgorihtm.ANGLE: self.rotation_spinbox.value(),
                RotatedGridPointCreationAlgorihtm.OUTPUT: "TEMPORARY_OUTPUT",
            }

            if self.number_radiobutton.isChecked():
                params[
                    RotatedGridPointCreationAlgorihtm.NB_COL
                ] = self.columns_spinbox.value()
                params[
                    RotatedGridPointCreationAlgorihtm.NB_ROW
                ] = self.rows_spinbox.value()
            else:
                params[
                    RotatedGridPointCreationAlgorihtm.HSPACING
                ] = self.horizontal_spacing_spinbox.value()
                params[
                    RotatedGridPointCreationAlgorihtm.VSPACING
                ] = self.vertical_spacing_spinbox.value()

            algo_str = "openlog:rotated_grid_point_creation"
            try:
                QGuiApplication.setOverrideCursor(QCursor(QtCore.Qt.WaitCursor))
                result = processing.run(algo_str, params)
                QGuiApplication.restoreOverrideCursor()
                self._bulk_layer = result[RotatedGridPointCreationAlgorihtm.OUTPUT]
                self._bulk_layer.setName(self.tr("Bulk collar creation"))
                QgsProject.instance().addMapLayer(self._bulk_layer)
            except QgsProcessingException as exc:
                QGuiApplication.restoreOverrideCursor()
                self.log(
                    self.tr("Can't create point on rotated grid : {0}").format(exc),
                    push=True,
                )

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

        Must be called when widget is not used anymore

        """
        self._model.remove_temp_layer()
        if self._bulk_layer:
            QgsProject.instance().removeMapLayer(self._bulk_layer.id())
            self._bulk_layer = None
        iface.mapCanvas().setMapTool(self._initial_tool)

    def get_collar_list(self) -> [Collar]:
        """
        Get collar list from CollarCreationLayerModel

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

        """
        return self._model.get_collar_list()
