"""
/***************************************************************************
 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 os
from math import sqrt, log10
from typing import Optional, List, Dict, Union

from datetime import datetime as dt

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.planes import *
from geogst.core.orientations.orientations import *
from geogst.dependencies.yaml.io import *
from geogst.io.rasters.gdal import *
from geogst.io.vectors.ogr import *

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:
            pass

        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
            pass

    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: 
            pass

        '''
        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: 
                pass
             
            try:               
                self.reset_input_point_states()
            except:
                pass

            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:
            pass                            

        try:
            if self.previous_map_tool is not None:
                self.canvas.setMapTool(self.previous_map_tool)
        except:
            pass

    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"

