"""
/***************************************************************************
 qgSurf - plugin for Quantum GIS

 Processing of geological planes and surfaces

                              -------------------
        begin                : 2011-12-21
        copyright            : (C) 2011-2026 by Mauro Alberti
        email                : alberti.m65@gmail.com

 ***************************************************************************/

# licensed under the terms of GNU GPL 3

/***************************************************************************
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 3 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 ***************************************************************************/
"""

import logging
from math import atan, isfinite

logger = logging.getLogger(__name__)

try:
    from osgeo import ogr
except ImportError:
    import ogr

from qgis.PyQt.QtWidgets import *
from qgis.core import Qgis

from geogst.core.geometries.grids.rasters import *
from geogst.io.rasters.gdal import *
from geogst.io.vectors.ogr import *

from ..apps.qgis.canvas import *
from ..apps.qgis.orientations import *
from ..apps.qgis.points import *
from ..apps.qgis.project import *
from ..apps.qgis.projections import *
from ..apps.qgis.tables import *
from ..apps.qt.messages import *
from ..apps.qgis.messages import *

from ..configurations.colors import *
from ..configurations.fields import *
from ..configurations.general_params import *
from ..configurations.messages import *
from ..configurations.plane_points_distances_params import *

from ..ui.auxiliary_windows.distances import *
from ..utils.planes import *


class PlaneDistancesWidget(QWidget):
    """
    Constructor
    
    """

    line_colors = [
        "white",
        "red",
        "blue",
        "yellow",
        "orange",
        "brown"
    ]

    def __init__(self,
                 plugin_folder: str,
                 module_name: str,
                 canvas,
                 plugin_qaction):

        super(PlaneDistancesWidget, self).__init__()

        self.plugin_folder = plugin_folder
        self.module_name = module_name

        self.canvas = canvas
        self.plugin = plugin_qaction

        self.setup_gui()
        self.initialize_parameters()
        self.store_current_map_crs()

    def initialize_parameters(self):

        self.previous_tool = None
        self.use_elevation_from_dem = False
        self.map_tool = None

        self.reset_everything()

    def reset_post_dem_change(self):

        self.disable_canvas_tools()
        self.reset_input_dem_parameters()
        self.reset_intersections()

    def reset_everything(self):

        self.disable_canvas_tools()
        self.reset_all_io_parameters()

    def reset_source_point_parameters(self):

        self.reset_source_point_in_map_QPushButton.setEnabled(False)
        self.reset_source_point_and_intersections()

    def reset_all_io_parameters(self):

        self.reset_all_input_parameters()
        self.reset_intersections()

    def reset_all_input_parameters(self):

        self.reset_input_dem_parameters()
        self.reset_source_point()
        self.reset_input_geoplane_parameters()

    def reset_input_dem_parameters(self):

        self.qgis_grid_layer = None
        self.elevation_grid = None
        self.current_source_point_elevation_value = None

    def disable_canvas_tools(self):

        self.set_source_point_in_map_QPushButton.setEnabled(False)
        self.reset_source_point_in_map_QPushButton.setEnabled(False)

        try:
            self.map_tool.canvasClicked.disconnect(self.update_source_point_location)
        except Exception:
            logger.debug("map_tool.canvasClicked was not connected", exc_info=True)

        try:
            self.restore_previous_map_tool(self.map_tool)
        except Exception:
            logger.debug("restore_previous_map_tool failed", exc_info=True)

    def reset_source_point_and_intersections(self):

        self.reset_intersections()
        self.reset_source_point()

    def reset_intersections(self):

        if hasattr(self, 'intersections_markers_list'):
            self.remove_intersection_markers_from_canvas()
        self.reset_intersections_markers_and_values()

    def reset_intersections_markers_list(self):

        self.intersections_markers_list = []

    def reset_intersections_values(self):

        self.intersections_x = []
        self.intersections_y = []
        self.intersections_xprt = {}
        self.inters = None

    def reset_intersections_markers_and_values(self):

        self.reset_intersections_markers_list()
        self.reset_intersections_values()

    def reset_source_point(self):

        self.clear_source_point_qlineedit_forms()
        if hasattr(self, 'source_point_marker_list'):
            self.remove_source_point_marker_from_canvas()
        self.reset_source_point_coordinates()
        self.reset_source_point_marker_list()

    def reset_source_point_coordinates(self):

        self.source_point_longitude = None
        self.source_point_latitude = None
        self.source_point_elevation = None

    def reset_source_point_marker_list(self):

        self.source_point_marker_list = []

    def _safe_remove_marker(self, mrk):
        """
        Rimuove il marker dalla sua scena solo se esiste ancora e appartiene a una scena.
        Evita i warning QGraphicsScene::removeItem.
        """
        try:
            if mrk is None:
                return

            scene = mrk.scene()
            if scene is not None:
                scene.removeItem(mrk)
            # opzionale: nascondi comunque il marker
            mrk.setVisible(False)

        except Exception:
            # se il wrapped C++ object è già distrutto o altro, ignora tranquillamente
            logger.debug("marker removal skipped: underlying C++ object likely destroyed", exc_info=True)

    def remove_intersection_markers_from_canvas(self):

        if not hasattr(self, 'intersections_markers_list'):
            return

        for mrk in self.intersections_markers_list:
            self._safe_remove_marker(mrk)

        # importantissimo: svuotare la lista
        self.intersections_markers_list = []

    def remove_source_point_marker_from_canvas(self):

        if not hasattr(self, 'source_point_marker_list'):
            return

        for mrk in self.source_point_marker_list:
            self._safe_remove_marker(mrk)

        # importantissimo: svuotare la lista
        self.source_point_marker_list = []

    def reset_input_geoplane_parameters(self):

        self.source_dip_direction = None
        self.source_dip_angle = None

    def refresh_raster_layer_list_for_elevation(self):

        try:
            self.choose_source_dem_QComboBox.currentIndexChanged[int].disconnect(self.get_chosen_dem)
        except Exception:
            logger.debug("choose_source_dem_QComboBox.currentIndexChanged was not connected", exc_info=True)

        try:
            self.reset_everything()
        except Exception:
            logger.debug("reset_everything failed", exc_info=True)

        try:
            self.choose_source_dem_QComboBox.clear()
        except:
            return

        self.choose_source_dem_QComboBox.addItem(OPTIONAL_TEXT)

        self.qgis_raster_layers = loaded_raster_layers()
        if self.qgis_raster_layers is None or len(self.qgis_raster_layers) == 0:
            return
        for layer in self.qgis_raster_layers:
            self.choose_source_dem_QComboBox.addItem(layer.name())

        self.choose_source_dem_QComboBox.currentIndexChanged[int].connect(self.get_chosen_dem)

    def refresh_raster_layer_list_for_distances(self):

        try:
            self.choose_distances_dem_QComboBox.clear()
        except:
            return

        self.choose_distances_dem_QComboBox.addItem(REQUIRED_TEXT)

        self.qgis_raster_layers_for_elevations = loaded_raster_layers()
        if self.qgis_raster_layers_for_elevations is None or len(self.qgis_raster_layers_for_elevations) == 0:
            return
        for layer in self.qgis_raster_layers_for_elevations:
            self.choose_distances_dem_QComboBox.addItem(layer.name())

    def setup_gui(self):

        layout = QVBoxLayout()

        widget = QTabWidget()

        widget.addTab(self.setup_point_intersections_tab(), "Processing")
        widget.addTab(self.setup_help_tab(), "Help")
        
        layout.addWidget(widget)

        self.setLayout(layout)
        self.adjustSize()

        self.setWindowTitle(f'{plugin_name} - {self.module_name}')

    def setup_point_intersections_tab(self):
        
        widget = QWidget()

        layout = QVBoxLayout()
        layout.addWidget(self.setup_analysis_parameters_loading())
        layout.addWidget(self.setup_processing_tabs())

        widget.setLayout(layout)
        return widget

    def setup_analysis_parameters_loading(self):

        widget = QWidget()

        layout = QGridLayout()

        # load geographical and geological parameters

        layout.addWidget(QLabel(self.tr("Load analysis parameters (plane source point and orientation) from file:")), 2, 0, 1, 2)

        input_file_browser = QPushButton(".....")
        input_file_browser.clicked.connect(self.load_analysis_parameters)
        layout.addWidget(input_file_browser, 2, 2, 1, 1)

        widget.setLayout(layout)
        return widget

    def setup_processing_tabs(self):

        widget = QWidget()

        layout = QVBoxLayout()

        toolbox = QToolBox()
        toolbox.addItem(self.setup_source_point_parameters_tab(), 'Plane -> source point definition')
        toolbox.addItem(self.setup_plane_orientation_parameters_tab(), 'Plane -> orientation definition')
        toolbox.addItem(self.setup_calculations_distances_from_dem_tab(), 'Calculate distances of DEM cell centers from plane')
        toolbox.addItem(self.setup_calculations_points_distances_tab(), 'Calculate distances of points from plane')
        toolbox.addItem(self.setup_export_geoparameters_tab(), 'Export analysis parameters')

        layout.addWidget(toolbox)
        widget.setLayout(layout)

        return widget

    def setup_help_tab(self):
        
        widget = QWidget()
        layout = QVBoxLayout()

        # About section

        text_browser = QTextBrowser(widget)

        url_path = f"file:///{self.plugin_folder}/help/help_pts_plane_dists.html"
        text_browser.setSource(QUrl(url_path))
        text_browser.setSearchPaths([f'{self.plugin_folder}/help/images/di'])

        layout.addWidget(text_browser)

        widget.setLayout(layout)

        return widget

    def setup_source_point_parameters_tab(self):
        
        widget = QWidget()

        layout = QGridLayout()

        self.set_source_point_in_map_QPushButton = QPushButton("Set source point in map")
        self.set_source_point_in_map_QPushButton.clicked[bool].connect(self.set_source_point_in_map)
        layout.addWidget(self.set_source_point_in_map_QPushButton, 0, 0, 1, 3)

        self.lock_z_value_to_dem_surface_QCheckBox = QCheckBox("Get elevation from DEM ->")
        self.lock_z_value_to_dem_surface_QCheckBox.setChecked(False)
        self.lock_z_value_to_dem_surface_QCheckBox.stateChanged[int].connect(self.update_source_point_elevation_value)
        layout.addWidget(self.lock_z_value_to_dem_surface_QCheckBox, 1, 0, 1, 1)

        self.choose_source_dem_QComboBox = QComboBox()
        self.choose_source_dem_QComboBox.addItem(OPTIONAL_TEXT)
        layout.addWidget(self.choose_source_dem_QComboBox, 1, 1, 1, 2)

        self.refresh_raster_layer_list_for_elevation_QPushButton = QPushButton("Update raster layer list")
        self.refresh_raster_layer_list_for_elevation_QPushButton.clicked[bool].connect(self.refresh_raster_layer_list_for_elevation)
        self.refresh_raster_layer_list_for_elevation_QPushButton.setEnabled(True)
        layout.addWidget(self.refresh_raster_layer_list_for_elevation_QPushButton, 2, 0, 1, 3)

        self.refresh_raster_layer_list_for_elevation()

        layout.addWidget(QLabel("Longitude (EPSG: 4326)"), 3, 0, 1, 1)
        self.source_point_longitude_QLineEdit = QLineEdit()
        self.source_point_longitude_QLineEdit.textEdited.connect(self.update_source_point_location)
        layout.addWidget(self.source_point_longitude_QLineEdit, 3, 1, 1, 2)
              
        layout.addWidget(QLabel("Latitude (EPSG: 4326)"), 4, 0, 1, 1)
        self.source_point_latitude_QLineEdit = QLineEdit()
        self.source_point_latitude_QLineEdit.textEdited.connect(self.update_source_point_location)
        layout.addWidget(self.source_point_latitude_QLineEdit, 4, 1, 1, 2)
                      
        layout.addWidget(QLabel("Elevation"), 5, 0, 1, 1)
        self.source_point_elevation_QLineEdit = QLineEdit()
        self.source_point_elevation_QLineEdit.textEdited.connect(self.check_elevation_congruence_with_dem)
        layout.addWidget(self.source_point_elevation_QLineEdit, 5, 1, 1, 2)

        self.reset_source_point_in_map_QPushButton = QPushButton("Reset source point")
        self.reset_source_point_in_map_QPushButton.clicked[bool].connect(self.reset_source_point_parameters)
        layout.addWidget(self.reset_source_point_in_map_QPushButton, 6, 0, 1, 3)

        widget.setLayout(layout)
        
        return widget

    def store_current_map_crs(self):

        self.map_crs = self.canvas.mapSettings().destinationCrs()

    def get_current_map_crs(self):

        return self.canvas.mapSettings().destinationCrs()

    def check_elevation_congruence_with_dem(self):
        
        if self.use_elevation_from_dem and float(self.source_point_elevation_QLineEdit.text()) != self.current_source_point_elevation_value:
            self.use_elevation_from_dem = False
            self.lock_z_value_to_dem_surface_QCheckBox.setChecked(False)
            
        self.current_source_point_elevation_value = float(self.source_point_elevation_QLineEdit.text())

    def setup_plane_orientation_parameters_tab(self):

        widget = QWidget()
        layout = QGridLayout()

        layout.addWidget(QLabel("Input dip azimuth is relative to:"), 0, 0, 1, 3)

        self.direction_azimuth_is_relative_to_geographic_north_QRadioButton = QRadioButton("Geographic North")
        self.direction_azimuth_is_relative_to_geographic_north_QRadioButton.setChecked(True)
        layout.addWidget(self.direction_azimuth_is_relative_to_geographic_north_QRadioButton, 1, 1, 1, 1)

        self.direction_azimuth_is_relative_to_map_top_QRadioButton = QRadioButton("Current map top (Y axis)")
        layout.addWidget(self.direction_azimuth_is_relative_to_map_top_QRadioButton, 1, 2, 1, 1)

        dip_dir_label = QLabel("Dip direction")
        dip_dir_label.setAlignment (Qt.AlignmentFlag.AlignCenter)       
        layout.addWidget(dip_dir_label, 2, 0, 1, 2)

        dip_ang_label = QLabel("Dip angle")
        dip_ang_label.setAlignment(Qt.AlignmentFlag.AlignCenter)       
        layout.addWidget(dip_ang_label, 2, 2, 1, 1)
        
        self.dip_direction_QDial = QDial()
        self.dip_direction_QDial.setRange(2, 360)
        self.dip_direction_QDial.setPageStep(1)
        self.dip_direction_QDial.setProperty("value", 180)
        self.dip_direction_QDial.setSliderPosition(180)
        self.dip_direction_QDial.setTracking(True)
        self.dip_direction_QDial.setOrientation(Qt.Orientation.Vertical)
        self.dip_direction_QDial.setWrapping(True)
        self.dip_direction_QDial.setNotchTarget(30.0)
        self.dip_direction_QDial.setNotchesVisible(True)
        self.dip_direction_QDial.valueChanged[int].connect(self.update_dip_direction_spinbox)
        layout.addWidget(self.dip_direction_QDial, 3, 0, 1, 2)
                
        self.DAngle_verticalSlider = QSlider()
        self.DAngle_verticalSlider.setRange(0, 90)
        self.DAngle_verticalSlider.setProperty("value", 45)
        self.DAngle_verticalSlider.setOrientation(Qt.Orientation.Vertical)
        self.DAngle_verticalSlider.setInvertedAppearance(True)
        self.DAngle_verticalSlider.setTickPosition(QSlider.TickPosition.TicksBelow)
        self.DAngle_verticalSlider.setTickInterval(15)
        self.DAngle_verticalSlider.valueChanged[int].connect(self.update_dip_angle_spinbox)
        layout.addWidget(self.DAngle_verticalSlider, 3, 2, 1, 1)

        self.dip_direction_QDoubleSpinBox = QDoubleSpinBox()
        self.dip_direction_QDoubleSpinBox.setRange(0.0, 359.999)
        self.dip_direction_QDoubleSpinBox.setDecimals(3)
        self.dip_direction_QDoubleSpinBox.setSingleStep(1.0)
        self.dip_direction_QDoubleSpinBox.valueChanged[float].connect(self.update_dip_direction_slider)
        layout.addWidget(self.dip_direction_QDoubleSpinBox, 4, 0, 1, 2)
         
        self.dip_angle_QDoubleSpinBox = QDoubleSpinBox()
        self.dip_angle_QDoubleSpinBox.setRange(0.0, 90.0)
        self.dip_angle_QDoubleSpinBox.setDecimals(3)
        self.dip_angle_QDoubleSpinBox.setSingleStep(0.1)
        self.dip_angle_QDoubleSpinBox.setProperty("value", 45)
        self.dip_angle_QDoubleSpinBox.valueChanged[float].connect(self.update_dip_angle_slider)
        layout.addWidget(self.dip_angle_QDoubleSpinBox, 4, 2, 1, 1)

        widget.setLayout(layout)

        return widget
        
    def setup_calculations_distances_from_dem_tab(self):

        widget = QWidget()
        layout = QGridLayout()

        layout.addWidget(QLabel("Use DEM"), 0, 0, 1, 1)

        self.choose_distances_dem_QComboBox = QComboBox()
        self.choose_distances_dem_QComboBox.addItem(REQUIRED_TEXT)
        layout.addWidget(self.choose_distances_dem_QComboBox, 0, 1, 1, 2)

        self.refresh_raster_layer_list_for_distances_QPushButton = QPushButton("Update raster layer list")
        self.refresh_raster_layer_list_for_distances_QPushButton.clicked[bool].connect(self.refresh_raster_layer_list_for_distances)
        self.refresh_raster_layer_list_for_distances_QPushButton.setEnabled(True)
        layout.addWidget(self.refresh_raster_layer_list_for_distances_QPushButton, 0, 3, 1, 1)

        self.refresh_raster_layer_list_for_distances()

        layout.addWidget(QLabel("Upper distance threshold (positive: above plane)"), 1, 0, 1, 3)

        self.above_thresold_QDoubleSpinBox = QDoubleSpinBox()
        self.above_thresold_QDoubleSpinBox.setMinimum(-1000.0)
        self.above_thresold_QDoubleSpinBox.setMaximum(1000.0)
        self.above_thresold_QDoubleSpinBox.setValue(100.0)
        layout.addWidget(self.above_thresold_QDoubleSpinBox, 1, 3, 1, 1)

        layout.addWidget(QLabel("Lower distance threshold (negative: below plane)"), 2, 0, 1, 3)

        self.lower_threshold_QDoubleSpinBox = QDoubleSpinBox()
        self.lower_threshold_QDoubleSpinBox.setMinimum(-1000.0)
        self.lower_threshold_QDoubleSpinBox.setMaximum(1000.0)
        self.lower_threshold_QDoubleSpinBox.setValue(-100.0)
        layout.addWidget(self.lower_threshold_QDoubleSpinBox, 2, 3, 1, 1)

        layout.addWidget(QLabel(self.tr("Save results in")), 3, 0, 1, 1)

        self.output_grid_path_input_QLineEdit = QLineEdit()
        layout.addWidget(self.output_grid_path_input_QLineEdit, 3, 1, 1, 1)

        output_browser = QPushButton(".....")
        output_browser.clicked.connect(self.select_output_grid_file)
        layout.addWidget(output_browser, 3, 2, 1, 1)

        self.load_output_grid_in_map_QCheckBox = QCheckBox("Load in map")
        layout.addWidget(self.load_output_grid_in_map_QCheckBox, 3, 3, 1, 1)

        self.calculate_distance_grid_QPushButton = QPushButton("Calculate distances grid")
        self.calculate_distance_grid_QPushButton.clicked[bool].connect(self.calculate_distances_of_dem_centers_from_plane)
        layout.addWidget(self.calculate_distance_grid_QPushButton, 4, 0, 1, 4)

        # final setup

        widget.setLayout(layout)

        return widget

    def setup_calculations_points_distances_tab(self):

        widget = QWidget()

        layout = QGridLayout()

        # input layer

        input_point_layer_QGroupBox = QGroupBox("Input point layer")
        input_point_layer_QGridLayout = QGridLayout()

        self.input_point_layer_QComboBox = QComboBox()
        input_point_layer_QGridLayout.addWidget(self.input_point_layer_QComboBox, 0, 0, 1, 2)

        input_point_layer_QGroupBox.setLayout(input_point_layer_QGridLayout)
        layout.addWidget(input_point_layer_QGroupBox)

        # plane values

        input_fields_QGroupBox = QGroupBox("Point layer source fields")
        input_fields_QGridLayout = QGridLayout()

        input_fields_QGridLayout.addWidget(QLabel("id"), 0, 0, 1, 1)
        self.id_field_QComboBox = QComboBox()
        input_fields_QGridLayout.addWidget(self.id_field_QComboBox, 0, 1, 1, 1)

        input_fields_QGridLayout.addWidget(QLabel("z"), 1, 0, 1, 1)
        self.z_field_QComboBox = QComboBox()
        input_fields_QGridLayout.addWidget(self.z_field_QComboBox, 1, 1, 1, 1)

        self.input_fields_QComboBoxes = [
            self.id_field_QComboBox,
            self.z_field_QComboBox]

        self.refresh_struct_point_lyr_combobox()

        self.input_point_layer_QComboBox.currentIndexChanged[int].connect(self.refresh_structural_fields_comboboxes)
        # from https://github.com/qgis/QGIS/issues/41135
        project = QgsProject.instance()
        project.layersAdded.connect(self.refresh_struct_point_lyr_combobox)
        project.layersRemoved.connect(self.refresh_struct_point_lyr_combobox)

        input_fields_QGroupBox.setLayout(input_fields_QGridLayout)
        layout.addWidget(input_fields_QGroupBox)

        # output layer

        output_layer_QGroupBox = QGroupBox("Output point layer")
        output_layer_QGridLayout = QGridLayout()

        self.output_layer_filepath_QLineEdit = QLineEdit()
        output_layer_QGridLayout.addWidget(self.output_layer_filepath_QLineEdit, 0, 0, 1, 1)

        self.define_output_filepath_QPushButton = QPushButton(".....")
        self.define_output_filepath_QPushButton.clicked.connect(self.define_output_vector_file)
        output_layer_QGridLayout.addWidget(self.define_output_filepath_QPushButton, 0, 1, 1, 1)

        self.load_output_shapefile_in_map_QCheckBox = QCheckBox("Load in map")
        output_layer_QGridLayout.addWidget(self.load_output_shapefile_in_map_QCheckBox, 0, 2, 1, 1)

        self.calculate_points_distances_QPushButton = QPushButton("Calculate distances")
        self.calculate_points_distances_QPushButton.clicked.connect(self.calculate_distances_of_points_from_plane)
        output_layer_QGridLayout.addWidget(self.calculate_points_distances_QPushButton, 1, 0, 1, 3)

        output_layer_QGroupBox.setLayout(output_layer_QGridLayout)
        layout.addWidget(output_layer_QGroupBox)

        widget.setLayout(layout)

        return widget


    def refresh_struct_point_lyr_combobox(self):

        self.loaded_point_layers = loaded_point_layers()
        self.input_point_layer_QComboBox.clear()

        self.input_point_layer_QComboBox.addItem(CHOOSE_TEXT)
        self.input_point_layer_QComboBox.addItems([layer.name() for layer in self.loaded_point_layers])

        self.reset_structural_field_comboboxes()

    def reset_structural_field_comboboxes(self):

        for structural_combox in self.input_fields_QComboBoxes:
            structural_combox.clear()
            structural_combox.addItem(UNDEFINED_TEXT)

    def refresh_structural_fields_comboboxes(self):

        self.reset_structural_field_comboboxes()

        point_shape_qgis_ndx = self.input_point_layer_QComboBox.currentIndex() - 1
        if point_shape_qgis_ndx <= -1:
            return

        self.point_layer = self.loaded_point_layers[point_shape_qgis_ndx]

        point_layer_field_list = self.point_layer.dataProvider().fields().toList()

        field_names = [field.name() for field in point_layer_field_list]

        for structural_combox in self.input_fields_QComboBoxes:
            structural_combox.addItems(field_names)

    def define_output_vector_file(self):

        output_filename, __ = QFileDialog.getSaveFileName(self,
                                                      self.tr("Save shapefile"),
                                                      "*.shp",
                                                      "shp (*.shp *.SHP)")
        if not output_filename:
            return
        self.output_layer_filepath_QLineEdit.setText(output_filename)

    def calculate_distances_of_points_from_plane(self):
        """
        Calculate distances of input layer points from geological plane.
        Coordina il flusso principale: validazione, calcolo, export.
        """
        try:
            # Validazione input
            err_msg = self.check_input_source_point_parameters()
            if err_msg:
                QMessageBox.critical(self, self.module_name, err_msg)
                return

            # Preparazione piano e layer
            plane_result = self._prepare_plane_and_layer()
            if isinstance(plane_result, str):
                warn_qgis(self.module_name, plane_result)
                return

            map_crs_cartesian_plane, point_layer, id_field_name, z_field_name = plane_result

            # Estrazione dati punti
            input_points_data, err = self._extract_point_layer_data(
                point_layer, id_field_name, z_field_name
            )
            if err:
                QMessageBox.critical(self, self.module_name, f"{err!r}")
                return

            if len(input_points_data) == 0:
                warn_qgis(self.module_name, "No data to calculate")
                return

            # Calcolo distanze
            point_attributes, geographic_coordinates = self._calculate_distances(
                input_points_data, point_layer, map_crs_cartesian_plane
            )

            # Export risultati
            output_path = self.output_layer_filepath_QLineEdit.text()
            if not output_path:
                QMessageBox.critical(
                    self,
                    self.module_name,
                    f"Output shapefile path is not defined"
                )
                return

            self._export_distances_to_shapefile(
                output_path, point_attributes, geographic_coordinates
            )

            # Carica layer se richiesto
            if self.load_output_shapefile_in_map_QCheckBox.isChecked():
                self._load_output_layer(output_path)

            QMessageBox.information(
                self,
                self.module_name,
                "Distance point layer created"
            )

        except Exception as e:
            QMessageBox.critical(
                self,
                self.module_name,
                f"Exception while calculating points distance:\n{e!r}"
            )

    def _prepare_plane_and_layer(self):
        """
        Prepara il piano cartesiano e valida il layer di input.
        
        Returns:
            Tuple (map_crs_cartesian_plane, point_layer, id_field_name, z_field_name)
            oppure stringa con messaggio di errore
        """
        # Estrai parametri piano
        current_plane_parameters = self.extract_current_plane_parameters()
        if isinstance(current_plane_parameters, str):
            return current_plane_parameters

        source_point_longitude, \
        source_point_latitude, \
        source_point_elevation, \
        dip_direction_as_north_azimuth, \
        source_dip_angle = current_plane_parameters

        # Proietta piano nel CRS della mappa
        map_crs = self.get_current_map_crs()
        result, err = reorient_plane_and_source_point(
            source_point_longitude=source_point_longitude,
            source_point_latitude=source_point_latitude,
            source_point_elevation=source_point_elevation,
            dip_direction_as_north_azimuth=dip_direction_as_north_azimuth,
            source_dip_angle=source_dip_angle,
            dest_crs=map_crs,
        )

        if err:
            return f"{err!r}"

        map_crs_plane_orientation, map_crs_src_pt_3d = result
        map_crs_cartesian_plane = CPlane3D.from_geol_plane(
            geol_plane=map_crs_plane_orientation,
            pt=map_crs_src_pt_3d
        )

        # Valida layer punti
        point_shape_qgis_ndx = self.input_point_layer_QComboBox.currentIndex() - 1
        if point_shape_qgis_ndx <= -1:
            return "Input point layer is not defined"

        point_layer = self.loaded_point_layers[point_shape_qgis_ndx]
        if not point_layer:
            return "Input point layer is not defined"

        # Valida campi
        id_field_name = self.id_field_QComboBox.currentText()
        if id_field_name == UNDEFINED_TEXT:
            return "Input id field is not defined"

        z_field_name = self.z_field_QComboBox.currentText()
        if z_field_name == UNDEFINED_TEXT:
            return "Input z field is not defined"

        return (map_crs_cartesian_plane, point_layer, id_field_name, z_field_name)

    def _extract_point_layer_data(self, point_layer, id_field_name: str, z_field_name: str):
        """
        Estrae geometrie e attributi dal layer punti.
        
        Returns:
            Tuple (input_points_data, err)
        """
        input_points_layer_data, err = extract_point_layer_geometries_and_attributes(
            layer=point_layer,
            fields=[z_field_name, id_field_name]
        )
        return input_points_layer_data, err

    def _calculate_distances(
        self,
        input_points_data,
        point_layer,
        map_crs_cartesian_plane
    ):
        """
        Calcola distanze dei punti dal piano.
        
        Returns:
            Tuple (point_attributes, geographic_coordinates)
        """
        from qgis.core import QgsMessageLog
        
        point_attributes = []
        geographic_point_coordinates = []
        point_layer_crs = point_layer.crs()
        map_crs = self.get_current_map_crs()

        for input_point_record in input_points_data:
            point_layer_crs_input_pt_x, point_layer_crs_input_pt_y, input_pt_z, id_of_input_point = input_point_record

            # Proietta nel CRS della mappa per calcolo distanza
            result, err = qgis_project_coords(
                x=point_layer_crs_input_pt_x,
                y=point_layer_crs_input_pt_y,
                src_crs=point_layer_crs,
                dest_crs=map_crs
            )

            if err:
                QgsMessageLog.logMessage(
                    f"Error projecting point {id_of_input_point}: {err!r}",
                    "qgSurf",
                    Qgis.MessageLevel.Warning
                )
                continue

            map_crs_input_pt_x, map_crs_input_pt_y = result
            map_crs_input_pt = Point(
                map_crs_input_pt_x,
                map_crs_input_pt_y,
                input_pt_z
            )

            signed_distance = map_crs_cartesian_plane.signed_distance_to_point(map_crs_input_pt)

            # Proietta in coordinate geografiche per export
            result, err = qgis_project_coords(
                x=point_layer_crs_input_pt_x,
                y=point_layer_crs_input_pt_y,
                src_crs=point_layer_crs,
            )

            if err:
                QgsMessageLog.logMessage(
                    f"Error projecting point {id_of_input_point} to geographic: {err!r}",
                    "qgSurf",
                    Qgis.MessageLevel.Warning
                )
                continue

            longitude, latitude = result

            point_attributes.append((
                id_of_input_point,
                longitude,
                latitude,
                input_pt_z,
                signed_distance,
            ))

            geographic_point_coordinates.append((longitude, latitude, input_pt_z))

        return point_attributes, geographic_point_coordinates

    def _export_distances_to_shapefile(
        self,
        output_path: str,
        point_attributes,
        geographic_coordinates
    ) -> None:
        """Esporta risultati in shapefile."""
        from qgis.core import QgsMessageLog
        
        QgsMessageLog.logMessage(
            f"Exporting to shapefile: {output_path}",
            "qgSurf",
            Qgis.MessageLevel.Info
        )

        field_names = [f["name"] for f in field_parameters_of_output_table]

        result, err = create_and_open_shapefile(
            path=output_path,
            geom_type=ogr.wkbPoint25D,
            fields_dict_list=field_parameters_of_output_table,
            crs=4326,
        )

        if err:
            raise Exception(f"Error creating shapefile: {err!r}")

        shapefile_datasource, point_shapelayer = result

        success, msg = try_write_pt_shapefile(
            point_layer=point_shapelayer,
            geoms=geographic_coordinates,
            field_names=field_names,
            attrs=point_attributes
        )

        if not success:
            raise Exception(f"Error writing shapefile: {msg}")

        # Flush e chiusura esplicita
        try:
            shapefile_datasource.SyncToDisk()
        except AttributeError:
            # Non tutti i driver hanno SyncToDisk
            pass

        point_shapelayer = None
        shapefile_datasource = None

    def _load_output_layer(self, output_path: str) -> None:
        """Carica il layer di output nel progetto QGIS."""
        from qgis.core import QgsVectorLayer, QgsProject, QgsMessageLog
        
        try:
            layer_name = os.path.splitext(os.path.basename(output_path))[0]

            QgsMessageLog.logMessage(
                f"Loading layer: {layer_name}",
                "qgSurf",
                Qgis.MessageLevel.Info
            )

            distances_point_layer = QgsVectorLayer(
                output_path,
                layer_name,
                "ogr",
            )

            if not distances_point_layer.isValid():
                QMessageBox.critical(
                    self,
                    self.module_name,
                    f"Unable to load layer '{layer_name}' from '{output_path}': layer is not valid",
                )
                return

            QgsProject.instance().addMapLayer(distances_point_layer)
            QgsMessageLog.logMessage(
                "Point layer was loaded successfully",
                "qgSurf",
                Qgis.MessageLevel.Info
            )

        except Exception as e:
            QMessageBox.critical(
                self,
                self.module_name,
                f"Unable to load point layer in project:\n{e!r}"
            )

    def setup_export_geoparameters_tab(self):

        widget = QWidget()

        layout = QGridLayout()

        self.export_analysis_parameters_QPushButton = QPushButton("Export analysis parameters")
        self.export_analysis_parameters_QPushButton.clicked.connect(self.export_analysis_parameters)
        layout.addWidget(self.export_analysis_parameters_QPushButton, 0, 0, 1, 1)

        self.export_params_file_path_QLineEdit = QLineEdit()
        layout.addWidget(self.export_params_file_path_QLineEdit, 0, 1, 1, 2)

        self.define_export_file_path_QPushButton = QPushButton(".....")
        self.define_export_file_path_QPushButton.clicked.connect(self.define_output_params_file_path)
        layout.addWidget(self.define_export_file_path_QPushButton, 0, 3, 1, 1)

        widget.setLayout(layout)

        return widget

    def export_analysis_parameters(self):

        export_file_path = self.export_params_file_path_QLineEdit.text()

        if export_file_path == "":
            QMessageBox.critical(
                self,
                self.module_name,
                f"No export file path has been defined"
            )
            return

        result = self.extract_current_plane_parameters()

        if isinstance(result, str):
            return None, Error(
                True,
                caller_name(),
                Exception(result),
                traceback.format_exc(),
            )

        source_point_longitude = result[0]
        source_point_latitude = result[1]
        source_point_elevation = result[2]
        dip_direction_as_north_azimuth = result[3]
        source_dip_angle = result[4]

        with open(export_file_path, "w") as ofile:
            ofile.write(f"longitude: {source_point_longitude}\n")
            ofile.write(f"latitude: {source_point_latitude}\n")
            ofile.write(f"elevation: {source_point_elevation}\n")
            ofile.write(f"dip direction azimuth (geographic North): {dip_direction_as_north_azimuth}\n")
            ofile.write(f"dip angle: {source_dip_angle}")

        QMessageBox.information(
            self,
            self.module_name,
            f"Processing parameters exported as file '{export_file_path}'"
        )
        return

    def load_analysis_parameters(self):

        try:

            input_file_path, _ = QFileDialog.getOpenFileName(
                self,
                self.tr("Load processing parameters"),
                "*.txt",
                "txt (*.txt *.TXT)"
            )

            if not input_file_path:
                QMessageBox.critical(
                    self,
                    self.module_name,
                    "No defined file path"
                )
                return

            with open(input_file_path, "r") as ifile:
                raw_params = ifile.readlines()

            params = list(map(lambda val: val.split(":")[-1].strip(), raw_params))

            self.lock_z_value_to_dem_surface_QCheckBox.setChecked(False)

            self.source_point_longitude_QLineEdit.setText(params[0])
            self.source_point_longitude = float(params[0])
            self.source_point_latitude_QLineEdit.setText(params[1])
            self.source_point_latitude = float(params[1])
            self.source_point_elevation_QLineEdit.setText(params[2])
            self.source_point_elevation = float(params[2])

            self.direction_azimuth_is_relative_to_geographic_north_QRadioButton.setChecked(True)
            self.dip_direction_QDoubleSpinBox.setValue(float(params[3]))
            self.dip_direction_as_north_azimuth = float(params[3])
            self.dip_angle_QDoubleSpinBox.setValue(float(params[4]))
            self.source_dip_angle = float(params[4])

            self.update_source_point_location()

            QMessageBox.information(
                self,
                self.module_name,
                "Parameters loaded"
            )
            return

        except Exception as e:

            QMessageBox.critical(
                self,
                self.module_name,
                f"Error while reading input processing parameters from file:\n{e!r}"
            )
            return

    def set_source_point_in_map(self):

        try:
            self.map_tool.canvasClicked.disconnect(self.update_source_point_location)
        except Exception:
            logger.debug("map_tool.canvasClicked was not connected", exc_info=True)

        self.store_current_map_crs()

        # mouse listener
        self.map_tool = EmitPointFromCanvas(
            self.canvas,
            self.plugin
        )

        # save the standard map tool for restoring it at the end
        self.previous_tool = self.canvas.mapTool()
        self.map_tool.canvasClicked.connect(self.update_source_point_location)
        self.map_tool.setCursor(Qt.CursorShape.CrossCursor)
        self.canvas.setMapTool(self.map_tool)

    def get_chosen_dem(self, ndx_dem_file = 0):

        self.qgis_grid_layer = None
        self.reset_post_dem_change()
        if self.qgis_raster_layers is None or len(self.qgis_raster_layers) == 0:
            return          
                                
        # no DEM layer defined  
        if ndx_dem_file == 0:
            return             

        self.qgis_grid_layer = self.qgis_raster_layers[ndx_dem_file - 1]

        dem_src = self.qgis_grid_layer.source()

        results, err = read_raster_band(
            raster_source=dem_src)

        if err:
            QMessageBox.critical(
                self,
                self.module_name,
                f"Exception while reading DEM content:\n{err!r}"
            )
            return

        geotransform, wkt_crs, _, data = results

        self.elevation_grid = Grid(
            array=data,
            geotransform=geotransform,
            crs=Crs(wkt_crs),
        )

        self.set_source_point_in_map_QPushButton.setEnabled(True)
        self.reset_source_point_in_map_QPushButton.setEnabled(True)

    def create_marker(
        self,
            canvas,
            prj_crs_x,
            prj_crs_y,
            pen_width=2,
            icon_type=2,
            icon_size=18,
            icon_color: Union[str, QColor] ='limegreen'
    ) -> QgsVertexMarker:
        
        marker = QgsVertexMarker(canvas)
        marker.setIconType(icon_type)
        marker.setIconSize(icon_size)
        marker.setPenWidth(pen_width)
        if isinstance(icon_color, QColor):
            color = icon_color
        else:
            color = QColor(icon_color)
        marker.setColor(color)
        marker.setCenter(QgsPointXY(prj_crs_x, prj_crs_y))

        return marker        

    def update_marker_in_canvas(self) -> bool:

        try:

            self.remove_intersection_markers_from_canvas()
            self.remove_source_point_marker_from_canvas()

            if self.source_point_longitude is not None and isfinite(self.source_point_longitude) and \
                    self.source_point_latitude is not None and isfinite(self.source_point_latitude):

                color = QColor("blue")

                result, err = qgis_project_coords(
                    x=self.source_point_longitude,
                    y=self.source_point_latitude,
                    dest_crs=self.map_crs,
                )

                if err:
                    QMessageBox.critical(
                        self,
                        self.module_name,
                        f"Exception while updating marker:\n{err!r}"
                    )
                    return True

                source_pt_x, source_pt_y = result

                source_point_marker = self.create_marker(
                    self.canvas,
                    source_pt_x,
                    source_pt_y,
                    icon_type=1,
                    icon_color=color)

                self.source_point_marker_list = [source_point_marker]

            self.canvas.refresh()

            return False

        except Exception as e:

            QMessageBox.critical(
                self,
                self.module_name,
                f"Exception while updating marker:\n{err!r}"
            )

            return True

    def update_source_point_location(
        self,
        qgs_point=None,
    ):

        try:

            self.reset_source_point_in_map_QPushButton.setEnabled(True)

            if self.sender() == self.map_tool:

                result, err = qgis_project_coords(
                    x=qgs_point.x(),
                    y=qgs_point.y(),
                    src_crs=self.map_crs,
                )

                if err:
                    QMessageBox.critical(
                        self,
                        self.module_name,
                        f"{err!r}"
                    )
                    return

                self.source_point_longitude, self.source_point_latitude = result

                self.source_point_longitude_QLineEdit.setText(str(self.source_point_longitude))
                self.source_point_latitude_QLineEdit.setText(str(self.source_point_latitude))

            elif self.sender() == self.source_point_longitude_QLineEdit:

                if self.source_point_longitude_QLineEdit.text() == '':
                    QMessageBox.critical(
                        self,
                        self.module_name,
                        "Error: longitude value is undefined"
                    )
                    return

                try:

                    self.source_point_longitude = float(self.source_point_longitude_QLineEdit.text())

                except Exception as e:

                    QMessageBox.critical(
                        self,
                        self.module_name,
                        f"Longitude value is not correctly defined:\n{e!r}"
                    )
                    return

            elif self.sender() == self.source_point_latitude_QLineEdit:

                if self.source_point_latitude_QLineEdit.text() == '':
                    QMessageBox.critical(
                        self,
                        self.module_name,
                        "Error: latitude value is undefined"
                    )
                    return

                try:

                    self.source_point_latitude = float(self.source_point_latitude_QLineEdit.text())

                except Exception as e:
                    QMessageBox.critical(
                        self,
                        self.module_name,
                        f"Latitude value is not correctly defined:\n{e!r}"
                    )
                    return

            if self.lock_z_value_to_dem_surface_QCheckBox.isChecked():

                elevation_value_from_dem = self.update_source_point_elevation_value()

                if elevation_value_from_dem is None:
                    self.current_source_point_elevation_value = None
                    self.source_point_elevation_QLineEdit.setText("")
                else:
                    self.current_source_point_elevation_value = elevation_value_from_dem
                    self.source_point_elevation_QLineEdit.setText(str(self.current_source_point_elevation_value))
                    self.use_elevation_from_dem = True

            _ = self.update_marker_in_canvas()

        except Exception as e:

            QMessageBox.critical(
                self,
                self.module_name,
                f"Exception while updating source point location:\n{err!r}"
            )
            return

    def update_source_point_elevation_value (self):
        """
        Update z value.
        
        """

        try:

            # does nothing when the height source is not from DEM

            if not self.lock_z_value_to_dem_surface_QCheckBox.isChecked():
                return None

            # prevent action when the DEM is not read

            if self.elevation_grid is None:
                return None

            # get z value from ptLyr

            if self.source_point_longitude is None or self.source_point_latitude is None:
                return None

            result, err = qgis_project_coords(
                x=self.source_point_longitude,
                y=self.source_point_latitude,
                dest_crs=self.qgis_grid_layer.crs(),
            )

            if err:

                QMessageBox.critical(
                    self,
                    self.module_name,
                    f"Exception while updating z value:\n{err!r}"
                )

                return

            srcpt_demcrs_x, srcpt_demcrs_y = result

            # return None or a numeric value
            return self.elevation_grid.interpolate_bilinear(
                srcpt_demcrs_x,
                srcpt_demcrs_y)

        except Exception as e:

            QMessageBox.critical(
                self,
                self.module_name,
                f"Exception while updating z value:\n{err!r}"
            )

            return

    def restore_previous_map_tool(self,
                                  map_tool):

        if map_tool:
            self.canvas.unsetMapTool(map_tool)

        if self.previous_tool:
            self.canvas.setMapTool(self.previous_tool)

    def clear_source_point_qlineedit_forms(self):
        
        for qlineedit_form in (
                self.source_point_longitude_QLineEdit,
                self.source_point_latitude_QLineEdit,
                self.source_point_elevation_QLineEdit):

            qlineedit_form.clear()

    def update_dip_direction_slider(self):
        """
        Update the value of the dip direction in the slider.""
        """        
        transformed_dipdirection = self.dip_direction_QDoubleSpinBox.value() + 180.0
        if transformed_dipdirection > 360.0:
            transformed_dipdirection -= 360.0
        self.dip_direction_QDial.setValue(int(transformed_dipdirection))

    def update_dip_direction_spinbox(self):
        """
        Update the value of the dip direction in the spinbox.
        """        
        real_dipdirection = self.dip_direction_QDial.value() - 180.0
        if real_dipdirection < 0.0:
            real_dipdirection += 360.0
        self.dip_direction_QDoubleSpinBox.setValue(real_dipdirection)

    def update_dip_angle_slider(self):
        """
        Update the value of the dip angle in the slider.
        """
        
        self.DAngle_verticalSlider.setValue(int(self.dip_angle_QDoubleSpinBox.value()))

    def update_dip_angle_spinbox(self):
        """
        Update the value of the dip angle in the spinbox.
        """        
        
        self.dip_angle_QDoubleSpinBox.setValue(self.DAngle_verticalSlider.value())

    def check_input_source_point_parameters(self
                                            ) -> str:

        # check if all input data are correct

        if self.source_point_longitude_QLineEdit.text() == '':
            return "Define the longitude of the source point in 'Geographic parameters' section"

        if self.source_point_latitude_QLineEdit.text() == '':
            return "Define the latitude of the source point in 'Geographic parameters' section"

        if self.source_point_elevation_QLineEdit.text() == '':
            return "Define the elevation of the source point in 'Geographic parameters' section"

        return ''

    def extract_current_plane_parameters(self) -> Union[str, tuple]:

        try:

            source_point_longitude = float(self.source_point_longitude_QLineEdit.text())
            source_point_latitude = float(self.source_point_latitude_QLineEdit.text())
            source_point_elevation = float(self.source_point_elevation_QLineEdit.text())
            source_dip_direction = self.dip_direction_QDoubleSpinBox.value()
            source_dip_angle = self.dip_angle_QDoubleSpinBox.value()
            direction_azimuth_is_relative_to_map = self.direction_azimuth_is_relative_to_map_top_QRadioButton.isChecked()

            # Calculates dip direction correction with respect to map CRS top (y-axis) direction

            if direction_azimuth_is_relative_to_map:

                geographic_north_direction_azimuth_cw, err = calculate_local_geographic_north_azimuth_from_epsg_4326(
                    longitude=source_point_longitude,
                    latitude=source_point_latitude,
                    planar_qgis_crs=self.map_crs,
                )

                if err:
                    return None, err

                dip_direction_as_north_azimuth = (source_dip_direction - geographic_north_direction_azimuth_cw) % 360.0

            else:

                dip_direction_as_north_azimuth = source_dip_direction % 360.0

            return (
                source_point_longitude,
                source_point_latitude,
                source_point_elevation,
                dip_direction_as_north_azimuth,
                source_dip_angle,
            )

        except Exception as e:

            return f"Exception while extracting analysis parameters:\n{e!r}"


    def preprocess_plane_and_source_point(
        self,
        dest_crs: 'QGIS_CRS',
    ) -> Tuple[Union[None, Tuple[Plane, Point]], Error]:

        try:

            result = self.extract_current_plane_parameters()

            if isinstance(result, str):
                return None, Error(
                    True,
                    caller_name(),
                    Exception(result),
                    traceback.format_exc(),
                )

            source_point_longitude = result[0]
            source_point_latitude = result[1]
            source_point_elevation = result[2]
            dip_direction_as_north_azimuth = result[3]
            source_dip_angle = result[4]

            # Correct dip direction and angle by DEM crs

            dest_crs_plane_orientation, err = convert_plane_orientation_to_crs(
                dip_direction_as_north_azimuth=dip_direction_as_north_azimuth,
                source_dip_angle=source_dip_angle,
                local_pt_longitude=source_point_longitude,
                local_pt_latitude=source_point_latitude,
                dest_crs=dest_crs,
            )

            if err:
                return None, err

            result, err = qgis_project_coords(
                x=source_point_longitude,
                y=source_point_latitude,
                dest_crs=dest_crs,
            )

            if err:
                return None, err

            dest_crs_src_pt_x, dest_crs_src_pt_y = result

            dest_crs_src_pt_3d = Point(
                dest_crs_src_pt_x,
                dest_crs_src_pt_y,
                source_point_elevation,
            )

            return (
                dest_crs_plane_orientation,
                dest_crs_src_pt_3d
            ), Error()

        except Exception as e:

            return None, Error(
                True,
                caller_name(),
                e,
                traceback.format_exc(),
            )

    def get_distances_dem(self) -> Union[str, Tuple[Grid, 'QGIS_CRS']]:

        try:

            if self.qgis_raster_layers_for_elevations is None or len(self.qgis_raster_layers_for_elevations) == 0:
                return "No raster layer is available in current project"

            current_raster_layers_index = self.choose_distances_dem_QComboBox.currentIndex()

            # no DEM layer defined
            if current_raster_layers_index == 0:
                return "No raster layer chosen"

            dem_layer = self.qgis_raster_layers_for_elevations[current_raster_layers_index - 1]
            dem_crs = dem_layer.crs()
            dem_src = dem_layer.source()

            results, err = read_raster_band(
                raster_source=dem_src)

            if err:
                return f"Exception while reading DEM file:\n{err!r}"

            geotransform, wkt_crs, _, data = results

            distances_grid = Grid(
                array=data,
                geotransform=geotransform,
                crs=Crs(wkt_crs),
            )

            return distances_grid, dem_crs

        except Exception as e:

            return f"Exception while reading DEM for distances:\n{e!r}"

    def calculate_distances_of_dem_centers_from_plane(self):
        """
        Calculate distances of input DEM cell centers from geological plane.
        """

        try:

            # check validity of geographical and geological input parameters

            err_msg = self.check_input_source_point_parameters()

            if err_msg:
                QMessageBox.critical(
                    self,
                    self.module_name,
                    err_msg
                )
                return

            # extracting plane parameters (source point plus plane orientation)

            current_plane_parameters = self.extract_current_plane_parameters()
            if isinstance(current_plane_parameters, str):
                warn_qgis(
                    self.module_name,
                    current_plane_parameters
                )
                return

            source_point_longitude, \
                source_point_latitude, \
                source_point_elevation, \
                dip_direction_as_north_azimuth, \
                source_dip_angle = current_plane_parameters

            # extract the input DEM for distances calculation

            result = self.get_distances_dem()

            if isinstance(result, str):
                QMessageBox.warning(
                    self,
                    self.module_name,
                    result
                )
                return

            distances_grid, dem_crs = result
            output_grid_path = self.output_grid_path_input_QLineEdit.text()

            # source point

            result, err = self.preprocess_plane_and_source_point(
                dest_crs=dem_crs
            )

            if err:
                QMessageBox.critical(
                    self,
                    self.module_name,
                    f"Exception while preprocessing plane and source point:\n{err!r}"
                )
                return

            geological_plane_in_dem_crs, source_point_in_dem_crrs = result

            # define distance array limits

            below_threshold = float(self.lower_threshold_QDoubleSpinBox.value())
            above_threshold = float(self.above_thresold_QDoubleSpinBox.value())

            if below_threshold >= above_threshold:
                QMessageBox.critical(
                    self,
                    self.module_name,
                    f"Below threshold {below_threshold} is not lower than upper threshold {above_threshold}"
                )
                return

            # populate array with distances from DEM

            cplane = CPlane3D.from_geol_plane(
                geol_plane=geological_plane_in_dem_crs,
                pt=source_point_in_dem_crrs
            )

            arr = distances_grid.array
            num_rows, num_cols = arr.shape
            distances = np.zeros((num_rows, num_cols)) * np.nan

            for i in range(num_rows):
                for j in range(num_cols):
                    val = arr[i, j]
                    if not np.isfinite(val):
                        continue
                    cc_pt_x, cc_pt_y = distances_grid.ijArrToxy(i, j)
                    dem_cell_center = Point(
                        cc_pt_x,
                        cc_pt_y,
                        val
                    )

                    distance = cplane.signed_distance_to_point(dem_cell_center)
                    if below_threshold <= distance <= above_threshold:
                        distances[i, j] = distance

            err = write_geotiff(
                file_path=output_grid_path,
                arr_out=distances,
                geotransform=GeoTransform.to_gdal_geotransform(distances_grid.geotransform),
                wkt_crs=distances_grid.wkt_crs
            )

            if err:
                QMessageBox.critical(
                    self,
                    self.module_name,
                    f"Exception while writing geotiff:\n{err!r}"
                )
                return

            if self.load_output_grid_in_map_QCheckBox.isChecked():

                try:

                    distances_grid_layer = QgsRasterLayer(
                        output_grid_path,
                        QFileInfo(output_grid_path).baseName()
                    )
                    QgsProject.instance().addMapLayer(distances_grid_layer)

                    if distances_grid_layer.isValid():
                        pass
                    else:
                        QMessageBox.critical(
                            self,
                            self.module_name,
                            "Unable to read basename and file path.\nOutput string is possibly invalid"
                        )

                except Exception as e:

                    QMessageBox.critical(
                        self,
                        self.module_name,
                        f"Unable to load grid layer in project:\n{e!r}"
                    )
                    return

            QMessageBox.information(
                self,
                self.module_name,
                "Distance grid created"
            )

        except Exception as e:

            QMessageBox.critical(
                self,
                self.module_name,
                f"Exception while calculating distance grid:\n{e!r}")
            return

    def define_output_params_file_path(self):

        output_filename, _ = QFileDialog.getSaveFileName(
            self,
            self.tr("Save processing parameters"),
            "*.txt",
            "txt (*.txt *.TXT)"
        )

        if not output_filename:
            return

        self.export_params_file_path_QLineEdit.setText(output_filename)

    def select_output_grid_file(self):

        output_filename, __ = QFileDialog.getSaveFileName(
            self,
            self.tr("Save grid as geotiff"),
            "*.tif",
            "tif (*.tif *.TIF)"
        )

        if not output_filename:
            return

        self.output_grid_path_input_QLineEdit.setText(output_filename)


