"""
/***************************************************************************
 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.                                   *
 *                                                                         *
 ***************************************************************************/
"""

# The "delete_selected_records_from_db" method
# contains code modified from (chp. 15) assetmanager.pyw by Summerfield

#!/usr/bin/env python
# Copyright (c) 2007-8 Qtrac Ltd. All rights reserved.
# This program or module 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 2 of the License, or
# version 3 of the License, or (at your option) any later version. It is
# provided for educational purposes and is distributed in the hope that
# it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
# the GNU General Public License for more details.

import logging
import os
from math import sqrt, log10
from typing import Optional, List, Dict, Union

from datetime import datetime as dt

logger = logging.getLogger(__name__)


try:
    from osgeo import ogr
except ImportError:
    import ogr

from qgis.PyQt import sip

from qgis.core import QgsProject
from qgis.PyQt.QtCore import QSignalBlocker

from qgis.PyQt.uic import loadUi
from qgis.PyQt.QtSql import *

from geogst.core.geometries.grids.rasters import *
from geogst.core.geometries.general.planes import *
from geogst.core.geology.orientations import *
from geogst.external_wrappers.yaml.io import *
from geogst.io.rasters.gdal import *
from geogst.io.vectors.ogr import *
from geogst.core.geometries.calculations.best_fit_plane import calculate_best_fit_plane_from_coordinates


from ..apps.gui.mpl_widget import *

from ..apps.qgis.canvas import *
from ..apps.qgis.lines import *
from ..apps.qgis.messages import *
from ..apps.qgis.orientations import *
from ..apps.qgis.polygons import *
from ..apps.qgis.project import *
from ..apps.qgis.projections import *
from ..apps.qt.databases import *
from ..apps.qt.filesystem import *
from ..apps.qt.messages import *
from ..apps.sqlite.sqlite3 import *

from ..configurations.general_params import *
from ..configurations.messages import *
from ..configurations.minor_params import *

from .config.indices import *
from .config.queries import *
from .config.constants import UIConstants

bfp_texts_file_name = "texts.yaml"
parameters_file_name = "parameters.yaml"



def calculate_signed_distance(
    dx: numbers.Real,
    dy: numbers.Real,
    dz: numbers.Real,
) -> Union[None, numbers.Real]:

    if dx is None or dy is None or dz is None:
        distance = None
    else:
        distance = sqrt(dx * dx + dy * dy + dz * dz)
        is_below = dz < 0.0
        if is_below:
            distance = - distance

    return distance


def check_table_presence_in_sqlite(
        curs,
        table_nm):

    try:

        check_query_str = check_query_template.format(
            table_name=table_nm)

        curs.execute(check_query_str)

        return curs.fetchone()[0]

    except Exception:

        return False


def are_xy_pairs_equal(xy_pair_1, xy_pair_2):

    if xy_pair_1[0] == xy_pair_2[0] and xy_pair_1[1] == xy_pair_2[1]:
        return True

    return False


def get_field_dict(key_val, flds_dicts):

    filt_dicts = list(filter(lambda dct: key_val in dct.keys(), flds_dicts))

    if len(filt_dicts) == 1:
        return filt_dicts[0][key_val]
    else:
        return None


def parse_db_params(sqlite_params):

    tables = sqlite_params["tables"]

    solutions_pars = tables["solutions"]
    src_points_pars = tables["src_pts"]

    solutions_tbl_nm = solutions_pars["name"]
    solutions_tbl_flds = solutions_pars["fields"]

    src_points_tbl_nm = src_points_pars["name"]
    src_points_tbl_flds = src_points_pars["fields"]

    return (
        solutions_tbl_nm,
        solutions_tbl_flds,
        src_points_tbl_nm,
        src_points_tbl_flds)


def remove_equal_consecutive_xypairs(xy_list):

    out_xy_list = [xy_list[0]]

    for n in range(1, len(xy_list)):
        if not are_xy_pairs_equal(xy_list[n], out_xy_list[-1]):
            out_xy_list.append(xy_list[n])

    return out_xy_list


def convert_list3_to_list(
        list3: List
) -> List:
    """
    input: a list of list of (x,y) tuples
    output: a list of (x,y) tuples
    """

    out_list = []
    for list2 in list3:
        for list1 in list2:
            out_list += list1

    return out_list


class BestFitPlaneWidget(QWidget):

    signal_for_updating_inputs = pyqtSignal()

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

        super(BestFitPlaneWidget, self).__init__()

        self.plugin_folder = plugin_folder
        self.module_name = module_name

        self.canvas = canvas
        self.plugin = plugin_qaction

        self.config_folder_path = os.path.join(
            self.plugin_folder,
            config_folder_name)

        self.db_tables_params = db_tables_params

        self.initialize_parameters()
        self.setup_gui()
        #self.destroyed.connect(lambda *_: self._restore_previous_map_tool())

        self.signal_for_updating_inputs.connect(self.update_input_points_management_buttons)

    def initialize_parameters(self):

        bfp_text_config_file_path = os.path.join(
            self.plugin_folder,
            config_folder_name,
            bfp_texts_file_name)

        texts_params = read_yaml(bfp_text_config_file_path)

        self.not_set_text = texts_params["not_set_text"]

        parameters_file_path = os.path.join(
            self.plugin_folder,
            config_folder_name,
            parameters_file_name)

        parameters = read_yaml(
            file_pth=parameters_file_path
        )

        self.max_number_of_input_points = parameters["bfp_num_max_input_points"]

        self.reset_dem_input_states()

        self.previous_map_tool = None
        self.crs_of_initial_input_point = None
        self.best_fit_plane_point_markers = []
        self.res_id = 0
        self.input_points_parameters = []
        self.curr_crs_best_fit_geological_plane = None
        self.sol_tbl_flds = None
        self.selection_model = None

        self._qt_conn_name = f"qgsurf_bfp_{id(self)}"
        
        # Flags to track signal-slot connections
        self._raster_layer_signals_connected = False
        self._xy_layer_signals_connected = False
        self._xyz_layer_signals_connected = False

    def reset_dem_input_states(self):
        
        self.dem, self.geoarray = None, None

    def setup_gui(self):

        dialog_layout = QVBoxLayout()

        self.main_widget = QTabWidget()

        self.main_widget.addTab(self.setup_processing_tab(), "Processing")
        self.main_widget.addTab(self.setup_results_tab(), "Saved results")
        self.main_widget.addTab(self.setup_configurations_tab(), "Saved result database")
        self.main_widget.addTab(self.setup_help_tab(), "Help")

        #self.main_widget.currentChanged.connect(self.onTabChange)

        dialog_layout.addWidget(self.main_widget)

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

    '''
    @pyqtSlot()
    def onTabChange(self):

        if hasattr(self, "solutions_model_QSqlTableModel") and self.solutions_model_QSqlTableModel:
            self.solutions_view_QTableView.setModel(None)
            self.solutions_model_QSqlTableModel = None
            self.selection_model = None

        self.setup_results_tableview()
        self.solutions_view_QTableView.update()
    '''

    def setup_processing_tab(self):
        
        widget = QWidget()

        layout = QVBoxLayout()

        layout.addWidget(self.setup_2d_input_features())
        layout.addWidget(self.setup_3d_input_features())
        layout.addWidget(self.setup_listing_of_source_points())
        layout.addWidget(self.setup_main_processing())

        widget.setLayout(layout)

        return widget

    def setup_results_tab(self):

        widget = QWidget()

        layout = QVBoxLayout()

        layout.addWidget(self.setup_results_processings())

        widget.setLayout(layout)

        return widget

    def setup_configurations_tab(self):

        widget = QWidget()

        layout = QVBoxLayout()

        layout.addWidget(self.setup_results_configurations())

        widget.setLayout(layout)

        return widget

    def setup_help_tab(self):

        qwidget = QWidget()
        layout = QVBoxLayout()

        url_path = f"file:///{self.plugin_folder}/help/help_bfp.html"

        text_browser = QTextBrowser(qwidget)
        text_browser.setSource(QUrl(url_path))
        text_browser.setSearchPaths([f'{self.plugin_folder}/help/images/bfp'])

        layout.addWidget(text_browser)

        qwidget.setLayout(layout)

        return qwidget

    def setup_results_tableview(self):

        result_db_path = self.path_of_result_db_QLineEdit.text()

        if result_db_path:

            success, msg = try_connect_to_sqlite3_db_with_qt(
                result_db_path,
                self._qt_conn_name,
            )

            if not success:
                warn_qt(self, self.module_name, msg)
                return

            db = QSqlDatabase.database(self._qt_conn_name)
            self.solutions_model_QSqlTableModel = QSqlTableModel(self.solutions_view_QTableView, db)
            #self.solutions_model_QSqlTableModel = QSqlTableModel(db=db)

            db_parameters = parse_db_params(self.db_tables_params)

            self.solutions_tblnm, self.sol_tbl_flds, self.pts_tbl_nm, self.pts_tbl_flds = db_parameters
            #self.solutions_model_QSqlTableModel = QSqlTableModel(db=QSqlDatabase.database())
            self.solutions_model_QSqlTableModel.setTable(self.solutions_tblnm)

            self.solutions_model_QSqlTableModel.setHeaderData(ID_SOL, Qt.Orientation.Horizontal, "id")
            self.solutions_model_QSqlTableModel.setHeaderData(DIP_DIR, Qt.Orientation.Horizontal, "dip direction")
            self.solutions_model_QSqlTableModel.setHeaderData(DIP_ANG, Qt.Orientation.Horizontal, "dip angle")
            self.solutions_model_QSqlTableModel.setHeaderData(DATASET, Qt.Orientation.Horizontal, "dataset")
            self.solutions_model_QSqlTableModel.setHeaderData(NOTES, Qt.Orientation.Horizontal, "notes")
            self.solutions_model_QSqlTableModel.setHeaderData(MPT_LON, Qt.Orientation.Horizontal, "mean point - lon")
            self.solutions_model_QSqlTableModel.setHeaderData(MPT_LAT, Qt.Orientation.Horizontal, "mean point - lat")
            self.solutions_model_QSqlTableModel.setHeaderData(MPT_Z, Qt.Orientation.Horizontal, "mean point - z")
            self.solutions_model_QSqlTableModel.setHeaderData(CREAT_TIME, Qt.Orientation.Horizontal, "created")

            self.solutions_model_QSqlTableModel.select()

            self.solutions_view_QTableView.setModel(self.solutions_model_QSqlTableModel)
            self.solutions_view_QTableView.setSelectionMode(QTableView.SelectionMode.MultiSelection)
            self.solutions_view_QTableView.setSelectionBehavior(QTableView.SelectionBehavior.SelectRows)
            self.solutions_view_QTableView.verticalHeader().hide()
            self.solutions_view_QTableView.resizeColumnsToContents()

            self.solutions_view_QTableView.resizeRowsToContents()
            self.solutions_view_QTableView.setSortingEnabled(True)

            self.selection_model = self.solutions_view_QTableView.selectionModel()

    def setup_results_processings(self):

        self.results_widget = QWidget()

        layout = QVBoxLayout()

        plot_selected_recs_QPushButton = QPushButton("Plot selected records")
        plot_selected_recs_QPushButton.clicked.connect(self.plot_selected_records_in_db)
        layout.addWidget(plot_selected_recs_QPushButton)

        delete_selected_recs_QPushButton = QPushButton("Delete selected records")
        delete_selected_recs_QPushButton.clicked.connect(self.delete_selected_records_from_db)
        layout.addWidget(delete_selected_recs_QPushButton)

        export_selected_records_QPushButton = QPushButton("Export selected records")
        export_selected_records_QPushButton.clicked.connect(self.export_selected_records_in_shapefile)
        layout.addWidget(export_selected_records_QPushButton)

        self.solutions_view_QTableView = QTableView()

        self.solutions_view_QTableView.show()

        layout.addWidget(self.solutions_view_QTableView)

        self.results_widget.setLayout(layout)

        return self.results_widget

    def setup_2d_input_features(self):

        groupbox = QGroupBox(self.tr("Elevations from DEM and locations from features (points/lines/polygons)"))
        
        layout = QGridLayout()
    
        layout.addWidget(QLabel("Get z values from DEM layer:"), 0, 0, 1, 1)

        self.define_dem_QComboBox = QComboBox()
        self.define_dem_QComboBox.addItem(self.not_set_text)

        layout.addWidget(self.define_dem_QComboBox, 0, 1, 1, 1)

        self.refresh_raster_layer_list()

        # Disconnect before connecting to avoid multiple connections
        if self._raster_layer_signals_connected:
            try:
                QgsProject.instance().layerWasAdded.disconnect(self.refresh_raster_layer_list)
            except (TypeError, RuntimeError):
                pass
            try:
                QgsProject.instance().layerRemoved.disconnect(self.refresh_raster_layer_list)
            except (TypeError, RuntimeError):
                pass
            self._raster_layer_signals_connected = False
        
        QgsProject.instance().layerWasAdded.connect(self.refresh_raster_layer_list)
        QgsProject.instance().layerRemoved.connect(self.refresh_raster_layer_list)
        self._raster_layer_signals_connected = True

        self.define_source_points_xy_in_map_QPushButton = QPushButton("Define input points x-y coordinates in map (3 or more points)")
        self.define_source_points_xy_in_map_QPushButton.clicked.connect(self.initialize_point_inputs_xy_from_map)
        layout.addWidget(self.define_source_points_xy_in_map_QPushButton, 1, 0, 1, 2)

        self.get_source_points_xy_from_layer_QPushButton = QPushButton("Get input points x-y coordinates from layer:")
        self.get_source_points_xy_from_layer_QPushButton.clicked.connect(self.add_points_to_listing_from_layer_xy)
        layout.addWidget(self.get_source_points_xy_from_layer_QPushButton, 2, 0, 1, 1)

        self.input_points_xy_source_layer_QComboBox = QComboBox()
        layout.addWidget(self.input_points_xy_source_layer_QComboBox, 2, 1, 1, 1)

        self.refresh_layer_xy_list_in_combobox()
        
        # Disconnect before connecting to avoid multiple connections
        if self._xy_layer_signals_connected:
            try:
                QgsProject.instance().layerWasAdded.disconnect(self.refresh_layer_xy_list_in_combobox)
            except (TypeError, RuntimeError):
                pass
            try:
                QgsProject.instance().layerRemoved.disconnect(self.refresh_layer_xy_list_in_combobox)
            except (TypeError, RuntimeError):
                pass
            self._xy_layer_signals_connected = False
        
        QgsProject.instance().layerWasAdded.connect(self.refresh_layer_xy_list_in_combobox)
        QgsProject.instance().layerRemoved.connect(self.refresh_layer_xy_list_in_combobox)
        self._xy_layer_signals_connected = True

        groupbox.setLayout(layout)
              
        return groupbox

    def setup_3d_input_features(self):

        groupbox = QGroupBox(self.tr("Elevations and locations from features (points with z field)"))

        layout = QGridLayout()

        self.get_source_points_xyz_from_layer_QPushButton = QPushButton("Get input points x-y-z coordinates from layer:")
        self.get_source_points_xyz_from_layer_QPushButton.clicked.connect(self.add_points_to_listing_from_layer_xyz)

        layout.addWidget(self.get_source_points_xyz_from_layer_QPushButton, 0, 0, 1, 1)

        self.input_points_xyz_source_layer_QComboBox = QComboBox()
        self.input_points_xyz_source_layer_QComboBox.currentIndexChanged[int].connect(self.update_load_xyz_points_button)
        layout.addWidget(self.input_points_xyz_source_layer_QComboBox, 0, 1, 1, 1)

        self.refresh_layer_xyz_list_in_combobox()
        
        # Disconnect before connecting to avoid multiple connections
        if self._xyz_layer_signals_connected:
            try:
                QgsProject.instance().layerWasAdded.disconnect(self.refresh_layer_xyz_list_in_combobox)
            except (TypeError, RuntimeError):
                pass
            try:
                QgsProject.instance().layerRemoved.disconnect(self.refresh_layer_xyz_list_in_combobox)
            except (TypeError, RuntimeError):
                pass
            self._xyz_layer_signals_connected = False
        
        QgsProject.instance().layerWasAdded.connect(self.refresh_layer_xyz_list_in_combobox)
        QgsProject.instance().layerRemoved.connect(self.refresh_layer_xyz_list_in_combobox)
        self._xyz_layer_signals_connected = True

        groupbox.setLayout(layout)

        return groupbox

    def update_load_xyz_points_button(self):

        if self.input_points_xyz_source_layer_QComboBox.currentText() == self.not_set_text:
            activated = False
        else:
            activated = True

        self.get_source_points_xyz_from_layer_QPushButton.setEnabled(activated)

    def setup_listing_of_source_points(self):
        
        groupbox = QGroupBox(self.tr("Source features"))
        
        layout = QGridLayout()

        layout.addWidget(QLabel("<i>point index -> longitude, latitude, elevation</i>"), 0, 0, 1, 2)
        self.listing_of_input_points_QListWidget = QListWidget()
        layout.addWidget(self.listing_of_input_points_QListWidget, 1, 0, 1, 2)

        self.reset_source_points_QPushButton = QPushButton("Reset all source features")
        self.reset_source_points_QPushButton.clicked.connect(self.reset_all_input_points)
        layout.addWidget(self.reset_source_points_QPushButton, 2, 0, 1, 2)

        self.enable_xy_input_points_buttons_and_comboboxes(False)

        groupbox.setLayout(layout)
                 
        return groupbox

    def setup_main_processing(self):

        groupbox = QGroupBox(self.tr("Best fit plane calculation"))

        layout = QGridLayout()

        self.calculate_best_fit_plane_QPushButton = QPushButton("Calculate best fit plane")
        self.calculate_best_fit_plane_QPushButton.clicked.connect(self.calculate_best_fit_plane_parameters)
        self.calculate_best_fit_plane_QPushButton.setEnabled(False)
        layout.addWidget(self.calculate_best_fit_plane_QPushButton, 0, 0, 1, 2)

        self.enable_xy_input_points_buttons_and_comboboxes(False)

        groupbox.setLayout(layout)

        return groupbox

    def setup_results_configurations(self):

        groupbox = QGroupBox(self.tr("Database for result storage (sqlite3)"))

        layout = QGridLayout()

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

        self.load_existing_sqlite_db_QPushButton = QPushButton("Load existing sqlite database")
        self.load_existing_sqlite_db_QPushButton.clicked.connect(self.load_existing_sqlite_db)
        layout.addWidget(self.load_existing_sqlite_db_QPushButton, 1, 0, 1, 2)

        self.create_new_sqlite_db_QPushButton = QPushButton("Create new sqlite database")
        self.create_new_sqlite_db_QPushButton.clicked.connect(self.create_result_sqlite_db)
        layout.addWidget(self.create_new_sqlite_db_QPushButton, 2, 0, 1, 2)

        groupbox.setLayout(layout)

        return groupbox

    def view_best_fit_plane_in_stereonet(self):
        """
        Plot plane solution in stereonet.

        :return: None
        """

        stereonet_dialog = SolutionStereonetDialog(
            module_name=self.module_name,
            curr_crs_oriented_plane=self.curr_crs_best_fit_geological_plane,
            central_point_epsg_4326=self.central_point_epsg4326,
            north_direct_plane=self.north_dir_best_fit_geological_plane,
            solution_sorted_eigenvalues=self.solution_sorted_eigenvalues,
            points=self.input_points_parameters,
            distances=self.points_ortho_distances_deltas,
            result_db_path=self.path_of_result_db_QLineEdit.text(),
            db_tables_params=self.db_tables_params,
            prj_crs=self.get_project_crs_long_descr()
        )
        stereonet_dialog.saved.connect(self.refresh_saved_results)
        stereonet_dialog.exec()

    def refresh_saved_results(self):
        if hasattr(self, "solutions_model_QSqlTableModel") and self.solutions_model_QSqlTableModel:
            self.solutions_model_QSqlTableModel.select()
            self.solutions_view_QTableView.resizeColumnsToContents()

    def create_result_sqlite_db(self):

        db_path = define_path_new_file(
            parent=self,
            show_msg="Create sqlite3 db",
            path=".",
            filter_text="(*.*);;(*)")

        if not db_path:
            return

        tables = self.db_tables_params["tables"]

        solutions_pars = tables["solutions"]
        src_points_pars = tables["src_pts"]

        success, msg = try_create_db_tables(
            db_path=db_path,
            tables_pars=[
                solutions_pars,
                src_points_pars])

        if success:

            self.path_of_result_db_QLineEdit.setText(db_path)

            self.setup_results_tableview()

            info_qgis(
                self.module_name,
                "Result db created")

        else:

            warn_qt(self, self.module_name, msg)

    def load_existing_sqlite_db(self):

        db_path = old_file_path(
            parent=self,
            show_msg="Open sqlite3 db",
            path=".",
            filter_text="(*.*);;(*)")

        if not db_path:
            return

        conn = None
        try:
            conn = sqlite3.connect(db_path)
            curs = conn.cursor()

            try:
                curs.execute(bfp_check_db_query)
            except Exception as e:
                warn_qt(
                    self,
                    self.module_name,
                    f"{e!r}"
                )
                return

            pars = parse_db_params(self.db_tables_params)
            sol_tbl_nm, sol_tbl_flds, pts_tbl_nm, pts_tbl_flds = pars

            tables_to_check = [sol_tbl_nm, pts_tbl_nm]

            tables_found = True
            for table in tables_to_check:
                if not check_table_presence_in_sqlite(curs, table):
                    warn_qt(
                        self,
                        self.module_name,
                        f"Table {table} not found in db {db_path}"
                    )
                    tables_found = False
                    break

                self.path_of_result_db_QLineEdit.setText(db_path)

                self.setup_results_tableview()

                info_qgis(
                    self.module_name,
                    "Result db set")
        finally:
            # Ensure connection is always closed
            if conn is not None:
                conn.close()

    def add_marker(self,
                   prj_crs_x,
                   prj_crs_y):

        marker = self.create_marker(self.canvas, prj_crs_x, prj_crs_y)       
        self.best_fit_plane_point_markers.append(marker)
        self.canvas.refresh()        

    def add_input_qgis_xy_point_to_listing(
        self,
        qgis_pt,
        add_marker=True
    ):

        if QgsProject.instance().crs().isGeographic():
            warn_qgis(
                self.module_name,
                "To use the BestFitPlane module the project CRS must be a planar projection"
            )
            return

        if self.crs_of_initial_input_point is None:

            self.crs_of_initial_input_point = QgsProject.instance().crs()

        elif QgsProject.instance().crs() != self.crs_of_initial_input_point:

            warn_qgis(
                self.module_name,
                "The project crs is not equal to that of the first inserted point. Reset points or restore project crs"
            )
            return


        prj_crs_x, prj_crs_y = qgis_pt.x(), qgis_pt.y()

        dem_crs_coord_x, dem_crs_coord_y = self.project_from_map_to_dem_crs(prj_crs_x, prj_crs_y)

        dem_z_value = self.geoarray.interpolate_bilinear(dem_crs_coord_x, dem_crs_coord_y)

        if dem_z_value is None:
            return

        if add_marker:
            self.add_marker(prj_crs_x, prj_crs_y)

        result, err = qgis_project_coords(
            x=prj_crs_x,
            y=prj_crs_y,
            src_crs=self.get_map_crs(),
        )

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

        lon_4326, lat_4326 = result

        curr_ndx = len(self.input_points_parameters) + 1
        self.input_points_parameters.append([curr_ndx, prj_crs_x, prj_crs_y, dem_z_value, lon_4326, lat_4326])
        self.listing_of_input_points_QListWidget.addItem(f"{curr_ndx} -> {lon_4326:.7f}, {lat_4326:.7f}, {dem_z_value:.3f}")

        self.signal_for_updating_inputs.emit()

    def add_input_xyz_point_to_listing(
        self,
        pt,
        add_marker=True
    ):

        if QgsProject.instance().crs().isGeographic():
            warn_qgis(
                self.module_name,
                "To use the BestFitPlane module the project CRS must be a planar projection"
            )
            return

        if self.crs_of_initial_input_point is None:

            self.crs_of_initial_input_point = QgsProject.instance().crs()

        elif QgsProject.instance().crs() != self.crs_of_initial_input_point:

            warn_qgis(
                self.module_name,
                "The project crs is not equal to that of the first inserted point. Reset points or restore project crs"
            )
            return

        prj_crs_x, prj_crs_y, z = pt

        if z is None:
            return

        if add_marker:
            self.add_marker(prj_crs_x, prj_crs_y)

        result, err = qgis_project_coords(
            x=prj_crs_x,
            y=prj_crs_y,
            src_crs=self.get_map_crs(),
        )

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

        lon_4326, lat_4326 = result

        curr_ndx = len(self.input_points_parameters) + 1
        self.input_points_parameters.append([curr_ndx, prj_crs_x, prj_crs_y, z, lon_4326, lat_4326])
        self.listing_of_input_points_QListWidget.addItem(f"{curr_ndx} -> {lon_4326:.7f}, {lat_4326:.7f}, {z:.3f}")

        self.signal_for_updating_inputs.emit()

    def reset_all_input_points(self):
        
        self.reset_point_input_values()
        self.crs_of_initial_input_point = None
        self.signal_for_updating_inputs.emit()

    def initialize_point_inputs_xy_from_map(self):

        if QgsProject.instance().crs().isGeographic():
            warn_qgis(
                self.module_name,
                "To use the BestFitPlane module, you need to set the project CRS to a planar projection"
            )
            return

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

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

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

    def add_points_to_listing_from_layer_xy(self):
        
        # get vector layer
        
        try:

            if not self.input_points_xy_source_layer_QComboBox.currentIndex() > 0:
                warn_qt(self, self.module_name, "Check chosen input layer")
                return

            source_layer_ndx = self.input_points_xy_source_layer_QComboBox.currentIndex() - 1
            source_layer = self.vector_layers[source_layer_ndx]

        except:

            warn_qt(self, self.module_name, "Check chosen input layer")
            return

        # read xy tuples from layer (with consecutive duplicates removed)
        # Initialize xy_pair_list to avoid UnboundLocalError
        xy_pair_list = []

        layer_geom_type = extract_geometry_type_of_vector_layer(source_layer)

        if layer_geom_type == 'point':

            xy_list = read_qgis_pt_layer(source_layer)

            if not xy_list:
                warn_qt(
                    self,
                    self.module_name,
                    "Is chosen layer empty?"
                )
                return

            # Convert xy_list (which contains [x, y] or [x, y, z]) to xy_pair_list (only x, y)
            xy_pair_list = [(pt[0], pt[1]) for pt in xy_list]

        elif layer_geom_type == 'line':

            xypair_list3 = extract_selected_lines_2d_with_attributes_from_layer_multipart(source_layer)

            if not xypair_list3:
                warn_qt(
                    self,
                    self.module_name,
                    "Is chosen layer empty?"
                )
                return

            xypair_list3_1 = [xypair_list02[0] for xypair_list02 in xypair_list3 ]
            xy_pair_list = convert_list3_to_list(xypair_list3_1)

        elif layer_geom_type == 'polygon':

            xypair_list2 = polygon_geometries(source_layer)

            if not xypair_list2:
                warn_qt(
                    self,
                    self.module_name,
                    "Is chosen layer empty?"
                )
                return

            xy_pair_list = [xy_pair for xy_pair_list in xypair_list2 for xy_pair in xy_pair_list]

        else:

            warn_qt(
                self,
                self.module_name,
                "Geometry type of chosen layer is not point, line or polygon"
            )
            return

        # Ensure xy_pair_list is initialized before processing
        if not xy_pair_list:
            return

        xy_pair_list = remove_equal_consecutive_xypairs(xy_pair_list)

        if len(xy_pair_list) > self.max_number_of_input_points:
            warn_qt(self, self.module_name, f"More than {self.max_number_of_input_points} points to handle. Please use less features or modify value in configurations/parameters.yaml")
            return

        # for all xy tuples, project to project CRS as a qgis point

        crs_of_map = self.get_map_crs()
        list_of_qgis_points_with_map_crs = []
        for (x, y) in xy_pair_list:
            qgis_pt_in_map_crs = project_qgs_point(
                qgs_pt(x, y),
                source_layer.crs(),
                crs_of_map
            )
            list_of_qgis_points_with_map_crs.append(qgis_pt_in_map_crs)

        # for all qgs points, process them for the input point processing queue

        for qgis_pt_in_map_crs in list_of_qgis_points_with_map_crs:
            self.add_input_qgis_xy_point_to_listing(
                qgis_pt=qgis_pt_in_map_crs,
                add_marker=False
            )

    def add_points_to_listing_from_layer_xyz(self):

        # get point layer

        try:

            if not self.input_points_xyz_source_layer_QComboBox.currentIndex() > 0:
                warn_qt(self, self.module_name, "Check chosen input layer")
                return

            source_layer_ndx = self.input_points_xyz_source_layer_QComboBox.currentIndex() - 1
            source_layer = self.vector_layers[source_layer_ndx]

        except:

            warn_qt(self, self.module_name, "Check chosen input point layer")
            return

        # read xyz tuples from point layer (with consecutive duplicates removed)

        xyz_coords = read_qgis_pt_layer(
            pt_layer=source_layer,
            field_list=["z"])

        if not xyz_coords:
            warn_qt(
                self,
                self.module_name,
                "Is chosen layer empty?"
            )
            return

        xyz_coords = remove_equal_consecutive_xypairs(xyz_coords)

        if len(xyz_coords) > self.max_number_of_input_points:
            warn_qt(self, self.module_name,
                    f"More than {self.max_number_of_input_points} points to handle. Please use less features or modify value in configurations/parameters.yaml")
            return

        # for all xy tuples, project to project CRS as a qgis point

        crs_of_map = self.get_map_crs()
        list_of_xyz_points_with_map_crs = []
        for (x, y, z) in xyz_coords:
            qgis_pt_in_map_crs = project_qgs_point(
                qgs_pt(x, y),
                source_layer.crs(),
                crs_of_map
            )
            list_of_xyz_points_with_map_crs.append(
                Point(
                    qgis_pt_in_map_crs.x(),
                    qgis_pt_in_map_crs.y(),
                    z
                ))

        # for all qgs points, process them for the input point processing queue

        for xyz_pt_in_map_crs in list_of_xyz_points_with_map_crs:
            self.add_input_xyz_point_to_listing(
                pt=xyz_pt_in_map_crs,
                add_marker=False
            )

    def reset_input_point_states(self):
       
        self.reset_point_input_values()        
        self.disable_xy_point_tools()

        self.signal_for_updating_inputs.emit()

    def reset_point_input_values(self):

        self.reset_xy_point_markers()
        self.reset_general_point_inputs()

    def reset_general_point_inputs(self):

        self.listing_of_input_points_QListWidget.clear()
        self.input_points_parameters = []
        self.curr_crs_best_fit_geological_plane = None

    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 reset_xy_point_markers(self):

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

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

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

    def enable_xy_input_points_buttons_and_comboboxes(self, choice = True):
        
        self.define_source_points_xy_in_map_QPushButton.setEnabled(choice)
        self.get_source_points_xy_from_layer_QPushButton.setEnabled(choice)
        self.input_points_xy_source_layer_QComboBox.setEnabled(choice)
        #self.reset_source_points_QPushButton.setEnabled(choice)

    def disable_xy_point_tools(self):

        self.enable_xy_input_points_buttons_and_comboboxes(False)

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

        '''
        try: 
            self.disable_map_tool(self.emit_point_from_canvas)
        except: 
            pass         
        '''
        self._restore_previous_map_tool()

    def refresh_raster_layer_list(self):

        self.reset_dem_input_states()
        
        # Use QSignalBlocker to prevent signal emissions during combobox update
        with QSignalBlocker(self.define_dem_QComboBox):
            try:
                self.define_dem_QComboBox.currentIndexChanged[int].disconnect(self.get_working_dem)
            except Exception:
                logger.debug("define_dem_QComboBox.currentIndexChanged was not connected", exc_info=True)

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

            try:
                self.define_dem_QComboBox.clear()
            except:
                return
            
            self.define_dem_QComboBox.addItem(self.not_set_text)
                                      
            self.rasterLayers = loaded_raster_layers()                 
            if self.rasterLayers is None or len(self.rasterLayers) == 0:
                return
            for layer in self.rasterLayers: 
                self.define_dem_QComboBox.addItem(layer.name())
                            
            self.define_dem_QComboBox.currentIndexChanged[int].connect(self.get_working_dem)                

    def refresh_layer_xy_list_in_combobox(self):
        
        try:
            self.input_points_xy_source_layer_QComboBox.clear()
        except:
            return
        
        self.input_points_xy_source_layer_QComboBox.addItem(self.not_set_text)
                                  
        self.vector_layers = loaded_point_layers() + loaded_line_layers() + loaded_polygon_layers()

        if self.vector_layers is None or len(self.vector_layers) == 0:
            return

        for layer in self.vector_layers:
            self.input_points_xy_source_layer_QComboBox.addItem(layer.name())

    def refresh_layer_xyz_list_in_combobox(self):

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

        self.input_points_xyz_source_layer_QComboBox.addItem(self.not_set_text)

        self.point_layers = loaded_point_layers()

        if self.point_layers is None or len(self.point_layers) == 0:
            return

        for layer in self.point_layers:
            self.input_points_xyz_source_layer_QComboBox.addItem(layer.name())

    def get_working_dem(self, ndx_DEM_file = 0): 
        
        self.reset_dem_input_states()       
        self.reset_input_point_states()        
        if self.rasterLayers is None or len(self.rasterLayers) == 0: 
            return          
                                
        # no DEM layer defined

        if ndx_DEM_file == 0: 
            return             

        self.dem = self.rasterLayers[ndx_DEM_file-1]

        result, err = read_raster_band_with_projection(self.dem.source())
        if err:
            warn_qt(
                self,
                self.module_name,
                f"{err!r} "
            )
            return

        geotransform, epsg, _, data, _ = result

        self.geoarray = GeoArray(
            geotransform=geotransform,
            epsg_code=int(epsg),
            arrays=[data]
        )

        self.enable_xy_input_points_buttons_and_comboboxes()

    def coords_within_dem_bndr(self, dem_crs_coord_x, dem_crs_coord_y):
        
        if dem_crs_coord_x <= self.geoarray.xmin or dem_crs_coord_x >= self.geoarray.xmax or \
           dem_crs_coord_y <= self.geoarray.ymin or dem_crs_coord_y >= self.geoarray.ymax:
            return False        
        return True        

    def create_marker(self, canvas, prj_crs_x, prj_crs_y, pen_width=UIConstants.DEFAULT_PEN_WIDTH, icon_type=UIConstants.DEFAULT_ICON_TYPE, icon_size=UIConstants.DEFAULT_ICON_SIZE, icon_color=UIConstants.DEFAULT_ICON_COLOR):
        
        marker = QgsVertexMarker(canvas)
        marker.setIconType(icon_type)
        marker.setIconSize(icon_size)
        marker.setPenWidth(pen_width)
        marker.setColor(QColor(icon_color))
        marker.setCenter(QgsPointXY(prj_crs_x, prj_crs_y))
        return marker        

    def update_input_points_management_buttons(self):

        if self.listing_of_input_points_QListWidget.count () >= 1:
            state = True
        else:
            state = False

        self.reset_source_points_QPushButton.setEnabled(state)

        if self.listing_of_input_points_QListWidget.count () >= 3:
            state = True
        else:
            state = False

        self.calculate_best_fit_plane_QPushButton.setEnabled(state)

    def calculate_best_fit_plane_parameters(self):

        x_ndx, y_ndx, z_ndx = 1, 2, 3

        coordinates = list(
            map(
                lambda coord: (coord[x_ndx], coord[y_ndx], coord[z_ndx]),
                self.input_points_parameters
            )
        )

        result, err = calculate_best_fit_plane_from_coordinates(
            coordinates=coordinates,
        )

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

        self.curr_crs_best_fit_geological_plane, self.curr_crs_central_point, self.solution_sorted_eigenvalues = result

        result, err = qgis_project_coords(
            x=self.curr_crs_central_point.x,
            y=self.curr_crs_central_point.y,
            src_crs=self.get_map_crs(),
        )

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

        lon_4326, lat_4326 = result

        self.central_point_epsg4326 = Point(
            lon_4326,
            lat_4326,
            self.curr_crs_central_point.z
        )

        self.north_dir_best_fit_geological_plane, err = correct_plane_azimuth_to_geographic_north(
            src_crs_plane=self.curr_crs_best_fit_geological_plane,
            src_crs_local_pt=self.curr_crs_central_point,
            src_crs=self.get_map_crs(),
        )

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

        self.curr_crs_best_fit_cartesian_plane = CPlane3D.from_geol_plane(
            geol_plane=self.curr_crs_best_fit_geological_plane,
            pt=self.curr_crs_central_point)

        self.points_ortho_distances_deltas = []

        for x, y, z in coordinates:

            deltas, err = self.curr_crs_best_fit_cartesian_plane.deltas_with_point(
                pt=Point(x, y, z)
            )
            if err:
                delta_x, delta_y, delta_z = None, None, None
            else:
                delta_x, delta_y, delta_z = deltas

            self.points_ortho_distances_deltas.append((delta_x, delta_y, delta_z))

        self.view_best_fit_plane_in_stereonet()

    def disable_points_definition(self):
        
        self.enable_xy_input_points_buttons_and_comboboxes(False)

        self.disable_points_map_tool()
        self.reset_xy_point_markers()
        self.listing_of_input_points_QListWidget.clear()

    def disable_map_tool(self, map_tool):
                            
        try:
            if map_tool is not None:
                self.canvas.unsetMapTool(map_tool)
        except Exception:
            logger.debug("canvas.unsetMapTool failed", exc_info=True)

        try:
            if self.previous_map_tool is not None:
                self.canvas.setMapTool(self.previous_map_tool)
        except Exception:
            logger.debug("canvas.setMapTool(previous_map_tool) failed", exc_info=True)

    def disable_points_map_tool(self):
        
        self.disable_map_tool(self.emit_point_from_canvas)

    def get_map_crs(self) -> QgsCoordinateReferenceSystem:

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

    def get_project_crs_authid(self) -> str:

        return self.get_map_crs().authid()

    def get_project_crs_descr(self) -> str:

        return self.get_map_crs().description()

    def get_project_crs_long_descr(self) -> str:

        return f"{self.get_project_crs_authid()} - {self.get_project_crs_descr()}"

    def align_to_destination_crs(
        self,
        x,
        y,
        source_crs,
        dest_crs
    ):
        
        if source_crs != dest_crs:
            dest_crs_qgs_pt = project_qgs_point(qgs_pt(x, y), source_crs, dest_crs)
            return dest_crs_qgs_pt.x(), dest_crs_qgs_pt.y()
        else:
            return x, y

    def project_from_map_to_dem_crs(self, x, y):

        return self.align_to_destination_crs(
            x=x,
            y=y,
            source_crs=self.get_map_crs(),
            dest_crs=self.dem.crs()
        )

    def plot_selected_records_in_db(self):

        if not self.sol_tbl_flds:
            warn_qt(
                self,
                self.module_name,
                msg_database_missing
            )
            return

        # get relevant fields names

        id_alias = self.sol_tbl_flds[0]["id"]["name"]
        dip_dir_alias = self.sol_tbl_flds[1]["dip_dir"]["name"]
        dip_ang_alias = self.sol_tbl_flds[2]["dip_ang"]["name"]

        # get selected records attitudes

        selected_ids = get_selected_recs_ids(self.selection_model)

        # create query string

        if not selected_ids:
            query = query_solutions_all_template.format(
                dip_dir_alias,
                dip_ang_alias,
                self.solutions_tblnm)
        else:
            selected_ids_string = ",".join(map(str, selected_ids))
            query = query_solutions_selection_template.format(
                dip_dir_alias, dip_ang_alias, self.solutions_tblnm, id_alias, selected_ids_string)

        # query the database

        db = QSqlDatabase.database(self._qt_conn_name)
        results, err = try_execute_query_with_qt(
            query=query,
            db=db,
        )

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

        # get vals for selected records

        planes = []
        while results.next():
            dip_dir = float(results.value(0))
            dip_ang = float(results.value(1))
            planes.append(Plane(dip_dir, dip_ang))

        # plot in stereoplot

        selected_recs_stereonet_dialog = SelectedSolutionsStereonetDialog(
            module_name=self.module_name,
            planes=planes,
            parent=self)

        selected_recs_stereonet_dialog.show()
        selected_recs_stereonet_dialog.raise_()
        selected_recs_stereonet_dialog.activateWindow()

    def delete_selected_records_from_db(self):

        if not self.selection_model:
            warn_qt(
                self,
                self.module_name,
                msg_database_missing
            )
            return

        selected_ids = get_selected_recs_ids(self.selection_model)
        if not selected_ids:
            warn_qt(
                self,
                "Delete records",
                "No selected records")
            return

        num_sel_recs = len(selected_ids)
        if num_sel_recs == 0:
            warn_qt(
                self,
                "Delete records",
                "No selected records")
            return
        else:
            msg = "<font color=red>Delete {} record(s)?</font>".format(num_sel_recs)
            if QMessageBox.question(
                    self,
                    "Delete solution",
                    msg,
                    QMessageBox.StandardButton.Yes|QMessageBox.StandardButton.No
            ) == QMessageBox.StandardButton.No:
                return

        self.solutions_view_QTableView.setSortingEnabled(False)
        self.solutions_model_QSqlTableModel.beginResetModel()

        # create query string

        import re
        if not re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", self.pts_tbl_nm):
            raise ValueError("Nome tabella non valido: %r" % self.pts_tbl_nm)
        table = self.pts_tbl_nm

        ids = selected_ids           # gli id che vuoi cancellare
        placeholders = ",".join(["?"] * len(ids))
        query = QSqlQuery(db=QSqlDatabase.database(self._qt_conn_name))
        query.prepare(f"DELETE FROM {table} WHERE id_sol IN ({placeholders})")    # nosec B608
        for v in ids:
            query.addBindValue(v)
        query.exec()

        for index in self.solutions_view_QTableView.selectedIndexes():
            self.solutions_model_QSqlTableModel.removeRow(index.row())

        self.solutions_model_QSqlTableModel.submitAll()
        QSqlDatabase.database(self._qt_conn_name).commit()

        self.solutions_model_QSqlTableModel.endResetModel()

        self.solutions_view_QTableView.setSortingEnabled(True)
        self.solutions_view_QTableView.update()

    def extract_output_shapefile_parameters(self,
                                            geom_type="point"
                                            ):

        output_shapefile_params_file = os.path.join(
            self.plugin_folder,
            config_folder_name,
            output_shapefile_params_flnm)

        plugin_params = read_yaml(output_shapefile_params_file)

        if geom_type == "point":
            return plugin_params["pt_shapefile"]
        elif geom_type == "line":
            return plugin_params["ln_shapefile"]
        else:
            return None

    def export_selected_records_in_shapefile(self):
        """
        Esporta record selezionati in shapefile.
        Coordina il flusso: validazione, dialog, preparazione shapefile, query, export.
        """

        if not self.selection_model:
            warn_qt(
                self,
                self.module_name,
                msg_database_missing
            )
            return

        # get selected records attitudes

        selected_ids = get_selected_recs_ids(self.selection_model)

        if not selected_ids or len(selected_ids) == 0:
            warn_qt(
                self,
                self.module_name,
                "No selected records to export"
            )
            return

        dialog = ExportDialog(
            self.get_map_crs())

        if dialog.exec():
            if dialog.shapefile_choice == "old":
                export_file_choice = "old shapefile"
                export_file_geom_type = "undefined"
            elif dialog.shapefile_choice == "new":
                export_file_choice = "new shapefile"
                if dialog.pt_shape_choice.isChecked():
                    export_file_geom_type = "point"
                elif dialog.ln_shape_choice.isChecked():
                    export_file_geom_type = "line"
            else:
                export_file_choice = "no shapefile"
                export_file_geom_type = "undefined"

            file_path = dialog.output_file_path.text()
            if file_path == "":
                warn_qt(self, self.module_name, "No shapefile path provided for export")
                return
            else:
                pass
        else:
            return

        if export_file_choice == "new shapefile" and os.path.exists(file_path):
            warn_qt(
                self,
                self.module_name,
                f"{file_path} already existing")
            return

        if export_file_choice == "old shapefile" and not os.path.exists(file_path):
            warn_qt(
                self,
                self.module_name,
                f"{file_path} not found")
            return

        if export_file_choice == "new shapefile":

            if export_file_geom_type == "point":
                ogr_geom_type = ogr.wkbPoint25D

            if export_file_geom_type == "line":
                ogr_geom_type = ogr.wkbLineString25D

            shape_pars = self.extract_output_shapefile_parameters(export_file_geom_type)
            if not shape_pars:
                warn_qt(self, self.module_name, f"Error: debug <shape_pars>: {shape_pars}")
                return

            err = create_shapefile(
                path=file_path,
                geom_type=ogr_geom_type,
                fields_dict_list=shape_pars,
                epsg_code=4326
            )

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

        if export_file_choice == "old shapefile":

            # need to get the shapefile geometric type (line or point, otherwise, report error)

            geometric_types, err = extract_geometry_types_from_shapefile(
                shapefile_pth=file_path
            )

            if err:
                error_qt(
                    self,
                    self.module_name,
                    f"while opening {file_path}: {err!r} "
                )
                return

            if geometric_types is None:
                error_qt(
                    self,
                    self.module_name,
                    f"Error: received 'geometric_types' equal to None"
                )
                return

            if len(list(geometric_types)) != 1:
                error_qt(
                    self,
                    self.module_name,
                    f"Geometric types number in {file_path} is not as expected (got {len(list(geometric_types))} instead of 1)"
                )
                return

            geometric_type = list(geometric_types)[0]

            if geometric_type not in (ogr.wkbPoint25D, ogr.wkbLineString25D):
                error_qt(
                    self,
                    self.module_name,
                    f"Geometric types number in {file_path} is {geometric_type}, should be {ogr.wkbPoint25D} or {ogr.wkbLineString25D}"
                )
                return

            if geometric_type == ogr.wkbPoint25D:
                export_file_geom_type = "point"
            else:
                export_file_geom_type = "line"

        # create query string

        if export_file_geom_type == "point":

            if not selected_ids:
                selected_points_query = select_all_points_query
            else:
                selected_ids_string = ",".join(map(str, selected_ids))
                selected_points_query = selected_points_query_template.format(
                    selected_ids_string)

            # query the database

            db_path = self.path_of_result_db_QLineEdit.text()
            success, selected_points = try_execute_query_with_sqlite3(
                db_path=db_path,
                query=selected_points_query)
            if not success:
                warn_qt(self, self.module_name, selected_points)
                return

        if export_file_geom_type == "line":

            # create query string

            if selected_ids:
                ids = selected_ids
            else:
                db = QSqlDatabase.database(self._qt_conn_name)
                results, err = try_execute_query_with_qt(
                    query=select_records_ids,
                    db=db,
                )
                if err:
                    warn_qt(
                        self,
                        self.module_name,
                        f"{err!r} ")
                    return

                # get ids for selected records

                ids = []
                while results.next():
                    id = int(results.value(0))
                    ids.append(id)

            selected_lines = {}

            for id in ids:

                select_solutions_query = select_solutions_query_template.format(id)
                db = QSqlDatabase.database(self._qt_conn_name)
                results, err = try_execute_query_with_qt(
                    query=select_solutions_query,
                    db=db,
                )

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

                results.first()
                dip_dir = results.value(0)
                dip_ang = results.value(1)
                dataset = results.value(2)
                notes = results.value(3)
                mpt_lon = results.value(4)
                mpt_lat = results.value(5)
                mpt_z = results.value(6)
                creat_time = results.value(7)

                selected_lines[id] = dict(vals=(
                    id,
                    dip_dir,
                    dip_ang,
                    dataset,
                    notes,
                    mpt_lon,
                    mpt_lat,
                    mpt_z,
                    creat_time))

                select_point_coordinates_query = select_point_coordinates_query_template.format(id)
                db = QSqlDatabase.database(self._qt_conn_name)
                results, err = try_execute_query_with_qt(
                    query=select_point_coordinates_query,
                    db=db,
                )

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

                # get ids for selected records

                records_longlatz = []

                while results.next():
                    long, lat, z = results.value(0), results.value(1), results.value(2)
                    records_longlatz.append((long, lat, z))

                selected_lines[id]["pts"] = records_longlatz

        # save results in export dataset

        err = Error()

        if export_file_geom_type == "point":

            err = self.write_records_to_point_shapefile(
                file_path,
                selected_points
            )

        if export_file_geom_type == "line":

            err = self.write_records_to_ln_shapefile(
                file_path,
                selected_lines
            )

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

        info_qgis(
            self.module_name,
            f"Data exported in {file_path}"
        )

    def write_records_to_point_shapefile(
        self,
        point_shapefile_path: str,
        solutions: List[Tuple]
    ) -> Error:

        try:

            shape_pars = self.extract_output_shapefile_parameters("point")
            fld_nms = list(map(lambda par: par["name"], shape_pars))

            return add_points_to_shapefile(
                path=point_shapefile_path,
                field_names=fld_nms,
                values=solutions,
                ndx_x_val=ndx_x_val)

        except Exception as e:

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

    def write_records_to_ln_shapefile(
        self,
        line_shapefile_path: str,
        id_vals_xyzs: Dict
    ) -> Error:

        try:

            shape_pars = self.extract_output_shapefile_parameters("line")
            fld_nms = list(map(lambda par: par["name"], shape_pars))

            return add_lines_to_shapefile(
                path=line_shapefile_path,
                field_names=fld_nms,
                values=id_vals_xyzs)

        except Exception as e:

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

    def _restore_previous_map_tool(self):
        try:
            if getattr(self, "emit_point_from_canvas", None) is None:
                return

            # se il tool corrente è il tuo, ripristina
            if self.canvas.mapTool() == self.emit_point_from_canvas:
                pm = getattr(self, "previous_map_tool", None)

                if pm is not None and not sip.isdeleted(pm):
                    self.canvas.setMapTool(pm)
                else:
                    # fallback: torna al Pan (più sicuro che lasciare la croce)
                    try:
                        iface.actionPan().trigger()
                    except Exception:
                        self.canvas.unsetMapTool(self.emit_point_from_canvas)

        finally:
            self.previous_map_tool = None

    def hideEvent(self, e):
        self._restore_previous_map_tool()
        super().hideEvent(e)

    def closeEvent(self, e):
        self._restore_previous_map_tool()
        super().closeEvent(e)


class SolutionStereonetDialog(QDialog):
    saved = pyqtSignal()

    def __init__(
        self,
        module_name,
        curr_crs_oriented_plane: Plane,
        central_point_epsg_4326: Point,
        north_direct_plane: Plane,
        solution_sorted_eigenvalues: np.ndarray,
        points,
        distances: List[Tuple[Union[None, numbers.Real], Union[None, numbers.Real], Union[None, numbers.Real]]],
        result_db_path,
        db_tables_params,
        prj_crs,
        parent=None
    ):

        super().__init__(parent)

        self.module_name = module_name
        self.plane = curr_crs_oriented_plane
        self.plane_central_point_epsg_4326 = central_point_epsg_4326
        self.north_direct_plane = north_direct_plane
        self.solution_sorted_eigenvalues = solution_sorted_eigenvalues
        self.pts = points
        self.distances = distances
        self.db_path = result_db_path
        self.db_tables_params = db_tables_params
        self.prj_crs = prj_crs
        self.plugin_fldrpth = os.path.dirname(__file__)

        layout = QVBoxLayout()

        s1, s2, s3 = self.solution_sorted_eigenvalues
        solution_msg =                 "<b>Best-fit plane solution</b><br>&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;<i>dip direction azimuth, dip angle</i><br>"
        solution_north_corrected_msg = "<i>azimuth relative to geographic North</i> -> {:05.1f}, {:04.1f}<br>".format(*self.north_direct_plane.vals)
        solution_current_crs_msg =     "<i>azimuth relative to curr. map top axis</i> -> {:05.1f}, {:04.1f}<br>".format(*self.plane.vals)
        eigenvalues_msg =             f"Sorted solution eigenvalues: {self.solution_sorted_eigenvalues}<br>"
        solution_log_ratio_s3s2_msg = f"-log10(S3/S2): {-log10(s3 / s2)}<br>"
        solution_log_ratio_s3s1_msg = f"-log10(S3/S1): {-log10(s3 / s1)}<br>"

        solution_wdg = QTextEdit(
            solution_msg +
            solution_north_corrected_msg +
            solution_current_crs_msg +
            eigenvalues_msg +
            solution_log_ratio_s3s2_msg +
            solution_log_ratio_s3s1_msg
        )

        solution_wdg.setReadOnly(True)
        solution_wdg.setMinimumHeight(UIConstants.SOLUTION_WIDGET_MIN_HEIGHT)
        layout.addWidget(solution_wdg)

        self.points_full_data = []
        pts_str = ""
        for pt, (dx, dy, dz) in zip(self.pts, self.distances):
            self.points_full_data.append([pt[0], pt[4], pt[5], pt[3]] + [calculate_signed_distance(dx, dy, dz), dx, dy, dz])
            pts_str += f"<br><i>{pt[0]}</i> -> {pt[4]}, {pt[5]}, {pt[3]}, {calculate_signed_distance(dx, dy, dz)}"

        points_wdt = QTextEdit(f"<b>Input points</b><br><i>(index -> long, lat, z, signed point distance from plane)</i>\n{pts_str}", self)
        points_wdt.setReadOnly(True)
        points_wdt.setMinimumHeight(UIConstants.POINTS_WIDGET_MIN_HEIGHT)

        layout.addWidget(points_wdt)

        mpl_widget = MplWidget(
            window_title="Stereoplot",
            type="Stereonet",
            data=self.north_direct_plane
        )

        layout.addWidget(mpl_widget)

        save_btn = QPushButton("Save best-fit-plane value")
        save_btn.clicked.connect(self.save_best_fit_plane_solution_to_sqlite_db)
        layout.addWidget(save_btn)

        self.setLayout(layout)

        self.setWindowTitle("Best fit plane solution")

    def save_best_fit_plane_solution_to_sqlite_db(self):

        if not self.db_path:
            warn_qt(
                self,
                self.module_name,
                "Result database not defined in 'Saved result database' tab"
            )
            return

        pars = parse_db_params(self.db_tables_params)

        sol_tbl_nm, sol_tbl_flds, pts_tbl_nm, pts_tbl_flds = pars

        noteDialog = SolutionNotesDialog(self.plugin_fldrpth, parent=self)

        if noteDialog.exec():

            dataset = noteDialog.label.toPlainText()
            notes = noteDialog.comments.toPlainText()

            conn = None
            try:

                conn = sqlite3.connect(self.db_path)
                curs = conn.cursor()

                # Insert a row of data

                values = [
                    None,
                    self.north_direct_plane.dipazim,
                    self.north_direct_plane.dipang,
                    dataset,
                    notes,
                    self.plane_central_point_epsg_4326.x,
                    self.plane_central_point_epsg_4326.y,
                    self.plane_central_point_epsg_4326.z,
                    dt.now()
                ]

                curs.execute(insert_solutions_query.format(sol_tbl_nm), values)

                # Get the max id of the saved solution

                id_fld_alias = get_field_dict('id', sol_tbl_flds)["name"]
                creat_time_alias = get_field_dict('creat_time', sol_tbl_flds)["name"]
                query_sol_id = query_sol_id_template.format(
                    id=id_fld_alias,
                    creat_time=creat_time_alias,
                    solutions=sol_tbl_nm)
                curs.execute(query_sol_id)
                last_sol_id = curs.fetchone()[0]

                # Create the query strings for updating the points table

                points_values = list(map(lambda pt_full_data: [None, last_sol_id, *pt_full_data], self.points_full_data))

                curs.executemany(insert_points_query.format(pts_tbl_nm), points_values)

                conn.commit()
                self.saved.emit()

                info_qgis(
                    self.module_name,
                    "Solution saved in result database"
                )

            finally:

                # Ensure connection is always closed
                if conn is not None:
                    conn.close()


class SolutionNotesDialog(QDialog):

    def __init__(self, plugin_dir, parent=None):
        super().__init__(parent=parent)
        loadUi(os.path.join(plugin_dir, '../ui/solution_notes.ui'), self)
        self.show()


class SelectedSolutionsStereonetDialog(QDialog):

    def __init__(self, module_name, planes, parent=None):

        super().__init__(parent)
        self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose)

        self.module_name = module_name
        self.planes = planes
        self.plugin_fldrpth = os.path.dirname(__file__)

        layout = QVBoxLayout()

        mpl_widget = MplWidget(
            window_title="Stereoplot",
            type="Stereonet",
            data=self.planes,
            set_rc_params=False)
        mpl_widget.adjustSize()

        layout.addWidget(mpl_widget)

        solutions_str = "\n".join(list(map(lambda plane: "{:05.1f}, {:04.1f}".format(*plane.vals), self.planes)))
        solutions_wdg = QPlainTextEdit("Solutions:\n{}".format(solutions_str))
        solutions_wdg.setMaximumHeight(UIConstants.SOLUTIONS_WIDGET_MAX_HEIGHT)
        layout.addWidget(solutions_wdg)

        self.setLayout(layout)

        self.setWindowTitle("Selected solutions")


class ExportDialog(QDialog):

    def __init__(self, projectCrs, parent=None):

        super().__init__(parent)

        self.projectCrs = projectCrs
        self.shapefile_choice = ""

        layout = QGridLayout()

        layout.addWidget(QLabel("Export selected results as:"), 0, 0, 1, 5)

        new_shapefile_layout = QHBoxLayout()

        new_shapefile_layout.addWidget(QLabel("new"))

        self.pt_shape_choice = QRadioButton("point")
        self.pt_shape_choice.setChecked(True)
        new_shapefile_layout.addWidget(self.pt_shape_choice)

        self.ln_shape_choice = QRadioButton("line")
        new_shapefile_layout.addWidget(self.ln_shape_choice)

        new_shapefile_layout.addWidget(QLabel("shapefile"))

        self.create_shapefile_pButton = QPushButton("create")
        self.create_shapefile_pButton.clicked.connect(self.shapefile_make_new)
        new_shapefile_layout.addWidget(self.create_shapefile_pButton)

        layout.addLayout(new_shapefile_layout, 1, 0, 1, 5)

        layout.addWidget(QLabel("existing shapefile"), 2, 0, 1, 4)

        self.use_shapefile_pButton = QPushButton("load")
        self.use_shapefile_pButton.clicked.connect(self.shapefile_load_existing)
        layout.addWidget(self.use_shapefile_pButton, 2, 4, 1, 1)

        self.output_file_path = QLineEdit()
        layout.addWidget(self.output_file_path, 3, 0, 1, 5)

        ok_button = QPushButton("&OK")
        ok_button.clicked.connect(self.accept)

        cancel_button = QPushButton("Cancel")
        cancel_button.clicked.connect(self.reject)

        button_layout = QHBoxLayout()
        button_layout.addStretch()
        button_layout.addWidget(ok_button)
        button_layout.addWidget(cancel_button)

        layout.addLayout(button_layout, 4, 0, 1, 5)

        self.setLayout(layout)

        self.setWindowTitle("Export")

    def shapefile_make_new(self):

        shapefile_path = define_path_new_file(
            self,
            "Choose shapefile name",
            "*.shp",
            "shp (*.shp *.SHP)")

        self.output_file_path.setText(shapefile_path)

        if shapefile_path:
            self.shapefile_choice = "new"

    def shapefile_load_existing(self):

        shapefile_path = old_file_path(
            self,
            "Choose shapefile name",
            "*.shp",
            "shp (*.shp *.SHP)")

        self.output_file_path.setText(shapefile_path)

        if shapefile_path:
            self.shapefile_choice = "old"

