# -*- coding: utf-8 -*-
"""
/***************************************************************************
 GEOL_QMAPS
                                 A QGIS plugin
 GEOL_QMAPS
 Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/
                              -------------------
        begin                : 2024-11-13
        git sha              : $Format:%H$
        copyright            : (C) 2024 by GEOL_QMAPS
        email                : GEOL_QMAPS
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   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 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 ***************************************************************************/
"""
from qgis.gui import QgsMessageBar
from qgis.PyQt.QtCore import (
    QSettings,
    QTranslator,
    QCoreApplication,
    Qt,
    QThread,
    pyqtSignal,
    QUrl,
)
from qgis.PyQt.QtGui import (
    QIcon,
    QColor,
    QBrush,
    QDesktopServices,
    QFont,
    QStandardItemModel,
    QStandardItem,
)
from qgis.PyQt import QtWidgets
from qgis.PyQt.QtWidgets import (
    QAction,
    QFileDialog,
    QDialog,
    QProgressBar,
    QApplication,
    QMainWindow,
    QDockWidget,
    QVBoxLayout,
    QHBoxLayout,
    QTabWidget,
    QLabel,
    QSizePolicy,
    QTableWidget,
    QTableWidgetItem,
    QPushButton,
    QTableWidget,
    QWidget,
    QComboBox,
    QGroupBox,
    QStyledItemDelegate,
    QListView,
    QButtonGroup,
)

from PyQt5.QtWidgets import (
    QHeaderView
)
from qgis.core import (
    Qgis,
    QgsProject,
    QgsVectorLayer,
    QgsPoint,
    QgsRectangle,
    QgsGeometry,
    QgsField,
    QgsFeature,
    QgsExpression,
    QgsCoordinateTransform,
    QgsCoordinateReferenceSystem,
    QgsApplication,
    QgsProviderRegistry,
    QgsLayerTreeGroup,
    QgsLayerTreeLayer,
    QgsDefaultValue,
    QgsCoordinateTransformContext,
)
from qgis.PyQt.QtCore import QVariant, QUrl
from qgis.utils import plugins, iface
from qgis.utils import qgsfunction
from qgis.PyQt.QtWidgets import QAction, QToolBar
from qgis.core import QgsProject, QgsLayerTreeGroup, QgsLayerDefinition
from qgis.PyQt.QtWidgets import QDockWidget
from qgis.PyQt.QtCore import Qt

import warnings

warnings.filterwarnings("ignore", category=UserWarning, module="\\*")

from PIL import Image
from PIL.ExifTags import TAGS, GPSTAGS

from osgeo import ogr
import os
import subprocess
import sys
import numpy as np
import shutil
import json
from xml.etree import ElementTree as ET  # ADD
from datetime import datetime
import re

# Initialize Qt resources from file resources.py
from .resources import *

# Import the code for the dialog
# from .WAXI_QF_dialog import WAXI_QFDialog
import os.path
from qgis.core import QgsRectangle, Qgis
from qgis.core.additions.edit import edit
import processing
from qgis.core import (
    QgsGeometry,
    QgsWkbTypes,
    QgsProject,
    QgsVectorLayer,
    QgsVectorFileWriter,
    QgsApplication,
    QgsFeature,
    QgsVectorDataProvider,
    QgsField,
    QgsFeature
)

import urllib.request
import tempfile
import zipfile
import shutil
from pathlib import Path
from qgis.PyQt.QtWidgets import QFileDialog

from scipy.spatial.distance import cdist
from processing.gui.AlgorithmExecutor import execute_in_place
import hashlib

import tempfile, zipfile, shutil, os
from qgis.PyQt.QtWidgets import QFileDialog, QMessageBox

# Libraries for manipulating Excel files
import pandas as pd
import platform

# Library for list copies
from copy import copy

# import warnings
# warnings.simplefilter(action='ignore', category=FutureWarning)


# Library for check the input file nature
from pathlib import Path
from .FieldMove_Import import FM_Import
from .GEOL_QMAPS_dockwidget import GEOL_QMAPSDockWidget
from .ppigrf import igrf, get_inclination_declination

# Importation des bibliothèques situées dans le dossier du plugin
plugin_directory = os.path.dirname(__file__)
biblio_python_directory = os.path.join(plugin_directory, "biblio_Python")
sys.path.append(biblio_python_directory)


class GEOL_QMAPS:
    """QGIS Plugin Implementation."""

    def __init__(self, iface):
        """Constructor.

        :param iface: An interface instance that will be passed to this class
            which provides the hook by which you can manipulate the QGIS
            application at run time.
        :type iface: QgsInterface
        """
        # Save reference to the QGIS interface
        self.iface = iface

        # initialize plugin directory
        self.plugin_dir = os.path.dirname(__file__)

        # initialize locale
        locale = str(QSettings().value("locale/userLocale"))[0:2]
        locale_path = os.path.join(
            self.plugin_dir, "i18n", "GEOL_QMAPS_{}.qm".format(locale)
        )

        if os.path.exists(locale_path):
            self.translator = QTranslator()
            self.translator.load(locale_path)
            QCoreApplication.installTranslator(self.translator)

        # Declare instance attributes
        self.actions = []
        self.menu = self.tr("&GEOL_QMAPS")
        # TODO: We are going to let the user set this up in a future iteration
        self.toolbar = self.iface.addToolBar("GEOL_QMAPS")
        self.toolbar.setObjectName("GEOL_QMAPS")

        # print "** INITIALIZING GEOL_QMAPS"

        self.pluginIsActive = False
        self.dlg = None

        # Check if plugin was started the first time in current QGIS session
        # Must be set in initGui() to survive plugin reloads
        self.first_start = None

        # Initialize the attributes so they exist when used later.
        self.create_lithologies = False 
        self.create_structures = False
        self.fichier_output_lithology = None
        self.fichier_output_structures = None

        ## Python library integration
        def install_library(library_name):
            try:
                if platform.system() == "Windows":
                    subprocess.check_call(
                        ["python", "-m", "pip", "install", library_name]
                    )
                else:
                    subprocess.check_call(
                        ["python3", "-m", "pip3", "install", library_name]
                    )
                print(f"Successfully installed {library_name}")
            except subprocess.CalledProcessError as e:
                print(f"Error installing {library_name}: {e}")

        # Library fiona
        try:
            import fiona

        except:
            install_library("fiona")

        # Library FuzzyWuzzy
        try:
            import fuzzywuzzy

        except:
            install_library("fuzzywuzzy")


    # noinspection PyMethodMayBeStatic
    def tr(self, message):
        """Get the translation for a string using Qt translation API.

        We implement this ourselves since we do not inherit QObject.

        :param message: String for translation.
        :type message: str, QString

        :returns: Translated version of message.
        :rtype: QString
        """
        # noinspection PyTypeChecker,PyArgumentList,PyCallByClass
        return QCoreApplication.translate("GEOL_QMAPS", message)

        # Lipari colorscale

    def lipari_color(self, score):
        """
        Returns a QColor based on the given normalized score (0 to 100)
        using a Roma colorscale (Cramieri et al.).
        """
        stops = [
            (0, QColor("#FF0000")),  # red
            (50, QColor("#FFFFFF")),  # white
            (100, QColor("#00FF00"))  # green
        ]
        if score <= stops[0][0]:
            return stops[0][1]
        if score >= stops[-1][0]:
            return stops[-1][1]
        for i in range(1, len(stops)):
            if score <= stops[i][0]:
                lower_score, lower_color = stops[i - 1]
                upper_score, upper_color = stops[i]
                t = (score - lower_score) / (upper_score - lower_score)
                r = lower_color.red() + t * (upper_color.red() - lower_color.red())
                g = lower_color.green() + t * (upper_color.green() - lower_color.green())
                b = lower_color.blue() + t * (upper_color.blue() - lower_color.blue())
                return QColor(int(r), int(g), int(b))

    # create legend with colorbar
    def create_legend_widget(self):
        # Create the legend container widget
        legend_container = QWidget()
        legend_container.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)

        # Horizontal layout for the legend container
        h_layout = QHBoxLayout(legend_container)
        h_layout.setContentsMargins(0, 0, 0, 0)
        h_layout.setSpacing(5)

        # Vertical layout for the color bar and top/bottom labels
        v_layout = QVBoxLayout()
        v_layout.setContentsMargins(0, 0, 0, 0)
        v_layout.setSpacing(2)

        top_label = QLabel("highest")
        top_label.setAlignment(Qt.AlignCenter)
        top_label.setFont(QFont("Arial", 6))

        bottom_label = QLabel("lowest")
        bottom_label.setAlignment(Qt.AlignCenter)
        bottom_label.setFont(QFont("Arial", 6))

        # Create the color bar widget (set fixed width and allow vertical expansion)
        color_bar = QLabel()
        color_bar.setFixedWidth(40)
        color_bar.setMinimumHeight(200)
        color_bar.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding)
        color_bar.setStyleSheet(
            "background: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0, "
            "stop:0 #FF0000, stop:0.5 #FFFFFF, stop:1 #00FF00);"
            "border: 1px solid black;"
        )

        # Add widgets to the vertical layout with the color bar taking extra space
        v_layout.addWidget(top_label, 0)
        v_layout.addWidget(color_bar, 1)
        v_layout.addWidget(bottom_label, 0)

        # Add the vertical layout to the horizontal layout, then add stretch for centering
        h_layout.addLayout(v_layout)
        h_layout.addStretch()

        legend_container.setLayout(h_layout)
        return legend_container

    def add_action(
        self,
        icon_path,
        text,
        callback,
        enabled_flag=True,
        add_to_menu=True,
        add_to_toolbar=True,
        status_tip=None,
        whats_this=None,
        parent=None,
    ):
        """Add a toolbar icon to the toolbar.

        :param icon_path: Path to the icon for this action. Can be a resource
            path (e.g. ':/plugins/foo/bar.png') or a normal file system path.
        :type icon_path: str

        :param text: Text that should be shown in menu items for this action.
        :type text: str

        :param callback: Function to be called when the action is triggered.
        :type callback: function

        :param enabled_flag: A flag indicating if the action should be enabled
            by default. Defaults to True.
        :type enabled_flag: bool

        :param add_to_menu: Flag indicating whether the action should also
            be added to the menu. Defaults to True.
        :type add_to_menu: bool

        :param add_to_toolbar: Flag indicating whether the action should also
            be added to the toolbar. Defaults to True.
        :type add_to_toolbar: bool

        :param status_tip: Optional text to show in a popup when mouse pointer
            hovers over the action.
        :type status_tip: str

        :param parent: Parent widget for the new action. Defaults None.
        :type parent: QWidget

        :param whats_this: Optional text to show in the status bar when the
            mouse pointer hovers over the action.

        :returns: The action that was created. Note that the action is also
            added to self.actions list.
        :rtype: QAction
        """

        icon = QIcon(icon_path)
        action = QAction(icon, text, parent)
        action.triggered.connect(callback)
        action.setEnabled(enabled_flag)

        if status_tip is not None:
            action.setStatusTip(status_tip)

        if whats_this is not None:
            action.setWhatsThis(whats_this)

        if add_to_toolbar:
            self.toolbar.addAction(action)

        if add_to_menu:
            self.iface.addPluginToMenu(self.menu, action)

        self.actions.append(action)

        return action

    def initGui(self):
        """Create the menu entries and toolbar icons inside the QGIS GUI."""

        icon_path = ":/plugins/GEOL_QMAPS/icon.png"
        self.add_action(
            icon_path,
            text=self.tr("GEOL_QMAPS"),
            callback=self.run,
            parent=self.iface.mainWindow(),
        )

        # will be set False in run()
        self.first_start = True

    # --------------------------------------------------------------------------

    def onClosePlugin(self):
        """Cleanup necessary items here when plugin dockwidget is closed"""

        # print "** CLOSING GEOL_QMAPS"

        # disconnects
        self.dlg.closingPlugin.disconnect(self.onClosePlugin)

        # remove this statement if dockwidget is to remain
        # for reuse if plugin is reopened
        # Commented next statement since it causes QGIS crashe
        # when closing the docked window:
        # self.dlg = None

        self.pluginIsActive = False

    def unload(self):
        """Removes the plugin menu item and icon from QGIS GUI."""

        # print "** UNLOAD GEOL_QMAPS"

        for action in self.actions:
            self.iface.removePluginMenu(self.tr("&GEOL_QMAPS"), action)
            self.iface.removeToolBarIcon(action)
        # remove the toolbar
        del self.toolbar

    # --------------------------------------------------------------------------

    def check_version(self):

        project = QgsProject.instance()

        plugin_version = template_version = "0.0.0"
        template_version_file_path = self.mynormpath(
            self.basePath + self.dir_99 + "Version.txt"
        )
        template_version_file = open(template_version_file_path, "r")
        version = template_version_file.readline()
        template_version = version[1:].split(".")
        template_version = [int(x) for x in version.lstrip("v").strip().split(".")]
        print(f"Template version {template_version}")

        metadata_path = self.mynormpath(
            os.path.dirname(os.path.realpath(__file__)) + "/metadata.txt"
        )
        plugin_version_file = open(metadata_path)
        metadata = plugin_version_file.readlines()
        for line in metadata:
            parts = line.split("=")
            if len(parts) == 2 and parts[0] == "version":
                pversion = parts[1]
                plugin_version = [int(x) for x in parts[1].strip().split(".")]

        version_text = "Plugin v" + pversion.rstrip() + "  Template " + version
        self.dlg.versions_label.setText(version_text)

        pv = ".".join(str(x) for x in plugin_version)
        tv = ".".join(str(x) for x in template_version)

        # 1. Template is newer than plugin
        if (
                (template_version[0] > plugin_version[0])
                or (
                template_version[0] == plugin_version[0]
                and template_version[1] > plugin_version[1]
        )
        ):
            self.iface.messageBar().pushMessage(
                "ERROR: Template {} newer than Plugin {}, please update plugin NOW!".format(pv, tv),
                level=Qgis.Critical,
                duration=45,
            )

        # 2. Template version < 3.1.0
        elif (
                template_version[0] < 3
                or (
                        template_version[0] == 3
                        and template_version[1] == 0
                )
        ):
            print("Template version < 3.1.0")
            self.iface.messageBar().pushMessage(
                f"Plugin {pv} newer than Template {tv}, uncertain behaviour! "
                "Please consider reimplementing a GEOL-QMAPS project manually from the "
                "latest template available on <a href='https://doi.org/10.5281/zenodo.7834717'>here</a>",
                level=Qgis.Warning,
                duration=45,
            )

        # 3. Template version = 3.1.0
        elif (
                template_version[0] == 3
                and template_version[1] == 1
                and template_version[2] == 0
        ):
            print("Template version = 3.1.0")
            self.iface.messageBar().pushMessage(
                f"Plugin {pv} newer than Template {tv}, uncertain behaviour! "
                "Please consider either reimplementing a GEOL-QMAPS project manually from the "
                "latest template available on <a href='https://doi.org/10.5281/zenodo.7834717'>here</a> "
                "or using the Update Template Version tool in the Database Management tab of the plugin "
                "(Compilation_Deformation zones_PG layer not handled though).",
                level=Qgis.Warning,
                duration=45,
            )

        # 3. Plugin is newer than template (any patch/minor difference)
        elif (
                (template_version[0] < plugin_version[0])
                or (
                        template_version[0] == plugin_version[0]
                        and template_version[1] < plugin_version[1]
                )
                or (
                        template_version[0] == plugin_version[0]
                        and template_version[1] == plugin_version[1]
                        and template_version[2] < plugin_version[2]
                )
        ):
            print ("Plugin is newer than template (any patch/minor difference)")
            self.iface.messageBar().pushMessage(
                f"Plugin {pv} newer than Template {tv}, uncertain behaviour! "
                "Please consider using the Update Template Version tool in the Database Management "
                "tab of the plugin.",
                level=Qgis.Warning,
                duration=45,
            )

        # 4. All good
        else:
            print("Plugin and Template - same version")
            self.iface.messageBar().pushMessage(
                "SUCCESS: Plugin {} and Template {} are compatible.".format(pv, tv),
                level=Qgis.Success,
                duration=15,
            )

        template_version_file.close()
        plugin_version_file.close()


    # GEOL-QMAPS Project Validation
    def is_valid_geol_qmaps_project(self, qgis_folder):
        """
        Validate that the selected folder is a GEOL-QMAPS project by checking
        for the presence of COMPILATION.gpkg in the 1_EXISTING_FIELD_DATABASE subfolder.
        """
        expected_0 = os.path.join(qgis_folder, "0_FIELD_DATA", "CURRENT_MISSION.gpkg")
        expected_1 = os.path.join(qgis_folder, "1_EXISTING_FIELD_DATABASE", "COMPILATION.gpkg")
        expected_99 = os.path.join(qgis_folder, "99_COMMAND_FILES_PLUGIN")
        if os.path.exists(expected_0) and os.path.exists(expected_1) and os.path.exists(expected_99):
            return True
        else:
            return False

    # Filename Length Check Helper
    import tempfile
    import os

    def _check_max_filename_length(self, directory, max_length=256):
        """
        Walk through 'directory' and gather any full file‐paths longer than max_length.
        If any are found, write them to a text file and show a clickable link in a
        QGIS Critical message. Returns True if all paths are OK, False otherwise.
        """
        bad_paths = []
        for root, dirs, files in os.walk(directory):
            for name in files + dirs:
                full_path = os.path.abspath(os.path.join(root, name))
                if len(full_path) > max_length:
                    bad_paths.append(full_path)

        if bad_paths:
            # Write the list to a temp file
            log_file = os.path.join(tempfile.gettempdir(), "geol_qmaps_bad_paths.txt")
            with open(log_file, "w", encoding="utf-8") as f:
                for p in bad_paths:
                    f.write(p + "\n")

            # Ensure the file:// URL is well-formed
            url = log_file.replace("\\", "/")
            if not url.startswith("/"):
                url = "/" + url

            # Push a critical message with hyperlink
            msg = (
                f"{len(bad_paths)} paths exceed {max_length} characters. "
                f"<a href='file://{url}'>Download list</a>"
            )
            self.iface.messageBar().pushMessage(
                msg,
                level=Qgis.Critical,
                duration=0  # stays until dismissed
            )
            return False

        return True


    #RUN
    def run(self):
        """Run method that loads and starts the plugin"""

        project = QgsProject.instance()
        proj_file_path = project.fileName()
        head_tail = os.path.split(proj_file_path)
        self.basePath = head_tail[0] + "/"
        self.dir_99 = "99_COMMAND_FILES_PLUGIN/"
        self.dir_0 = "0_FIELD_DATA/"
        self.dir_1 = "1_EXISTING_FIELD_DATABASE/"
        self.dir_11 = "11_ORTHOPHOTOGRAPHY-SATELLITE_IMAGERY/"
        self.geopackage_file_path = (
            self.basePath + "/1_EXISTING_FIELD_DATABASE/" + "/COMPILATION.gpkg"
        )

        self.layers_names = [
            "Deformation zones_PG",
            "Local lithologies_PT",
            "Supergene lithologies_PT",
            "Sedimentary lithologies_PT",
            "Volcanoclastic lithologies_PT",
            "Igneous extrusive lithologies_PT",
            "Igneous intrusive lithologies_PT",
            "Metamorphic lithologies_PT",
            "Bedding-Lava flow-S0_PT",
            "Dikes-Sills_PT",
            "Folds_PT",
            "Foliation-cleavage_PT",
            "Fractures_PT",
            "Lineations_PT",
            "Shear zones and faults_PT",
            "Veins_PT",
            "Lithological contacts_PT"
        ]
        self.layers_names_all = [
            "Magnetic susceptibility_PT",
            "Density_PT",
            "Observations_PT",
            "Photographs_PT",
            "Sampling_PT",
            "Stops_PT",
        ]
        self.layers_names_all = self.layers_names_all + self.layers_names

        self.FM_Import = FM_Import(None)

        if self.is_valid_geol_qmaps_project(self.basePath) is True:
            if not self.pluginIsActive:
                self.pluginIsActive = True

                # print "** STARTING GEOL_QMAPS"

                # dockwidget may not exist if:
                #    first run of plugin
                #    removed on close (see self.onClosePlugin method)
                if self.dlg == None:
                    # Create the dockwidget (after translation) and keep reference
                    self.dlg = GEOL_QMAPSDockWidget()

                # connect to provide cleanup on closing of dockwidget
                self.dlg.closingPlugin.connect(self.onClosePlugin)

                # show the dockwidget
                # TODO: fix to allow choice of dock location

                # Create the dialog with elements (after translation) and keep reference
                # Only create GUI ONCE in callback, so that it will only load when the plugin is started

                self.iface.addDockWidget(Qt.RightDockWidgetArea, self.dlg)

                # Find existing dock widgets in the right area
                right_docks = [
                    d
                    for d in self.iface.mainWindow().findChildren(QDockWidget)
                    if self.iface.mainWindow().dockWidgetArea(d)
                    == Qt.RightDockWidgetArea
                ]
                # If there are other dock widgets, tab this one with the first one found
                if right_docks:
                    for dock in right_docks:
                        if dock != self.dlg:
                            self.iface.mainWindow().tabifyDockWidget(dock, self.dlg)
                            # Optionally, bring your plugin tab to the front
                            self.dlg.raise_()
                            break

                self.dlg.show()

                self.check_version()

                if self.first_start == True:
                    self.first_start = False

                    ### Accessing the main window : QDialog
                    # self.dlg = WAXI_QFDialog()
                    # self.dlg.setFixedSize(1131, 600)

                    ### Connection of PushButtons ###
                    # QField to QGIS Sync
                    self.dlg.pushButton_QFieldPackage.clicked.connect(self.select_QFieldPackage)
                    self.dlg.pushButton_QGISFolder.clicked.connect(self.select_QGISFolder)
                    self.dlg.pushButton_SyncQFieldToQGIS.clicked.connect(self.SyncQFieldToQGIS)

                    # Rejig Project
                    self.dlg.pushButton_37.clicked.connect(self.select_old_project_folder)
                    self.dlg.rejig_pushButton_4.clicked.connect(self.rejig_project)

                    # Project_parameters
                    self.dlg.projName_pushButton.clicked.connect(self.updateProjectTitle)
                    self.dlg.merge_pushButton.clicked.connect(self.mergeProjects)
                    self.dlg.merge_pushButton_2.clicked.connect(self.removeDuplicates)
                    self.dlg.merge_layers_pushButton_2.clicked.connect(self.merge_2_layers)

                    self.dlg.clip_pushButton.clicked.connect(self.clipToCanvas)
                    self.dlg.pushButton_19.clicked.connect(self.resetWindow_fieldwork_preparation)

                    # Import_data (the first button connects all the other buttons with the correct input parameters as the program runs)
                    self.dlg.pushButton_9.clicked.connect(self.handlePushButton9)
                    self.dlg.pushButton_11.clicked.connect(self.Go_Back_table1)
                    self.dlg.pushButton_12.clicked.connect(self.Go_Back_table2)
                    self.dlg.pushButton_25.clicked.connect(self.Go_Back_table3)
                    self.dlg.pushButton_14.clicked.connect(self.Generate_Output_QGIS_Layers)
                    self.dlg.pushButton_13.clicked.connect(self.click_Reset_This_Window)

                    #User by default
                    self.dlg.pushButton_user_default.clicked.connect(self.set_user_by_default)
                    self.fill_ComboBox_layers_user()

                    # Save a new CURRENT_MISSION+DICTIONARIES.qlr file
                    self.dlg.pushButton_save__project_template_style_2.clicked.connect(self.save_template_style)  # ADD

                    # Export_data
                    self.dlg.export_pushButton.clicked.connect(self.exportData)
                    self.dlg.pushButton_22.clicked.connect(self.resetWindow_data_management)

                    # Stop
                    self.dlg.virtual_pushButton.clicked.connect(self.virtualStops)

                    # Stereo
                    self.dlg.stereonet_pushButton.clicked.connect(self.set_stereoConfig)
                    self.dlg.stereonet_pushButton_2.clicked.connect(
                        lambda: QDesktopServices.openUrl(
                            QUrl("https://github.com/swaxi/qgis-stereonet")
                        )
                    )

                    # Dictinaries
                    self.dlg.csv_pushButton.clicked.connect(self.addCsvItem)
                    self.dlg.csv_pushButton_2.clicked.connect(self.deleteCsvItem)

                    #Picture Management
                    self.dlg.pushButton_update_source_photo.clicked.connect(self.update_source_photo)

                    # HELP
                    # Send email to Mark Jessell and Julien Perret
                    self.dlg.pushButton_24.clicked.connect(
                        lambda: QDesktopServices.openUrl(
                            QUrl(
                                "mailto:julien.perret@uwa.edu.au;mark.jessell@uwa.edu.au"
                            )
                        )
                    )
                    self.dlg.pushButton_28.clicked.connect(
                        lambda: QDesktopServices.openUrl(
                            QUrl(
                                "mailto:julien.perret@uwa.edu.au;mark.jessell@uwa.edu.au"
                            )
                        )
                    )

                    # Connection to the Github issues page site  : https://github.com/swaxi/WAXI_QF/issues/
                    self.dlg.pushButton_23.clicked.connect(
                        lambda: QDesktopServices.openUrl(
                            QUrl("https://github.com/swaxi/WAXI_QF/issues/")
                        )
                    )

                    # Connection to the WAXI site  : https://waxi4.org/
                    self.dlg.pushButton_41.clicked.connect(
                        lambda: QDesktopServices.openUrl(QUrl("https://waxi4.org/"))
                    )

                    # Connection to the help file  : https://github.com/swaxi/GEOL-QMAPS/tree/main#5-workflow
                    self.dlg.pushButton_21.clicked.connect(
                        lambda: QDesktopServices.openUrl(
                            QUrl("https://github.com/swaxi/GEOL-QMAPS/tree/main#5-workflow")
                        )
                    )

                    self.dlg.pushButton_30.clicked.connect(
                        lambda: QDesktopServices.openUrl(
                            QUrl("https://github.com/swaxi/GEOL-QMAPS/blob/Julien_2505/README.md#65-help---roadmap-tab")
                        )
                    )

                    #  Connection to the AMIRA site : https://amira.global/
                    self.dlg.pushButton_40.clicked.connect(
                        lambda: QDesktopServices.openUrl(QUrl("https://amira.global/"))
                    )

                    #  Connection to the Zenodo repository for the corresponding template release:
                    self.dlg.pushButton_34.clicked.connect(
                        lambda: QDesktopServices.openUrl(
                            QUrl("https://doi.org/10.5281/zenodo.7834717")
                        )
                    )

                    # PushButtons to search for files on the computer
                    self.dlg.pushButton_FM_project_select.clicked.connect(self.import_FM_Project)
                    self.dlg.pushButton_7.clicked.connect(self.select_file_to_import)
                    self.dlg.pushButton_6.clicked.connect(self.select_clip_poly)
                    self.dlg.pushButton.clicked.connect(self.select_dst_directory)
                    self.dlg.pushButton_29.clicked.connect(self.select_main_project)
                    self.dlg.pushButton_20.clicked.connect(self.select_sub_project)
                    self.dlg.pushButton_27.clicked.connect(self.select_merged_directory)
                    self.dlg.pushButton_5.clicked.connect(self.select_export_directory)
                    self.dlg.pushButton_15.clicked.connect(self.select_file_source_path_photo)  # ADD
                    self.dlg.pushButton_17.clicked.connect(self.select_file_export_template_style)  # ADD

                    self.dlg.comboBox.currentIndexChanged.connect(self.update_combobox_delete)

                    #Set Orientation Measurement Style
                    self.dlg.structure_style_on_pushButton.clicked.connect(self.set_orientation_style)
                    self.dlg.structure_style_off_pushButton.clicked.connect(self.set_orientation_style)

                    self.dlg.merge_current_existing_pushButton_3.clicked.connect(self.merge_current_to_existing)

                    self.dlg.pushButton_use_exif_azimuth.clicked.connect(self.use_exif_azimuth)

                    head_tail = os.path.split(proj_file_path)
                    main_project_path = head_tail[0] + "/"
                    self.dictionaries_path = (main_project_path + self.dir_99 + "/Dictionaries.gpkg")
                    self.csvs = self.get_csv_list(self.dictionaries_path)
                    self.csvs = self.csvs[1:]

                    # ComboBox for create dropdown list of all csv files
                    self.sorted_csv_combobBox()

                    # set default structural emasurement from project in gui
                    layer = QgsProject.instance().mapLayersByName("Veins_PT")[0]
                    # Find 'Measure' field index
                    field_index = layer.fields().indexFromName("Measure")

                    # Update default field value
                    default_value = layer.defaultValueDefinition(field_index).expression()

                    if default_value == "'Dip - dip direction'":
                        self.dlg.structure_style_on_pushButton.setChecked(True)
                    else:
                        self.dlg.structure_style_off_pushButton.setChecked(True)

                    # Combobox Merge 2 layers
                    self.fill_ComboBox()
                    QgsProject.instance().layersAdded.connect(self.update_ComboBox)
                    QgsProject.instance().layersRemoved.connect(self.update_ComboBox)

                    self.sheetHashUUID = []
                    self.define_tips()

                    ### Show the dialog
                    self.dlg.show()

        else:
            title = "GEOL-QMAPS Plugin Error"
            text = ("A project based on a GEOL-QMAPS template must be loaded before using this plugin. "
            "You can download the template <a href='https://doi.org/10.5281/zenodo.7834717'>here</a>."
            )
            # critical() will show a modal dialog with a red “stop” icon
            QMessageBox.critical(self.iface.mainWindow(),title,text)

    ###############################################################################
    ####################         Page 1 : Import data            ##################
    ###############################################################################
    ###############################################################################
    ################# Step 0 : Check the input file nature ########################
    ###############################################################################

    def test_input_file_nature(chemin):
        chemin = Path(chemin)

        if chemin.exists():

            # Retrieve the file extension to determine its nature
            extension = chemin.suffix.lower()

            # Tests the nature of the file based on its extension

            if extension == ".shp" or extension == ".gpkg":
                return "Fichier Shapefile ou Geopackage"

            else:
                return "Fichier non compatible"
        else:
            return "Le fichier n'existe pas !"

    ###############################################################################
    ######## Step 1 : Check layer coordinates + Create geometry column ############
    ###############################################################################

    def convert_coordinates_WGS84(self, layer):

        # Selection of all entities in the layer
        layer.selectAll()

        # Creation of a new 'Geometry' column in the layer's attribute table
        layer.startEditing()
        layer.addAttribute(QgsField("Geometry", QVariant.String))

        # Recuperation of CRS (Coordinate Reference System)
        crs = layer.crs()

        # Conversion of coordinates to WGS84
        for feature in layer.selectedFeatures():
            geometrie = feature.geometry()

            transformation = QgsCoordinateTransform(
                crs, QgsProject.instance().crs(), QgsProject.instance()
            )
            geometrie.transform(transformation)

            feature["Geometry"] = geometrie.asWkt()
            layer.updateFeature(feature)

        # Save changes
        layer.commitChanges()
        layer.removeSelection()

        # Refreshing the view in QGIS
        QgsProject.instance().reloadAllLayers()

    ###############################################################################
    # Step 2 : Export the layer in Excel format + Fill Table1 with Columns pairs ##
    ###############################################################################

    def export_layer_fill_Table1(self, layer):
        from fuzzywuzzy import fuzz

        # 1. Load your input file into a DataFrame
        noms_des_champs = [field.name() for field in layer.fields()]
        data_list = []
        for feature in layer.getFeatures():
            ligne = {
                field.name(): (feature[field.name()] if feature[field.name()] is not None else "")
                for field in layer.fields()
            }
            data_list.append(ligne)
        input_file = pd.DataFrame(data_list, columns=noms_des_champs)
        input_file = input_file.astype(str)

        # 2. Load columns_reference_WAXI4.csv
        WAXI_projet_path = os.path.abspath(QgsProject.instance().fileName())
        emplacement_files_WAXI_columns = os.path.join(
            os.path.dirname(WAXI_projet_path),
            self.dir_99 + "/columns_reference_WAXI4.csv",
        )
        column_reference = pd.read_csv(emplacement_files_WAXI_columns)
        list_column_reference = column_reference.columns.tolist()

        # 3. Load columns_reference_fieldnames_aliases_WAXI4.csv
        alias_file_path = os.path.join(
            os.path.dirname(WAXI_projet_path),
            self.dir_99 + "/columns_reference_fieldnames_aliases_WAXI4.csv",
        )
        alias_df = pd.read_csv(alias_file_path)

        # Build the forward alias mapping (real_name → alias) as a class attribute
        self.alias_mapping = dict(
            zip(alias_df.iloc[:, 0].astype(str), alias_df.iloc[:, 1].astype(str))
        )
        # Build the reverse mapping (alias → real_name) as a class attribute
        self.reverse_alias_mapping = {v: k for k, v in self.alias_mapping.items()}

        # Define self.alias_list as a class attribute
        self.alias_list = list(self.alias_mapping.values())
        if "NULL" not in self.alias_list:
            self.alias_list.insert(0, "NULL")

        # 4. Pair creation based on similarity score
        list_column_input = input_file.columns.tolist()
        list_trio_columns = []
        for col in list_column_input:
            list_trio_columns.append([col, "NULL", 0])

        # 5. Fuzzy matching logic
        for k in range(len(list_column_input)):
            best_column = "NULL"
            score = 0
            if list_column_input[k] == "NULL" or list_column_input[k].strip() == "":
                list_trio_columns[k][1] = "NULL"
                list_trio_columns[k][2] = 0
            else:
                for column in list_column_reference:
                    sum_score = 0
                    sum_score += fuzz.token_set_ratio(list_column_input[k], column) * 3
                    contenu_column = column_reference[column]
                    for elt2 in contenu_column:
                        sum_score += fuzz.token_set_ratio(list_column_input[k], str(elt2))
                    if sum_score > score:
                        score = sum_score
                        best_column = column

                # Use the alias from self.alias_mapping if available
                if best_column != "NULL" and best_column in self.alias_mapping:
                    alias_value = self.alias_mapping[best_column]
                else:
                    alias_value = best_column

                list_trio_columns[k][1] = alias_value
                list_trio_columns[k][2] = score

        # Modification: Ensure no duplicate assignments among the alias values
        list_trio_columns_trie = list_trio_columns
        for place1 in range(len(list_trio_columns) - 1):
            test = list_trio_columns[place1][1]
            for place2 in range(len(list_trio_columns) - 1):
                if place1 != place2:
                    if test != "NULL":
                        if test == list_trio_columns[place2][1]:
                            if list_trio_columns[place1][2] > list_trio_columns[place2][2]:
                                list_trio_columns_trie[place2][1] = "NULL"
                                list_trio_columns_trie[place2][2] = 0
                            elif list_trio_columns[place1][2] < list_trio_columns[place2][2]:
                                list_trio_columns_trie[place1][1] = "NULL"
                                list_trio_columns_trie[place1][2] = 0
                            else:
                                if abs(len(list_trio_columns[place1][0]) - len(list_trio_columns[place1][1])) < abs(
                                        len(list_trio_columns[place2][0]) - len(list_trio_columns[place1][1])):
                                    list_trio_columns_trie[place2][1] = "NULL"
                                    list_trio_columns_trie[place2][2] = 0
                                else:
                                    list_trio_columns_trie[place1][1] = "NULL"
                                    list_trio_columns_trie[place1][2] = 0

        ### Populate the table "Database Fields"
        self.dlg.tableWidget1.setColumnCount(3)
        column_names1 = ["Legacy data value", "Assigned standard value", "Modify the assigned value"]
        self.dlg.tableWidget1.setHorizontalHeaderLabels(column_names1)
        self.dlg.tableWidget3.setWordWrap(True)
        self.dlg.tableWidget1.horizontalHeader().setSectionResizeMode(QHeaderView.Interactive) #adjustable width of the columns
        self.dlg.tableWidget1.verticalHeader().setVisible(False) #vertical header (1 to n) invisible

        # Set the header font size
        header_font = self.dlg.tableWidget1.horizontalHeader().font()
        header_font.setPointSize(8)  # Adjust this value as needed
        self.dlg.tableWidget1.horizontalHeader().setFont(header_font)

        # Create a new legend widget and set it in the legend box for Table1
        legend_widget = self.create_legend_widget()
        legend_main_layout = QVBoxLayout(self.dlg.legendbox_3)
        legend_main_layout.setContentsMargins(0, 0, 0, 0)
        legend_main_layout.addWidget(legend_widget, stretch=1, alignment=Qt.AlignHCenter)
        self.dlg.legendbox_3.setLayout(legend_main_layout)

        # Compute the real maximum score among all rows (ignoring "Geometry")
        valid_scores = [score for (old, new, score) in list_trio_columns_trie if old != "Geometry"]
        max_score = max(valid_scores) if valid_scores else 1  # avoid division by zero

        # Set the row count once to the total number of unique structure triplets
        self.dlg.tableWidget1.setRowCount(len(list_trio_columns_trie)-1)

        for k, (old, new, score) in enumerate(list_trio_columns_trie):
            if old != "Geometry":
                # Create and set the legacy value item with custom font
                legacy_item = QTableWidgetItem(str(old))
                legacy_item.setFont(QFont("Arial", 8))  # Adjust font family and size as needed
                self.dlg.tableWidget1.setItem(k, 0, legacy_item)

                # Create and set the assigned value item with custom font
                assigned_item = QTableWidgetItem(str(new))
                assigned_item.setFont(QFont("Arial", 8))
                self.dlg.tableWidget1.setItem(k, 1, assigned_item)

                # Normalize the current row score based on the maximum score found
                new_score = (score / max_score) * 100.0 if max_score > 0 else 0

                # Use the Lipari colorscale helper to get the corresponding color
                new_color = self.lipari_color(new_score)

                # Apply the new color to both legacy and assigned cells
                for column in range(2):
                    item = self.dlg.tableWidget1.item(k, column)
                    if item:
                        item.setBackground(new_color)

                # Forbid editing of the first two columns
                legacy_item.setFlags(legacy_item.flags() & ~Qt.ItemIsEditable)
                assigned_item.setFlags(assigned_item.flags() & ~Qt.ItemIsEditable)

                # Create composite widget for "Modify the assigned value" column:
                modify_widget = QWidget(self.dlg)
                h_layout = QHBoxLayout(modify_widget)
                h_layout.setContentsMargins(0, 0, 0, 0)

                # QComboBox with alias options
                combo = QComboBox(modify_widget)
                for alias in self.alias_list:
                    combo.addItem(alias)
                if new in self.alias_list:
                    combo.setCurrentIndex(self.alias_list.index(new))
                else:
                    combo.setCurrentIndex(0)
                h_layout.addWidget(combo)

                # Connect the combo box so that when the selection changes, update_assigned_value is called
                combo.currentIndexChanged.connect(
                    lambda idx, row=k, combo=combo: self.update_assigned_value(row, combo.currentText()))

                # "Delete" button to remove the assigned alias
                btn_delete = QPushButton("Delete", modify_widget)
                btn_delete.setMinimumHeight(17)
                btn_delete.clicked.connect(lambda state, row=k: self.button_delete1(row))
                h_layout.addWidget(btn_delete)

                modify_widget.setLayout(h_layout)
                self.dlg.tableWidget1.setCellWidget(k, 2, modify_widget)

                # Set cell dimensions
                self.dlg.tableWidget1.resizeRowsToContents()
                self.dlg.tableWidget1.setColumnWidth(0, 185)
                self.dlg.tableWidget1.setColumnWidth(1, 185)
                self.dlg.tableWidget1.setColumnWidth(2, 185)

            # Initialise cell editing status and table state (if required)
            self.row_edit_status = [False] * len(list_trio_columns_trie)
            self.table1States = []
            self.table1States.append(self.getTableState1())

        return input_file

    # Retrieve the status of all tableWidget1 cells
    def getTableState1(self):

        state = []
        for row in range(self.dlg.tableWidget1.rowCount()):
            row_state = []
            for col in range(2):
                item = self.dlg.tableWidget1.item(row, col)

                # Text
                if isinstance(item, QTableWidgetItem):
                    cell_text = item.text()
                    cell_color = (
                        item.background().color().name()
                        if item.background().style() != Qt.NoBrush
                        else None
                    )

                # ComboBox
                elif isinstance(item, QWidget):
                    combo_box = self.dlg.tableWidget1.cellWidget(row, col)
                    if isinstance(combo_box, QComboBox):
                        cell_text = combo_box.currentText()
                    else:
                        cell_text = ""
                    cell_color = (
                        item.background().color().name()
                        if item.background().style() != Qt.NoBrush
                        else None
                    )
                else:
                    cell_text = ""
                    cell_color = None

                cell_state = {"text": cell_text, "color": cell_color}
                row_state.append(cell_state)
            state.append(row_state)

        return state

    # Delete row
    def button_delete1(self, row):

        # Saving the previous state of the displayboard
        self.table1States.append(self.getTableState1())

        for col in range(2):
            item = self.dlg.tableWidget1.item(row, col)
            if item is not None:
                item.setText("-")

        combo_box_item = self.dlg.tableWidget1.cellWidget(row, 1)

        if isinstance(combo_box_item, QComboBox):
            combo_box_item.hide()
            combo_box_item.setCurrentText("-")
            self.dlg.tableWidget1.setCellWidget(row, 1, combo_box_item)

        # Prohibit editing of these cells
        item1 = self.dlg.tableWidget1.item(row, 0)
        item2 = self.dlg.tableWidget1.item(row, 1)
        item1.setFlags(item1.flags() & ~Qt.ItemIsEditable)
        item2.setFlags(item2.flags() & ~Qt.ItemIsEditable)

        self.dlg.tableWidget1.repaint()

    # Modify row if column name didn't match
    def button_edit1(self, row):

        WAXI_projet_path = os.path.abspath(QgsProject.instance().fileName())
        emplacement_files_WAXI_columns = os.path.join(
            os.path.dirname(WAXI_projet_path),
            self.dir_99 + "/columns_reference_WAXI4.csv",
        )

        column_reference = pd.read_csv(emplacement_files_WAXI_columns)
        list_column_reference = column_reference.columns.tolist()

        # CASE 1 : If the line has already been deleted
        if (
            self.dlg.tableWidget1.item(row, 0).text() == "-"
            and self.dlg.tableWidget1.item(row, 1).text() == "-"
        ):
            return

        # CASE 2 : If the button is clicked for the 1st time
        if not self.row_edit_status[row]:

            self.table1States.append(self.getTableState1())

            current_text = self.dlg.tableWidget1.item(row, 1).text()

            combo_box = QComboBox()
            combo_box.addItems(list_column_reference)
            self.dlg.tableWidget1.setCellWidget(row, 1, combo_box)
            combo_box.setCurrentText(current_text)

            item = self.dlg.tableWidget1.item(row, 1)
            item.setFlags(item.flags() | Qt.ItemIsEditable)
            self.row_edit_status[row] = True

        # CASE 3 : f the button is clicked for the 2nd time
        else:
            combo_box = self.dlg.tableWidget1.cellWidget(row, 1)
            item = self.dlg.tableWidget1.item(row, 1)

            # ComboBox
            if isinstance(combo_box, QComboBox):
                selected_text = combo_box.currentText()
                self.dlg.tableWidget1.removeCellWidget(row, 1)

            # Text
            else:
                selected_text = item.text()

            item.setText(selected_text)

            table_state = self.getTableState1()
            cell_state = table_state[row][1]
            original_color = QColor(cell_state["color"])
            item.setBackground(original_color)

            item.setFlags(item.flags() & ~Qt.ItemIsEditable)
            self.row_edit_status[row] = False

        self.dlg.tableWidget1.repaint()

    #Update assigned value
    def update_assigned_value(self, row, new_alias):
        from fuzzywuzzy import fuzz  # ensure fuzz is imported
        legacy_item = self.dlg.tableWidget1.item(row, 0)
        if legacy_item is None:
            return
        legacy_value = legacy_item.text()
        new_score = fuzz.token_set_ratio(legacy_value, new_alias)  # new_score in [0, 100]
        assigned_item = self.dlg.tableWidget1.item(row, 1)
        if assigned_item:
            assigned_item.setText(new_alias)
        else:
            assigned_item = QTableWidgetItem(new_alias)
            self.dlg.tableWidget1.setItem(row, 1, assigned_item)
        # Instead of using HSV, use lipari_color helper:
        new_color = self.lipari_color(new_score)
        legacy_item.setBackground(new_color)
        assigned_item.setBackground(new_color)

        # Set the new background colour for both legacy and assigned value cells
        legacy_item.setBackground(new_color)
        assigned_item.setBackground(new_color)

    def update_assigned_value2(self, row, new_alias):
        from fuzzywuzzy import fuzz
        legacy_item = self.dlg.tableWidget2.item(row, 0)
        if legacy_item is None:
            return
        legacy_value = legacy_item.text()
        new_score = fuzz.token_set_ratio(legacy_value, new_alias)
        assigned_item = self.dlg.tableWidget2.item(row, 1)
        if assigned_item:
            assigned_item.setText(new_alias)
        else:
            assigned_item = QTableWidgetItem(new_alias)
            self.dlg.tableWidget2.setItem(row, 1, assigned_item)
        new_color = self.lipari_color(new_score)
        legacy_item.setBackground(new_color)
        assigned_item.setBackground(new_color)

    def update_assigned_value3(self, row, new_alias):
        from fuzzywuzzy import fuzz  # ensure fuzz is imported
        legacy_item = self.dlg.tableWidget3.item(row, 0)
        if legacy_item is None:
            return
        legacy_value = legacy_item.text()
        new_score = fuzz.token_set_ratio(legacy_value, new_alias)
        assigned_item = self.dlg.tableWidget3.item(row, 1)
        if assigned_item:
            assigned_item.setText(new_alias)
        else:
            assigned_item = QTableWidgetItem(new_alias)
            self.dlg.tableWidget3.setItem(row, 1, assigned_item)
        new_color = self.lipari_color(new_score)
        legacy_item.setBackground(new_color)
        assigned_item.setBackground(new_color)

    # Come Back
    def Go_Back_table1(self):

        if self.table1States:
            previousState = self.table1States.pop()
            for row, row_state in enumerate(previousState):
                for col, cell in enumerate(row_state):
                    # texte de la cellule
                    cell_text = cell["text"]
                    item = self.dlg.tableWidget1.item(row, col)
                    item.setText(cell_text)

                    # couleur de la cellule
                    cell_color = cell["color"]
                    if cell_color:
                        item.setBackground(QColor(cell_color))

        else:
            self.iface.messageBar().pushMessage(
                "No previous action !", level=Qgis.Warning, duration=45
            )

    # Content retrieval of QTableWidget 1 : COLUMNS NAMES CHECK  ##
    def recup_contenu_1(self):
        list_columns_check_OK = []
        new_values_count = {}

        for row in range(self.dlg.tableWidget1.rowCount()):
            if self.dlg.tableWidget1.item(row, 0):
                old = self.dlg.tableWidget1.item(row, 0).text()
                if old != "-":
                    # Retrieve the alias directly from the table
                    if isinstance(self.dlg.tableWidget1.cellWidget(row, 1), QComboBox):
                        alias = self.dlg.tableWidget1.cellWidget(row, 1).currentText()
                    else:
                        alias = self.dlg.tableWidget1.item(row, 1).text()

                    if alias != "NULL":
                        # No more reversing here—store the alias as-is
                        if alias in new_values_count:
                            self.iface.messageBar().pushMessage(
                                f"Erreur: La valeur '{alias}' est en double !",
                                level=Qgis.Warning,
                                duration=45,
                            )
                            return
                        new_values_count[alias] = 1

                        # (old, alias)
                        list_columns_check_OK.append([old, alias])

        list_columns_check_OK.append(["Geometry", "Geometry"])
        return list_columns_check_OK

    ###############################################################################
    ##########    Step 4 : Creation of a DataFrame sorted and checked      ########
    ###############################################################################

    def DataFrame_columns_check(self, input_file, list_columns_check, name_input_file):
        # Guard against None (so an empty mapping still yields the default cols)
        if list_columns_check is None:
            list_columns_check = []

        # list_columns_check might look like: [[old_legacy_field, alias], ... ]
        fichier_output = pd.DataFrame()
        list_columns_check3 = []

        for old_legacy_field, alias in list_columns_check:
            if alias != "NULL":
                try:
                    fichier_output[alias] = input_file[old_legacy_field]
                    list_columns_check3.append(alias)
                except KeyError:
                    pass

        # Add a column to specify Geographic Coordinates Reference System
        fichier_output["CRS"] = "WSG84_EPSG:4326"
        list_columns_check3.append("CRS")

        # Add a column to specify INPUT file name
        fichier_output["Source"] = name_input_file.split(".")[0]
        list_columns_check3.append("Source")

        # Add a column for RAW DATA
        fichier_output["Existing databases - raw data"] = None
        list_columns_check3.append("Existing databases - raw data")

        for index, row in input_file.iterrows():
            ligne_raw_data = []

            for column, value in row.items():
                if str(column) != "Geometry":
                    ligne_raw_data.append(f"{column} : {value} ; ")

            fichier_output.at[index, "Existing databases - raw data"] = "".join(
                ligne_raw_data
            )

        self.fichier_output_cp = fichier_output.copy(deep=True)

        return fichier_output, list_columns_check3

    ###############################################################################
    ###########    Step 5 : Fill Table2 with Lithologies pairs     ################
    ###############################################################################

    def fill_Table2(self, fichier_output):

        # from openpyxl import Workbook
        from fuzzywuzzy import fuzz

        # If user never mapped “Lithology - Outcrop Lithology,” skip with a warning:
        if "Lithology - Outcrop Lithology" not in fichier_output.columns:
            self.iface.messageBar().pushMessage(
                "No column named 'Lithology - Outcrop Lithology' found in your data. Lithology Names table will be empty.",
                level=Qgis.Warning,
                duration=10
            )
            return

        value_counts = self.fichier_output_cp["Lithology - Outcrop Lithology"].value_counts().to_dict()

        # List of input lithologies
        list_lithology_input = fichier_output["Lithology - Outcrop Lithology"].tolist()

        list_lithology_reference = self.get_first_column_text(
            self.dictionaries_path, "General__List of all lithologies"
        )

        # Empty list of lithology name pairs (input value, reference value)
        list_trio = []
        for k in range(0, len(list_lithology_input)):
            list_trio.append([list_lithology_input[k], "NULL", 0])

        # Identification with Fuzzywuzzy :
        for k in range(0, len(list_lithology_input)):

            # Initialize similarity score and new name
            score = 0
            new_rock_name = "NULL"

            if list_lithology_input[k] == "NULL" or list_lithology_input[k] == " ":

                list_trio[k][1] = "NULL"
                list_trio[k][2] = 0

            else:

                ## Condition 1 : prefix leuco, micro and meta to be deleted in front of names
                letters = [
                    "áéíóúàèìòùâêîôûäëïöüçÁÉÍÓÚÀÈÌÒÙÂÊÎÔÛÄËÏÖÜÇ",
                    "aeiouaeiouaeiouaeioucAEIOUAEIOUAEIOUAEIOUC",
                ]
                sylables = [
                    ["Micro-", ""],
                    ["micro-", ""],
                    ["Meta-", ""],
                    ["meta-", ""],
                    ["Micro", ""],
                    ["micro", ""],
                    ["Meta", ""],
                    ["meta", ""],
                    # modifiers
                    ["Micro-", ""],
                    ["micro-", ""],
                    ["Meta-", ""],
                    ["meta-", ""],
                    ["Micro", ""],
                    ["micro", ""],
                    ["Meta", ""],
                    [
                        "meta",
                        "",
                    ],
                    # abbreviations
                    [" sst ", "Sandstone"],
                    [" Sst ", "Sandstone"],
                    [" qtzite ", "Quartzite"],
                    [" Qtzite ", "Quartzite"],
                    [" qtz ", "Quartzite"],
                    [" QTZ ", "quartz"],
                    [" lst ", "Limestone"],
                    [" Lst ", "Limestone"],
                    # francophone translations
                    ["Argillite ", "Shale "],
                    ["Argillites ", "Shale "],
                    ["argillite ", "Shale "],
                    ["argillites ", "Shale "],
                    ["Argilite ", "Shale "],  # typo
                    ["Argilites ", "Shale "],  # typo
                    ["argilite ", "Shale "],  # typo
                    ["argilites ", "Shale "],  # typo
                    ["Gres ", "Sandstone "],
                    ["gres ", "Sandstone "],
                    ["Volcaniques ", "Volcanics "],
                    ["Volcanique ", "Volcanic "],
                    ["Volcanosediment ", "Volcanoclastic "],
                    ["Schiste ", "Schist "],
                    ["Schistes ", "Schist "],
                ]
                for i in range(len(letters[0])):
                    list_lithology_input[k] = list_lithology_input[k].replace(
                        letters[0][i], letters[1][i]
                    )

                for i in range(len(sylables)):
                    list_lithology_input[k] = list_lithology_input[k].replace(
                        sylables[i][0], sylables[i][1]
                    )

                for rock_reference in list_lithology_reference:

                    new_score = fuzz.token_set_ratio(
                        list_lithology_input[k], rock_reference
                    )

                    # If the 2 words don't begin with the same letter : -15 penalty on the score
                    if list_lithology_input[k][0].lower() != rock_reference[0].lower():
                        new_score = new_score - 15

                    if new_score > score:
                        score = new_score
                        new_rock_name = rock_reference

                # Add the name that matched the most
                list_trio[k][1] = new_rock_name

                # Add match score
                list_trio[k][2] = score

        # Creation of a lithology checklist for the user
        # Build list_trio_litho_no_duplicate from list_trio_struct by excluding duplicates and triplets with null legacy values
        list_trio_litho_no_duplicate = []
        seen_legacies = set()

        for trio in list_trio:
            legacy_value = str(trio[0]).strip()  # Convert to string and remove surrounding whitespace
            # Skip triplets where the legacy value is "NULL" (case-insensitive) or empty
            if legacy_value.upper() == "NULL" or legacy_value == "":
                continue
            if legacy_value not in seen_legacies:
                seen_legacies.add(legacy_value)
                list_trio_litho_no_duplicate.append(trio)

        ###    Filling 2 : LITHOLOGIES NAME ## input = list_couple     ###

        ### Populate the table "Lithology Names"
        self.dlg.tableWidget2.setColumnCount(3)
        column_names2 = ["Legacy data value", "Assigned standard value", "Modify the assigned value"]
        self.dlg.tableWidget2.setHorizontalHeaderLabels(column_names2)
        self.dlg.tableWidget2.horizontalHeader().setSectionResizeMode(QHeaderView.Interactive)  # interactive width of the columns
        self.dlg.tableWidget2.verticalHeader().setVisible(False)  # vertical header (1 to n) invisible
        self.dlg.tableWidget2.setWordWrap(True) # Enable word wrap on the table widget so that cells wrap text

        # Set the header font size
        header_font = self.dlg.tableWidget2.horizontalHeader().font()
        header_font.setPointSize(8)  # Adjust this value as needed
        self.dlg.tableWidget2.horizontalHeader().setFont(header_font)

        # Create a new legend widget and set it in the legend box
        legend_widget = self.create_legend_widget()
        legend_main_layout = QVBoxLayout(self.dlg.legendbox)
        legend_main_layout.setContentsMargins(0, 0, 0, 0)
        legend_main_layout.addWidget(legend_widget, stretch=1, alignment=Qt.AlignHCenter)
        self.dlg.legendbox.setLayout(legend_main_layout)

        # Re-organize the list of unique lithology pairs (sorted in ascending order of score)
        list_trio_litho_no_duplicate = sorted(list_trio_litho_no_duplicate, key=lambda x: x[2], reverse=False)

        # Dynamically compute the maximum score (ignoring any rows with a "-" legacy value)
        valid_scores = [score for (old, new, score) in list_trio_litho_no_duplicate if old != "-"]
        max_score = max(valid_scores) if valid_scores else 1  # Avoid division by zero

        # Set the row count once to the total number of unique structure triplets
        self.dlg.tableWidget2.setRowCount(len(list_trio_litho_no_duplicate))

        for k, (old, new, score) in enumerate(list_trio_litho_no_duplicate):
            # Prepare the legacy text; here we append the count if available
            legacy_text = str(old) + " : " + str(value_counts.get(old, 0))
            legacy_item = QTableWidgetItem(legacy_text)
            legacy_item.setFont(QFont("Arial", 8))
            self.dlg.tableWidget2.setItem(k, 0, legacy_item)

            # Set the assigned value cell
            assigned_item = QTableWidgetItem(str(new))
            assigned_item.setFont(QFont("Arial", 8))
            self.dlg.tableWidget2.setItem(k, 1, assigned_item)

            # Normalize the current row's score relative to the dynamic maximum
            new_score = (score / max_score) * 100.0 if max_score > 0 else 0
            # Get the corresponding color from the Lipari colorscale
            new_color = self.lipari_color(new_score)

            # Apply the color to both legacy and assigned cells
            for column in range(2):
                item = self.dlg.tableWidget2.item(k, column)
                if item:
                    item.setBackground(new_color)

            # Forbid editing of first 2 columns
            item1 = self.dlg.tableWidget2.item(k, 0)
            item2 = self.dlg.tableWidget2.item(k, 1)
            item1.setFlags(item1.flags() & ~Qt.ItemIsEditable)
            item2.setFlags(item2.flags() & ~Qt.ItemIsEditable)

            # Delete and Edit actions:
            modify_widget = QWidget(self.dlg)
            h_layout = QHBoxLayout(modify_widget)
            h_layout.setContentsMargins(0, 0, 0, 0)

            # QComboBox with lithology options (using the reference list of lithologies)
            combo = QComboBox(modify_widget)
            for litho in list_lithology_reference:
                combo.addItem(litho)
            if new in list_lithology_reference:
                combo.setCurrentIndex(list_lithology_reference.index(new))
            else:
                combo.setCurrentIndex(0)
            h_layout.addWidget(combo)

            # Connect the combo box change signal to update the assigned value and recalc the matching score
            combo.currentIndexChanged.connect(
                lambda idx, row=k, combo=combo: self.update_assigned_value2(row, combo.currentText()))

            # "Delete" button remains
            btn_delete = QPushButton("Delete", modify_widget)
            btn_delete.setMinimumHeight(17)
            btn_delete.clicked.connect(lambda state, row=k: self.button_delete2(row))
            h_layout.addWidget(btn_delete)

            modify_widget.setLayout(h_layout)
            self.dlg.tableWidget2.setCellWidget(k, 2, modify_widget)

            # Set cells dimension
            self.dlg.tableWidget2.resizeRowsToContents()
            self.dlg.tableWidget2.setColumnWidth(0, 185)
            self.dlg.tableWidget2.setColumnWidth(1, 185)
            self.dlg.tableWidget2.setColumnWidth(2, 185)

        # Initialize cell editing status + actions performed in table
        self.row_edit_status = [False] * len(list_trio_litho_no_duplicate)
        self.table2States = []
        self.table2States.append(self.getTableState2())

    # Retrieve the status of all tableWidget2 cells
    def getTableState2(self):
        state2 = []
        for row in range(self.dlg.tableWidget2.rowCount()):
            row_state = []
            for col in range(2):
                item = self.dlg.tableWidget2.item(row, col)

                # Text
                if isinstance(item, QTableWidgetItem):
                    cell_text = item.text()
                    cell_color = (
                        item.background().color().name()
                        if item.background().style() != Qt.NoBrush
                        else None
                    )

                # ComboBox
                elif isinstance(item, QWidget):
                    combo_box = self.dlg.tableWidget2.cellWidget(row, col)
                    if isinstance(combo_box, QComboBox):
                        cell_text = combo_box.currentText()
                    else:
                        cell_text = ""
                    cell_color = (
                        item.background().color().name()
                        if item.background().style() != Qt.NoBrush
                        else None
                    )
                else:
                    cell_text = ""
                    cell_color = None

                cell_state = {"text": cell_text, "color": cell_color}
                row_state.append(cell_state)
            state2.append(row_state)
        return state2

    # Delete row
    def button_delete2(self, row):

        self.table2States.append(self.getTableState2())

        for col in range(2):
            item = self.dlg.tableWidget2.item(row, col)
            if item is not None:
                item.setText("-")

        combo_box_item = self.dlg.tableWidget2.cellWidget(row, 1)

        if isinstance(combo_box_item, QComboBox):
            combo_box_text = combo_box_item.currentText()
            combo_box_item.hide()
            combo_box_item.setCurrentText("-")
            self.dlg.tableWidget2.setCellWidget(row, 1, combo_box_item)

        item1 = self.dlg.tableWidget2.item(row, 0)
        item2 = self.dlg.tableWidget2.item(row, 1)
        item1.setFlags(item1.flags() & ~Qt.ItemIsEditable)
        item2.setFlags(item2.flags() & ~Qt.ItemIsEditable)

        self.dlg.tableWidget2.repaint()

    # Come back
    def Go_Back_table2(self):

        if self.table2States:
            previousState = self.table2States.pop()
            for row, row_state in enumerate(previousState):
                for col, cell in enumerate(row_state):

                    cell_text = cell["text"]
                    item = self.dlg.tableWidget2.item(row, col)
                    item.setText(cell_text)

                    cell_color = cell["color"]
                    if cell_color:
                        item.setBackground(QColor(cell_color))

        else:
            self.iface.messageBar().pushMessage(
                "No previous action !", level=Qgis.Warning, duration=45
            )

    ## Retrieving QTableWidget 2 content : LITHOLOGIES NAMES CHECK  ##

    def recup_contenu_2(self):
        list_lithologies_unique_check_OK = []
        for row in range(self.dlg.tableWidget2.rowCount()):
            old = self.dlg.tableWidget2.item(row, 0).text().split(" :")[0]
            if old != "-":
                # Retrieve the alias
                if isinstance(self.dlg.tableWidget2.cellWidget(row, 1), QComboBox):
                    alias = self.dlg.tableWidget2.cellWidget(row, 1).currentText()
                else:
                    alias = self.dlg.tableWidget2.item(row, 1).text()

                # Convert alias → real_name
                real_name = self.reverse_alias_mapping.get(alias, alias)
                list_lithologies_unique_check_OK.append([old, real_name])

        return list_lithologies_unique_check_OK

    ###############################################################################
    ###        Step 7 : Lithologies sorting in differents Excel sheets          ###
    ###############################################################################

    def lithologies_sorting(
        self,
        fichier_output,
        list_columns_check3,
        list_lithologies_unique_check_OK,
        name_input_file,
    ):

        # from openpyxl import Workbook
        from fuzzywuzzy import fuzz

        # Add lithologies to Excel file

        list_old = []
        for k in range(len(list_lithologies_unique_check_OK)):
            list_old.append(list_lithologies_unique_check_OK[k][0])

        fichier_output = fichier_output[fichier_output["Lithology - Outcrop Lithology"].isin(list_old)]
        fichier_output = fichier_output.reset_index(drop=True)

        # We browse the column containing the old lithos in file_output
        for m in range(len(fichier_output["Lithology - Outcrop Lithology"])):

            # We browse the list of unique (old, new) pairs verified by the user
            for k in range(len(list_lithologies_unique_check_OK)):

                if (
                    fichier_output["Lithology - Outcrop Lithology"].iloc[m]
                    == list_lithologies_unique_check_OK[k][0]
                ):
                    fichier_output["Lithology - Outcrop Lithology"].iloc[m] = list_lithologies_unique_check_OK[
                        k
                    ][1]

        ### Sorting lithologies ###

        # Lists of reference rocks from the WAXI4 QGIS project

        litho_local = self.get_first_column_text(
            self.dictionaries_path, "Local lithologies__List of lithologies"
        )
        litho_supergene = self.get_first_column_text(
            self.dictionaries_path, "Supergene lithologies__List of lithologies"
        )
        litho_sedimentary = self.get_first_column_text(
            self.dictionaries_path, "Sedimentary lithologies__List of lithologies"
        )
        litho_volcanoclastic = self.get_first_column_text(
            self.dictionaries_path,
            "Volcanoclastic lithologies__List of lithologies (clast size-based)",
        )
        litho_igneous_extrusive = self.get_first_column_text(
            self.dictionaries_path, "Igneous extrusive lithologies__List of lithologies"
        )
        litho_igneous_intrusive = self.get_first_column_text(
            self.dictionaries_path, "Igneous intrusive lithologies__List of lithologies"
        )
        litho_metamorphic = self.get_first_column_text(
            self.dictionaries_path, "Metamorphic lithologies__List of lithologies"
        )

        ### Creation of the output Workbook ###

        fichier_output_lithology = {}  # create dictionary of pandas dataframes
        project = QgsProject.instance()

        worksheets_lithology = [
            "Local lithologies_PT",
            "Supergene lithologies_PT",
            "Sedimentary lithologies_PT",
            "Volcanoclastic lithologies_PT",
            "Igneous extrusive lithologies_PT",
            "Igneous intrusive lithologies_PT",
            "Metamorphic lithologies_PT",
        ]

        for litho_class in worksheets_lithology:

            # Retrieving the reference layer in QGIS
            layer = project.mapLayersByName(litho_class)[0]

            # Retrieve field names from the attribute table for this layer
            header = [str(field.name()) for field in layer.fields()]

            basename = os.path.basename(name_input_file)
            filename_without_extension = os.path.splitext(basename)[0]
            # Creation of the worksheet
            name_worksheet1 = litho_class + "_" + filename_without_extension
            fichier_output_lithology[name_worksheet1] = pd.DataFrame(columns=header)

        for k in range(0, len(fichier_output["Lithology - Outcrop Lithology"])):

            r = fichier_output["Lithology - Outcrop Lithology"][k]

            # litho_local :
            if r in litho_local and r != "Unknown":
                fichier_output_lithology = self.add_row_to_fichier_output_lithology(
                    k,
                    fichier_output_lithology,
                    fichier_output,
                    "Local lithologies_PT_",
                    list_columns_check3,
                    filename_without_extension,
                )

            # litho_supergene :
            elif r in litho_supergene and r != "Unknown":
                fichier_output_lithology = self.add_row_to_fichier_output_lithology(
                    k,
                    fichier_output_lithology,
                    fichier_output,
                    "Supergene lithologies_PT_",
                    list_columns_check3,
                    filename_without_extension,
                )

            # litho_sedimentary :
            elif r in litho_sedimentary and r != "Unknown":
                fichier_output_lithology = self.add_row_to_fichier_output_lithology(
                    k,
                    fichier_output_lithology,
                    fichier_output,
                    "Sedimentary lithologies_PT_",
                    list_columns_check3,
                    filename_without_extension,
                )

            # litho_volcanoclastic :
            elif r in litho_volcanoclastic and r != "Unknown":
                fichier_output_lithology = self.add_row_to_fichier_output_lithology(
                    k,
                    fichier_output_lithology,
                    fichier_output,
                    "Volcanoclastic lithologies_PT_",
                    list_columns_check3,
                    filename_without_extension,
                )

            # litho_volcanic :
            elif r in litho_igneous_extrusive and r != "Unknown":
                fichier_output_lithology = self.add_row_to_fichier_output_lithology(
                    k,
                    fichier_output_lithology,
                    fichier_output,
                    "Igneous extrusive lithologies_PT_",
                    list_columns_check3,
                    filename_without_extension,
                )

            # litho_plutonic :
            elif r in litho_igneous_intrusive and r != "Unknown":
                """print(k,
                fichier_output_lithology,
                fichier_output,
                "Igneous intrusive lithologies_PT_",
                list_columns_check3,
                filename_without_extension)"""
                fichier_output_lithology = self.add_row_to_fichier_output_lithology(
                    k,
                    fichier_output_lithology,
                    fichier_output,
                    "Igneous intrusive lithologies_PT_",
                    list_columns_check3,
                    filename_without_extension,
                )

            # litho_metamorphic :
            elif r in litho_metamorphic and r != "Unknown":
                fichier_output_lithology = self.add_row_to_fichier_output_lithology(
                    k,
                    fichier_output_lithology,
                    fichier_output,
                    "Metamorphic lithologies_PT_",
                    list_columns_check3,
                    filename_without_extension,
                )
        return fichier_output_lithology

    # add each row to the master dictionary of pandas
    def add_row_to_fichier_output_lithology(
            self,
            k,
            fichier_output_lithology,
            fichier_output,
            name_prefix,
            list_columns_check3,
            filename_without_extension,
    ):
        full_name = name_prefix + filename_without_extension
        header_local = fichier_output_lithology[full_name].columns

        ligne = []
        for col_reference in header_local:
            # Convert real name to alias:
            alias_for_col = self.alias_mapping.get(col_reference, col_reference)
            if alias_for_col in fichier_output.columns:
                ligne.append(fichier_output.at[k, alias_for_col])
            else:
                ligne.append("")

        # If "Reference" is in the columns and not assigned in the DAtabase Fields table, set it to the imported layer name.
        if "Reference" in header_local and "Reference" not in list_columns_check3:
            ref_index = header_local.get_loc("Reference")
            ligne[ref_index] = filename_without_extension
            
        fichier_output_lithology[full_name].loc[len(fichier_output_lithology[full_name])] = ligne
        return fichier_output_lithology

    ###############################################################################
    ###########    Step 8 : Fill Table3 with Structure pairs       ################
    ###############################################################################

    def fill_Table3(self, fichier_output):

        # from openpyxl import Workbook
        from fuzzywuzzy import fuzz

        #Check
        if "Structures - Structure Type" not in fichier_output.columns:
            self.iface.messageBar().pushMessage(
                "No column named 'Structures - Structure Type' found in your data. Structure Types table will be empty.",
                level=Qgis.Warning,
                duration=10
            )
            return

        '''# List of input structures
        list_structure_input = fichier_output["Structures - Structure Type"].tolist()

        WAXI_projet_path = os.path.abspath(QgsProject.instance().fileName())
        emplacement_files_WAXI_columns = os.path.join(
            os.path.dirname(WAXI_projet_path),
            self.dir_99 + "/columns_types_structures_WAXI4.csv",
        )

        Dataframe = pd.read_csv(emplacement_files_WAXI_columns)'''

        # Load the structure‐types lookup from the plugin folder
        columns_csv = os.path.join(self.plugin_dir, "columns_types_structures.csv")
        if not os.path.isfile(columns_csv):
            self.iface.messageBar().pushMessage(
                f"ERROR: Cannot find {columns_csv}", level=Qgis.Critical, duration=10
            )
            return
        Dataframe = pd.read_csv(columns_csv)

        # Empty list of structure name pairs (input value, reference value)
        list_structure_input = fichier_output["Structures - Structure Type"].tolist()
        list_trio_struct = []
        for k in range(0, len(list_structure_input)):
            list_trio_struct.append([list_structure_input[k], "NULL", 0])

        # Structure_type column modified to conform
        for k in range(0, len(list_structure_input)):

            type_structure = list_structure_input[k]

            if type_structure == "NULL" or type_structure == " ":

                list_trio_struct[k][1] = "NULL"
                list_trio_struct[k][2] = 0

            else:
                score = 0
                structure = "NULL"

                # Go through all the boxes in the Excel file to determine the type of structures
                for index, row in Dataframe.iloc[1:].iterrows():
                    for colonne, valeur in row.items():

                        new_score = fuzz.token_set_ratio(type_structure, valeur)

                        if new_score > score:
                            score = new_score
                            structure = colonne

                if score < 50 and (
                    "vein" in type_structure or "veins" in type_structure
                ):
                    list_trio_struct[k][1] = "Veins_PT"
                    list_trio_struct[k][2] = 90

                else:
                    list_trio_struct[k][1] = structure
                    list_trio_struct[k][2] = score

        # Create a structure checklist for the user
        # Build list_trio_struct_no_duplicate from list_trio_struct by excluding duplicates and triplets with null legacy values
        list_trio_struct_no_duplicate = []
        seen_legacies = set()

        for trio in list_trio_struct:
            legacy_value = str(trio[0]).strip()  # Convert to string and remove surrounding whitespace
            # Skip triplets where the legacy value is "NULL" (case-insensitive) or empty
            if legacy_value.upper() == "NULL" or legacy_value == "":
                continue
            if legacy_value not in seen_legacies:
                seen_legacies.add(legacy_value)
                list_trio_struct_no_duplicate.append(trio)

        ###    Filling 3 : STRUCTURE NAME ## input = list_trio_struct_no_duplicate     ###

        ### Populate the table "Structures Types"
        self.dlg.tableWidget3.setColumnCount(3)
        column_names3 = ["Legacy data value", "Assigned standard value", "Modify the assigned value"]
        self.dlg.tableWidget3.setHorizontalHeaderLabels(column_names3)
        self.dlg.tableWidget3.horizontalHeader().setSectionResizeMode(QHeaderView.Interactive)  # adjustable width of the columns
        self.dlg.tableWidget3.verticalHeader().setVisible(False)  # vertical header (1 to n) invisible
        self.dlg.tableWidget3.setWordWrap(True) # Enable word wrap on the table widget so that cells wrap text

        # Set the header font size
        header_font = self.dlg.tableWidget3.horizontalHeader().font()
        header_font.setPointSize(8)  # Adjust this value as needed
        self.dlg.tableWidget3.horizontalHeader().setFont(header_font)

        # Create a new legend widget and set it in the legend box
        legend_widget = self.create_legend_widget()
        legend_main_layout = QVBoxLayout(self.dlg.legendbox_2)
        legend_main_layout.setContentsMargins(0, 0, 0, 0)
        legend_main_layout.addWidget(legend_widget, stretch=1, alignment=Qt.AlignHCenter)
        self.dlg.legendbox_2.setLayout(legend_main_layout)

        # Assume list_trio_struct_no_duplicate is a list of structure pairs (old, new, score)
        list_trio_struct_no_duplicate = sorted(list_trio_struct_no_duplicate, key=lambda x: x[2], reverse=False)

        # Dynamically compute the maximum score among rows (ignoring rows where legacy is "-")
        valid_scores = [score for (old, new, score) in list_trio_struct_no_duplicate if old != "-"]
        max_score = max(valid_scores) if valid_scores else 1  # Avoid division by zero

        # Set the row count once to the total number of unique structure triplets
        self.dlg.tableWidget3.setRowCount(len(list_trio_struct_no_duplicate))

        for k, (old, new, score) in enumerate(list_trio_struct_no_duplicate):
            # Create and set the legacy value item with custom font
            legacy_item = QTableWidgetItem(str(old))
            legacy_item.setFont(QFont("Arial", 8))
            self.dlg.tableWidget3.setItem(k, 0, legacy_item)

            # Create and set the assigned value item with custom font
            assigned_item = QTableWidgetItem(str(new))
            assigned_item.setFont(QFont("Arial", 8))
            self.dlg.tableWidget3.setItem(k, 1, assigned_item)

            # Normalize the score dynamically relative to the real maximum
            new_score = (score / max_score) * 100.0 if max_score > 0 else 0
            # Get the color using Lipari colorscale helper (which maps 0-100 to a continuous color)
            new_color = self.lipari_color(new_score)

            # Apply the new color as the background for both cells
            for col in range(2):
                item = self.dlg.tableWidget3.item(k, col)
                if item:
                    item.setBackground(new_color)


            # Forbid editing of first 2 columns
            item1 = self.dlg.tableWidget3.item(k, 0)
            item2 = self.dlg.tableWidget3.item(k, 1)
            item1.setFlags(item1.flags() & ~Qt.ItemIsEditable)
            item2.setFlags(item2.flags() & ~Qt.ItemIsEditable)

            # Create composite widget for "Modify the assigned value" column:
            modify_widget = QWidget(self.dlg)
            h_layout = QHBoxLayout(modify_widget)
            h_layout.setContentsMargins(0, 0, 0, 0)

            # Create a QComboBox for structure choices.
            # (Assuming you extract structure options from your DataFrame)
            structure_options = list(Dataframe.columns)
            combo = QComboBox(modify_widget)
            for option in structure_options:
                combo.addItem(option)
            if new in structure_options:
                combo.setCurrentIndex(structure_options.index(new))
            else:
                combo.setCurrentIndex(0)
            h_layout.addWidget(combo)

            # Connect the combo box signal to update the assigned value and recalc the matching score.
            combo.currentIndexChanged.connect(
                lambda idx, row=k, combo=combo: self.update_assigned_value3(row, combo.currentText())
            )

            # Create the Delete button.
            btn_delete = QPushButton("Delete", modify_widget)
            btn_delete.setMinimumHeight(17)
            btn_delete.clicked.connect(lambda state, row=k: self.button_delete3(row))
            h_layout.addWidget(btn_delete)

            modify_widget.setLayout(h_layout)
            self.dlg.tableWidget3.setCellWidget(k, 2, modify_widget)

            # Set cell dimensions
            self.dlg.tableWidget3.resizeRowsToContents()
            self.dlg.tableWidget3.setColumnWidth(0, 185)
            self.dlg.tableWidget3.setColumnWidth(1, 185)
            self.dlg.tableWidget3.setColumnWidth(2, 185)

        # Initialize cell editing status + actions performed in table
        self.row_edit_status = [False] * len(list_trio_struct_no_duplicate)
        self.table3States = []
        self.table3States.append(self.getTableState3())

    # Retrieve the status of all tableWidget3 cells
    def getTableState3(self):

        state3 = []
        for row in range(self.dlg.tableWidget3.rowCount()):
            row_state = []
            for col in range(2):
                item = self.dlg.tableWidget3.item(row, col)

                # Text
                if isinstance(item, QTableWidgetItem):
                    cell_text = item.text()
                    cell_color = (
                        item.background().color().name()
                        if item.background().style() != Qt.NoBrush
                        else None
                    )

                # ComboBox
                elif isinstance(item, QWidget):
                    combo_box = self.dlg.tableWidget3.cellWidget(row, col)
                    if isinstance(combo_box, QComboBox):
                        cell_text = combo_box.currentText()
                    else:
                        cell_text = ""
                    cell_color = (
                        item.background().color().name()
                        if item.background().style() != Qt.NoBrush
                        else None
                    )
                else:
                    cell_text = ""
                    cell_color = None

                cell_state = {"text": cell_text, "color": cell_color}
                row_state.append(cell_state)
            state3.append(row_state)

        return state3

    # Delete row
    def button_delete3(self, row):

        self.table3States.append(self.getTableState3())

        for col in range(2):
            item = self.dlg.tableWidget3.item(row, col)
            if item is not None:
                item.setText("-")

        combo_box_item = self.dlg.tableWidget3.cellWidget(row, 1)

        if isinstance(combo_box_item, QComboBox):
            combo_box_item.hide()
            combo_box_item.setCurrentText("-")
            self.dlg.tableWidget3.setCellWidget(row, 1, combo_box_item)

        item1 = self.dlg.tableWidget3.item(row, 0)
        item2 = self.dlg.tableWidget3.item(row, 1)
        item1.setFlags(item1.flags() & ~Qt.ItemIsEditable)
        item2.setFlags(item2.flags() & ~Qt.ItemIsEditable)

        self.dlg.tableWidget3.repaint()

    # Come back
    def Go_Back_table3(self):

        if self.table3States:
            previousState = self.table3States.pop()
            for row, row_state in enumerate(previousState):
                for col, cell in enumerate(row_state):

                    cell_text = cell["text"]
                    item = self.dlg.tableWidget3.item(row, col)
                    item.setText(cell_text)

                    cell_color = cell["color"]
                    if cell_color:
                        item.setBackground(QColor(cell_color))

        else:
            self.iface.messageBar().pushMessage(
                "No previous action !", level=Qgis.Warning, duration=45
            )

    ## Retrieving QTableWidget 2 content : LITHOLOGIES NAMES CHECK  ##

    def recup_contenu_3(self):
        """
        Collects the final user-verified mapping for Structures from tableWidget3.
        Returns a list of sublists, each of the form:
          [old_legacy_value, assigned_structure, standard_layer, Type, Kinematics]
        """
        print("Enter recup_contenu_3")
        structure_map = []
        # List of standard mappings: each sublist is
        # [legacy value, standard "Structures - Structure Type", standard layer, Type]
        AssignedStructures = [
            ['','Lineations - Boudin', 'Lineations_PT', 'Boudin'],
            ['','Lineations - Crenulation Lineation', 'Lineations_PT', 'Lc'],
            ['','Lineations - Intersection Lineation', 'Lineations_PT', 'Li'],
            ['','Lineations - Mineral Lineation', 'Lineations_PT', 'Lm'],
            ['','Lineations - Stretching Lineation', 'Lineations_PT', 'Ls'],
            ['','Lineations - Slickenside', 'Lineations_PT', 'Slickenside'],
            ['','Lineations_PT - Paleoflow Direction', 'Lineations_PT', 'Paleoflow direction'],
            ['','Lineations - Striations, Groovings, Casts', 'Lineations_PT', 'Striations, groovings, casts'],
            ['','Lineations_PT - UST', 'Lineations_PT', 'UST'],
            ['','Lineations_PT - Bearing', 'Lineations_PT', 'Bearing lineation'],
            ['','Bedding-Lava flow-S0_PT - Unknown Polarity', 'Bedding-Lava flow-S0_PT', 'Unknown'],
            ['','Bedding-Lava flow-S0_PT - Normal Polarity', 'Bedding-Lava flow-S0_PT', 'Normal'],
            ['','Bedding-Lava flow-S0_PT - Reverse Polarity', 'Bedding-Lava flow-S0_PT', 'Inverted'],
            ['','Foliation-cleavage_PT', 'Foliation-cleavage_PT', ''],
            ['','Shear zones and faults_PT - Unclear Kinematics', 'Shear zones and faults_PT', 'Unclear'],
            ['','Shear zones and faults_PT - Flattening - pure shear', 'Shear zones and faults_PT', 'Flattening - pure shear'],
            ['','Shear zones and faults_PT - Normal-Slip', 'Shear zones and faults_PT', 'Normal-slip'],
            ['','Shear zones and faults_PT - Low-Angle Detachment', 'Shear zones and faults_PT', 'Low-angle detachment'],
            ['','Shear zones and faults_PT - Reverse-Slip', 'Shear zones and faults_PT', 'Reverse-slip'],
            ['','Shear zones and faults_PT - Dextral-Slip', 'Shear zones and faults_PT', 'Dextral-slip'],
            ['','Shear zones and faults_PT - Sinistral-Slip', 'Shear zones and faults_PT', 'Sinistral-slip'],
            ['','Folds_PT - Unknown, Recumbent, Vertical', 'Folds_PT', 'Unknown'],
            ['','Folds_PT - Anticline-Antiform', 'Folds_PT', 'Antiform'],
            ['','Folds_PT - Syncline-Synform', 'Folds_PT', 'Synform'],
            ['','Folds_PT - M-shaped', 'Folds_PT', 'M fold'],
            ['','Folds_PT - S-shaped', 'Folds_PT', 'S fold'],
            ['','Folds_PT - Z-shaped', 'Folds_PT', 'Z fold'],
            ['','Fractures_PT', 'Fractures_PT', ''],
            ['','Veins_PT', 'Veins_PT', ''],
            ['','Dikes-Sills_PT', 'Dikes-Sills_PT', ''],
            ['','Lithological contacts_PT', 'Lithological contacts_PT', ''],
        ]

        for row in range(self.dlg.tableWidget3.rowCount()):
            item0 = self.dlg.tableWidget3.item(row, 0)
            if not item0:
                continue
            old_value = item0.text().strip()
            if not old_value or old_value == "-":
                continue

            widget1 = self.dlg.tableWidget3.cellWidget(row, 1)
            if isinstance(widget1, QComboBox):
                assigned_value = widget1.currentText().strip()
            else:
                itm = self.dlg.tableWidget3.item(row, 1)
                assigned_value = itm.text().strip() if itm else "NULL"

            # find matching by alias (mapping[1]):
            matched = False
            for std in AssignedStructures:
                if std[1] == assigned_value:
                    print(f"Standard value selected in AssignedStructures: {assigned_value}")
                    structure_map.append([
                        old_value,  # original text
                        assigned_value,  # alias chosen
                        std[2],  # standard layer name
                        std[3],  # Type
                    ])
                    print(f"Updated structure_map: {structure_map}")
                    matched = True
                    break

            if not matched:
                print(f"Standard value selected not in AssignedStructures: {assigned_value}")
                structure_map.append([
                    old_value,
                    assigned_value,
                    "Unknown",
                    "",  # no Type
                ])
                print(f"Updated structure_map: {structure_map}")

        print(f"[DEBUG] Built structure_map with {len(structure_map)} entries:")
        for entry in structure_map:
            print("  ", entry)

        return structure_map

    ###############################################################################
    ######         Step 9 : Structure sorting in differents Excel sheets     ######
    ###############################################################################

    def structure_sorting(self, fichier_output, structure_map, list_columns_check3, name_input_file):
        """
        Sorts the rows in fichier_output into multiple structure-type DataFrames
        based on the user-confirmed mapping.
        Each mapping in structure_map is a list with:
          [old_legacy_value, assigned_structure, standard_layer, Type, Kinematics]
        """
        print("Enter structure_sorting")
        # 1) Build lookup dict
        map_dict = {}
        for old_val, assigned, layer, type_val in structure_map:
            key = old_val.strip()
            # normalize empty → None
            t = type_val.strip() or None
            map_dict[key] = {
                "assigned": assigned,
                "layer": layer,
                "Type": t,
            }
            print(f"[DEBUG] mapping for '{key}': layer='{layer}', Type='{t}'")

        # 2) Ensure DataFrame has the columns we need
        for col in ("Type", "Standard_Layer"):
            if col not in fichier_output.columns:
                fichier_output[col] = ""

        # 3) Apply to each row
        for idx, row in fichier_output.iterrows():
            legacy = str(row.get("Structures - Structure Type", "")).strip()
            if legacy in map_dict:
                info = map_dict[legacy]
                fichier_output.at[idx, "Structures - Structure Type"] = info["assigned"]
                fichier_output.at[idx, "Standard_Layer"] = info["layer"]
                if info["Type"] is not None:
                    fichier_output.at[idx, "Type"] = info["Type"]
                    print(f"[DEBUG] Row {idx}: set Type='{info['Type']}'")
            else:
                print(f"[DEBUG] Row {idx}: no mapping for '{legacy}', skipping")

        # 3) Create empty DataFrames for each known structure layer.
        project = QgsProject.instance()
        structure_layer_names = [
            "Lineations_PT",
            "Bedding-Lava flow-S0_PT",
            "Foliation-cleavage_PT",
            "Shear zones and faults_PT",
            "Folds_PT",
            "Fractures_PT",
            "Veins_PT",
            "Dikes-Sills_PT",
            "Lithological contacts_PT"
        ]
        fichier_output_structures = {}
        base_filename = os.path.splitext(os.path.basename(name_input_file))[0]

        for struct_layer_name in structure_layer_names:
            layer_list = project.mapLayersByName(struct_layer_name)
            if not layer_list:
                continue
            # For each found layer, define the header from its fields:
            header = [field.name() for field in layer_list[0].fields()]
            # Only add "Reference" if it was not assigned in Table1.
            if "Reference" not in list_columns_check3 and "Reference" not in header:
                header.append("Reference")
            df_key = f"{struct_layer_name}_{base_filename}"
            # Create a new DataFrame with this header.
            fichier_output_structures[df_key] = pd.DataFrame(columns=header)
        print(f"Fichier_output_structures with headers, before filling:{fichier_output_structures}")

        # 4) Append each row of fichier_output to the appropriate structure-type DataFrame.
        for i in range(len(fichier_output)):
            standard_layer = (fichier_output.loc[i, "Standard_Layer"]
            if "Standard_Layer" in fichier_output.columns
            else fichier_output.loc[i, "Structures - Structure Type"])
            if standard_layer in structure_layer_names:
                df_key = f"{standard_layer}_{base_filename}"
                if df_key not in fichier_output_structures:
                    continue
                target_df = fichier_output_structures[df_key]
                print(f"Targeted dataframe before append:\n{target_df}")  # debug
                row_to_append = []
                for col_reference in target_df.columns:
                    lc = col_reference.lower()
                    if lc == "type":
                        # copy the Type field
                        value = fichier_output.at[i, "Type"]
                    else:
                        # fall back via alias mapping
                        alias_for_col = self.alias_mapping.get(col_reference, col_reference)
                        if alias_for_col in fichier_output.columns:
                            value = fichier_output.at[i, alias_for_col]
                        else:
                            value = ""
                    row_to_append.append(value)
                # If "Reference" exists and was not assigned in Table1, fill it.
                if "Reference" in target_df.columns and "Reference" not in list_columns_check3:
                    ref_index = target_df.columns.get_loc("Reference")
                    row_to_append[ref_index] = base_filename
                target_df.loc[len(target_df)] = row_to_append
                print(f"Row to append to the targeted dataframe: {row_to_append}")

        return fichier_output_structures


    ###############################################################################
    ##   Step 9 : Import the 2 fichier_output (lithology + structure) into QGIS  ##
    ###############################################################################

    def import_Excel_create_QGISfile(self, file_lithology, file_structure, name_input_file):

        print(f"file_structure: {file_structure}")
        # --- ensure root group exists
        project = QgsProject.instance()
        root = project.layerTreeRoot()

        harm_group = root.findGroup("HARMONISED LEGACY SCRATCH DATA")
        # Prepare the "HARMONISED LEGACY SCRATCH DATA" group at top of the Tree
        project = QgsProject.instance()
        root = project.layerTreeRoot()
        harm_group = root.findGroup("HARMONISED LEGACY SCRATCH DATA")
        if not harm_group:
            harm_group = root.insertGroup(0, "HARMONISED LEGACY SCRATCH DATA")

        # Prepare the 'name_input_file' subgroup within the "HARMONISED LEGACY SCRATCH DATA" group
        base_name = os.path.splitext(name_input_file)[0]
        sub_harm_group = harm_group.findGroup(base_name)
        if not sub_harm_group:
            sub_harm_group = harm_group.insertGroup(0, base_name)

        # from openpyxl import Workbook, load_workbook

        if file_lithology and file_structure:
            workbooks = [file_lithology, file_structure]

        elif file_lithology:
            workbooks = [file_lithology]
            # file_lithology.save(r"C:\\Users\\00073294\\Dropbox\\temp_dropbox\\GEOL-QMAPS_v3.0.10 - test 1 and test 2 for merging tool\\debug.xlsx")

        elif file_structure:
            workbooks = [file_structure]
            # file_structure.save(
            #    r"C:\\Users\\00073294\\Dropbox\\WAXI4\\gis\\####W4S5\\debug.xlsx"
            # )

        else:
            workbooks = []
        self.sheetHashUUID = {}
        for workbook in workbooks:

            # loop through all the sheets in the Excel workbook
            for sheet in workbook:

                # Get the sheet’s DataFrame
                df = workbook[sheet]

                # Retrieve column names
                headers = list(df.columns)

                # Retrieve some key indices
                dipdir_idx = headers.index("Dip_Dir") if "Dip_Dir" in headers else None
                # print(f"dip dir index in {layer_name} is:{dipdir_idx}")
                strike_idx = headers.index("Strike_RHR") if "Strike_RHR" in headers else None
                # print(f"strike index in {layer_name} is:{strike_idx}")
                measure_idx = headers.index("Measure") if "Measure" in headers else None
                # print(f"Type of structural measurement index in {layer_name} is:{measure_idx}")
                trend_idx = headers.index("Trend") if "Trend" in headers else None
                # print(f"Younging trend index in {layer_name} is:{trend_idx}")
                younging_idx = headers.index("Younging") if "Younging" in headers else None
                # print(f"Younging indication index in {layer_name} is:{younging_idx}")

                # Retrieve geometry column index
                geometry_column_index = headers.index("Geometry")

                # Vector layer creation
                layer_name = sheet
                layer = QgsVectorLayer("Point?crs=epsg:4326", layer_name, "memory")

                if "_PT" in layer_name:
                    name_reference = layer_name.split("_PT")[0]
                    name_reference = name_reference + "_PT"

                elif "_PG" in layer_name:
                    name_reference = layer_name.split("_PG")[0]
                    name_reference = name_reference + "_PG"

                project = QgsProject.instance()
                layer_reference = project.mapLayersByName(name_reference)[0]

                type_field_data = []
                for field in layer_reference.fields():
                    type_donnee = field.typeName()
                    type_field_data.append(type_donnee)

                field_names = layer_reference.fields().names()

                # Add fields name
                layer_fields = []
                compte = 0

                for header in headers:
                    type_donnee = type_field_data[compte]

                    if type_donnee == "String":
                        field = QgsField(header, QVariant.String)
                    elif type_donnee == "Integer":
                        # print("int",header)
                        field = QgsField(header, QVariant.LongLong)
                    elif type_donnee == "Integer64":
                        # print("int64",header)
                        field = QgsField(header, QVariant.LongLong)
                    elif type_donnee == "JSON":
                        field = QgsField(header, QVariant.String)
                    layer_fields.append(field)
                    compte += 1

                layer.dataProvider().addAttributes(layer_fields)
                layer.updateFields()

                # Add fields content by looping through sheet rows
                for index, row in workbook[sheet].iterrows():

                    # Convert row to mutable list
                    row_vals = list(row)

                    #Measure field --> structural measurement
                    if measure_idx is not None:
                        dipdir_val = row_vals[dipdir_idx] if dipdir_idx   is not None else None
                        strike_val = row_vals[strike_idx] if strike_idx   is not None else None
                        try:
                            dipdir_num = float(dipdir_val)
                        except (TypeError, ValueError):
                            dipdir_num = None
                        try:
                            strike_num = float(strike_val)
                        except (TypeError, ValueError):
                            strike_num = None
                        #print(f"Structural measurement confirmed: fixing DipDir ({dipdir_num}) or Strike value({strike_num}) for entity {index} in {layer_name} ongoing...")

                        # 1) If Dip_Dir is non-null & Strike_RHR is null ⇒ compute Strike_RHR & Measure
                        if dipdir_idx is not None and strike_idx is not None and dipdir_num is not None and (
                                strike_num is None or pd.isna(strike_val) or strike_val == ""):
                            # Now dipdir_num is guaranteed not None, so comparison is safe
                            computed = dipdir_num - 90 if dipdir_num > 90 else dipdir_num + 270
                            row_vals[strike_idx] = str(computed)
                            row_vals[measure_idx] = "Dip - dip direction"

                        # 2) Else if Strike_RHR is non-null & Dip_Dir is null ⇒ compute Dip_Dir & Measure
                        elif dipdir_idx is not None and strike_idx is not None and strike_num is not None and (
                                dipdir_num is None or pd.isna(dipdir_val) or dipdir_val == ""):
                            # Now strike_num is guaranteed not None
                            computed = strike_num + 90 if strike_num < 270 else strike_num - 270
                            row_vals[dipdir_idx] = str(computed)
                            row_vals[measure_idx] = "Strike (right-hand rule) - dip"

                        #print(f"field values for entity {index} in {layer_name} are {row_vals} ")

                    # 3) If Trend is non-null ⇒ set Younging = 'Yes'
                    if trend_idx is not None and younging_idx is not None:
                        trend_val = row_vals[trend_idx]
                        try:
                            trend_num = float(trend_val)
                        except (TypeError, ValueError):
                            trend_num = None
                        #print(f"Lithology point with Younging indication confirmed: fixing Younging indication for entity {index} in {layer_name} ongoing...")
                        if pd.notna(trend_val) and trend_val != "":
                            row_vals[younging_idx] = "Yes"
                        #print(f"Younging indicator now set to {row_vals[younging_idx]}")

                    #print(f"field values for entity {index} in {layer_name} are {row_vals} ")

                    # rebuild row from the updated row_vals list so geometry + attributes use the new values
                    row = pd.Series(row_vals, index=row.index)

                    # Geometry updated with row_vals
                    feature = QgsFeature(layer.fields())
                    geometry_wkt = row[geometry_column_index]
                    feature.setGeometry(QgsGeometry.fromWkt(geometry_wkt))
                    
                    # Other fields of the attribute table by looping through sheet columns
                    for i, value in enumerate(row):

                        if field_names[i] == "Existing databases - raw data":
                            hash = hashlib.sha256(value.encode()).hexdigest()

                        type_donnee = type_field_data[i]
                        if type_donnee == "String" or type_donnee == "JSON":
                            if field_names[i] == "UUID":
                                feature.setAttribute(
                                    i, str(hash)
                                )  # relies on 'UUID' field coming after 'Existing databases - raw data' field
                                self.sheetHashUUID[str(hash)] = [new_text]
                            elif (
                                field_names[i] == "Measure" and ("Lineations_PT" or "Folds_PT") in sheet
                            ):
                                feature.setAttribute(i, "Trend - Plunge")
                            elif field_names[i] == "Existing databases - raw data":
                                new_text = {}
                                for pair in value[:-1].split(";"):
                                    if ":" in pair:
                                        key = pair.split(":")[0]
                                        entry = pair.split(":")[1]
                                        new_text[key] = entry
                                feature.setAttribute(i, str(new_text))

                            else:
                                feature.setAttribute(i, str(value))
                        elif type_donnee in ["Integer", "Integer64"]:
                            if value.strip():
                                try:
                                    feature.setAttribute(i, int(float(value.strip())))
                                except ValueError:
                                    feature.setAttribute(i, None)
                    layer.dataProvider().addFeature(feature)
                layer.commitChanges()

                # Add the layer to the QGIS project
                if layer.featureCount() > 0:
                    QgsProject.instance().addMapLayer(layer,addToLegend=False)
                    sub_harm_group.insertLayer(0, layer)
                iface.mapCanvas().refresh()

    ###############################################################################
    ######                   Import Legacy Field Data (.shp)               ########
    ###############################################################################

    def method_import_data(self):
        '''Retrieve the path of the file to be processed from the computer (input by the user)'''
        if os.path.exists(self.mynormpath(self.dlg.lineEdit_13.text())):

            path_layer_to_import = self.dlg.lineEdit_13.text()

            # File name retrieval
            segments = path_layer_to_import.split("/")
            name_layer_to_import = segments[-1]

            # Load layer into QGIS
            layer = QgsVectorLayer(path_layer_to_import, name_layer_to_import, "ogr")

            if layer.isValid():
                pass
            else:
                self.iface.messageBar().pushMessage("Erreur", "Unable to load selected layer !", level=Qgis.Critical)

            # Step 1 : Check layer coordinates + Create the Geometry column
            self.convert_coordinates_WGS84(layer)

            # Step 2 : Export the layer in Excel format + Fill Table1 with Columns pairs
            fichier_input = self.export_layer_fill_Table1(layer)

            self.iface.messageBar().pushMessage(
                "Selected File loaded: Please proceed further with Step 2 (Database Fields table)", level=Qgis.Success, duration=45
            )

            return fichier_input, name_layer_to_import

        else:
            self.iface.messageBar().pushMessage(
                "Directory not found: " + self.dlg.lineEdit_13.text(),
                level=Qgis.Warning,
                duration=45,
            )

    def method_columns_check_OK(self, fichier_input, name_layer_to_import):
        '''Check database fields and populate Lithology and Structure Tables'''
        # Step 1 : Retrieving data from QTableWidget1
        list_columns_check = self.recup_contenu_1()

        # Step 2 : Dataframe creation with sorted and verified columns
        fichier_output, list_columns_check3 = self.DataFrame_columns_check(fichier_input, list_columns_check, name_layer_to_import)

        if "Lithology - Outcrop Lithology" in list_columns_check3:
            # Step 3 LITHO : Fill Table2 with Lithologies pairs
            self.fill_Table2(fichier_output)

        if "Structures - Structure Type" in list_columns_check3:
            # Step 3bis STRUCTURE : Fill Table3 with Structure pairs
            self.fill_Table3(fichier_output)

        self.iface.messageBar().pushMessage(
            "Names of columns checked: Please proceed further with Step 2 (Lithology Names and/or Structure Types tables)", level=Qgis.Success, duration=45
        )

        # If "Structures - Structure Type" is selected in Table1, display a warning about how to handle linear and planar measurements separately in case their measurements are not in distinct fields.
        if "Structures - Structure Type" in list_columns_check3:
            msg = QMessageBox(self.dlg)
            msg.setIcon(QMessageBox.Warning)
            msg.setWindowTitle("Warning")
            msg.setText(
                "If your data source includes both linear and planar measurements, and that dip direction and "
                "trend/plunge direction (and/or dip and plunge, respectively) share the same data fields, "
                "please ensure that you import them separately.\n\n"
                "• For planar measurements, assign legacy fields to standard values as follows:\n"
                "  – Structures – Planar Measurements / Dip Direction\n"
                "  – Structures – Planar Measurements / Dip\n\n"
                "• For linear measurements, assign them using:\n"
                "  – Structures – Linear Measurements / Trend – Plunge Direction\n"
                "  – Structures – Linear Measurements / Trend – Plunge\n\n"
                "If you want to add kinematics information to legacy lineations, please make sure to have a "
                "“Kinematics” column in the legacy database, with values limited to:\n"
                "Dextral-slip, Sinistral-slip, Reverse-slip, Normal-slip, Low-angle detachment. \n"
                "This field should be assigned to “Structures - Kinematics“."
            )
            msg.setStandardButtons(QMessageBox.Ok)
            msg.exec_()

        return fichier_output, list_columns_check3

    def method_lithologies_check_OK(self, name_layer_to_import, fichier_output, list_columns_check3):
        '''Check lithologies and get the files ready for export to scratch standard layers'''
        # Step 1 : Retrieving data from QTableWidget2
        list_lithologies_unique_check_OK = self.recup_contenu_2()

        # Step 2 : Organizing lithologies in different sheets of an Excel file
        fichier_output_lithology = self.lithologies_sorting(
            fichier_output,
            list_columns_check3,
            list_lithologies_unique_check_OK,
            name_layer_to_import,
        )
        return fichier_output_lithology

    def method_structures_check_OK(self, fichier_output, list_columns_check3, name_layer_to_import):
        '''Check Structures and get the files ready for export to scratch standard layers'''
        print("Enter method_structures_check_OK")
        # Step 1: call self.structure_sorting or any function that builds a dictionary
        structure_map=self.recup_contenu_3()
        fichier_output_structures = self.structure_sorting(fichier_output, structure_map, list_columns_check3, name_layer_to_import)

        # Step 2: actually return that dictionary
        return fichier_output_structures

    def method_import_data_as_layers(self, fichier_output_lithology, fichier_output_structures):
        '''Import the Excel file into QGIS and create different QGIS files'''
        self.import_Excel_create_QGISfile(fichier_output_lithology, fichier_output_structures, self.name_layer_to_import)
        self.iface.messageBar().pushMessage(
            "Data imported in the QGIS project as scratch layers: please proceed further with Step 4.", level=Qgis.Success, duration=45
        )

    ###############################################################################
    ######             Import Legacy Field Data (.shp) - Buttons           ########
    ###############################################################################
    def click_import_data(self):
        '''Connect Columns check OK button correctly'''
        self.fichier_input, self.name_layer_to_import = self.method_import_data()
        self.dlg.pushButton_9.setEnabled(True)
        self.dlg.pushButton_9.clicked.connect(self.handlePushButton9)

    def handlePushButton9(self):
        '''Check that the necessary data has been imported'''
        if hasattr(self, 'fichier_input') and hasattr(self, 'name_layer_to_import'):
            self.click_columns_check_OK(self.fichier_input, self.name_layer_to_import)
        else:
            self.iface.messageBar().pushMessage( "Please import the data first!", level=Qgis.Warning, duration=45)


    def click_columns_check_OK(self, fichier_input, name_layer_to_import):
        """
        Called when handlePushButton9 is clicked.
        Uses the helper method_columns_check_OK to generate the lithologies and/or structures tables.
        """
        fichier_output = pd.DataFrame()
        list_columns_check3 = []

        fichier_output, list_columns_check3 = self.method_columns_check_OK(fichier_input, name_layer_to_import)

        # Connect Lithologies check OK button correctly
        if "Lithology - Outcrop Lithology" in list_columns_check3:
            self.dlg.pushButton_10.clicked.connect(
                lambda: self.click_lithologies_check_OK(name_layer_to_import, fichier_output, list_columns_check3)
            )

        # Connect Structures check OK button correctly
        if "Structures - Structure Type" in list_columns_check3:
            self.dlg.pushButton_26.clicked.connect(
                lambda: self.click_structure_check_OK(fichier_output,list_columns_check3,name_layer_to_import)
            )

    def click_lithologies_check_OK(self, name_layer_to_import, fichier_output, list_columns_check3):
        """
        Called when pushbutton10 is clicked.
        Uses the helper method_lithologies_check_OK to generate the lithologies DataFrame.
        """
        self.fichier_output_lithology = self.method_lithologies_check_OK(name_layer_to_import, fichier_output, list_columns_check3)
        self.create_lithologies = True
        self.iface.messageBar().pushMessage("Names of lithologies checked: Please proceed further with Step 2 (if you need to check the Structure Types table) or 3", level=Qgis.Success, duration=45)

    def click_structure_check_OK(self, fichier_output, list_columns_check3, name_layer_to_import):
        """
        Called when pushbutton26 is clicked.
        Uses the helper method_structures_check_OK to generate the structures DataFrame.
        """
        print("Enter click_structure_check_OK")
        self.fichier_output_structures = self.method_structures_check_OK(fichier_output, list_columns_check3, name_layer_to_import)
        self.create_structures = True
        self.iface.messageBar().pushMessage("Types of structures checked: Please proceed further with Step 2 (if you need to check the Lithology Names table) or 3", level=Qgis.Success, duration=45)

    def Generate_Output_QGIS_Layers(self):
        """
        Generates scratch QGIS layers based on the DataFrames produced by
        the check_OK methods. Handles three cases:
          • both lithologies & structures
          • only lithologies
          • only structures
        """
        # Do we have valid lithology and/or structure outputs?
        has_lith = hasattr(self, 'fichier_output_lithology') \
                   and self.fichier_output_lithology is not None
        has_struc = hasattr(self, 'fichier_output_structures') \
                    and self.fichier_output_structures is not None

        if has_lith and has_struc:
            # Both lithology and structure DataFrames exist
            self.iface.messageBar().pushMessage(
                "Generating lithologies and structures layers...",
                level=Qgis.Info, duration=10
            )
            self.method_import_data_as_layers(
                self.fichier_output_lithology,
                self.fichier_output_structures
            )

        elif has_lith:
            # Only lithology DataFrame exists
            self.iface.messageBar().pushMessage(
                "Generating lithologies layers...",
                level=Qgis.Info, duration=10
            )
            self.method_import_data_as_layers(
                self.fichier_output_lithology,
                None
            )

        elif has_struc:
            # Only structure DataFrame exists
            self.iface.messageBar().pushMessage(
                "Generating structures layers...",
                level=Qgis.Info, duration=10
            )
            self.method_import_data_as_layers(
                None,
                self.fichier_output_structures
            )

        else:
            # Neither was validated
            self.iface.messageBar().pushMessage(
                "Please validate lithologies and/or structures before creating layers.",
                level=Qgis.Warning, duration=45
            )

        # Reset flags and clear input
        self.create_lithologies = False
        self.create_structures = False
        self.dlg.lineEdit_13.clear()

    def click_Reset_This_Window(self):
        '''Reset the Window button'''
        self.resetWindow_import_data()

    ###############################################################################
    ################            Fieldwork Preparation               ###############
    ###############################################################################

    def safe_copy_file(self, src_path, dst_path):
        if os.path.exists(src_path):
            shutil.copyfile(src_path, dst_path)
        else:
            print(src_path, "not found")

    def safe_copy_tree(self, src_path, dst_path):
        if os.path.exists(src_path):
            shutil.copytree(src_path, dst_path)
        else:
            print(src_path, "not found")

    ### Clip to Canvas ###

    def clipToCanvas(self):
        """Clips all layers in the CURRENT_MISSION and COMPILATION GeoPackages to the current canvas extent or an optional polygon."""
        # Get output directory
        out_dir = self.dlg.lineEdit_3.text().strip()
        if not out_dir:
            self.iface.messageBar().pushMessage(
                "Please provide an output directory for clipping.", level=Qgis.Warning, duration=10
            )
            return

        project = QgsProject.instance()
        old_proj = os.path.dirname(project.fileName()) + "/"
        new_proj = self.mynormpath(out_dir) + "/"

        # Validate filename and folder name lengths for output (preflight) (Windows max 256 chars)
        targets = [
            new_proj,
            new_proj + self.dir_0,
            new_proj + self.dir_1,
            new_proj + self.dir_99
        ]
        for p in targets:
            full = os.path.abspath(self.mynormpath(p))
            if len(full) > 256:
                self.iface.messageBar().pushMessage(
                    f"ERROR: Output path too long ({len(full)} chars): {full}. Select another repository with a shorter filepath to the current project.",
                    level=Qgis.Critical,
                    duration=15
                )
                # Clear directory field for next operation
                self.dlg.lineEdit_8.clear()
                return

        # Paths for the two GeoPackages to clip
        gpkg_paths = [
            (old_proj + self.dir_1 + "COMPILATION.gpkg", new_proj + self.dir_1 + "COMPILATION.gpkg"),
            (old_proj + self.dir_0 + "CURRENT_MISSION.gpkg", new_proj + self.dir_0 + "CURRENT_MISSION.gpkg")
        ]

        # Determine clipping layer: polygon shapefile or canvas extent
        clip_shp = self.dlg.lineEdit_8.text().strip()
        if clip_shp:
            clip_layer = QgsVectorLayer(clip_shp, os.path.basename(clip_shp), "ogr")
        else:
            e = self.iface.mapCanvas().extent()
            geom = QgsGeometry().fromRect(
                QgsRectangle(e.xMinimum(), e.yMinimum(), e.xMaximum(), e.yMaximum())
            )
            clip_layer = QgsVectorLayer(f"Polygon?crs={project.crs().authid()}", "canvas_extent", "memory")
            with edit(clip_layer):
                feat = QgsFeature()
                feat.setGeometry(geom)
                clip_layer.addFeature(feat)

        # Create required directories
        for path in [new_proj, new_proj + self.dir_0, new_proj + self.dir_1, new_proj + self.dir_99]:
            os.makedirs(self.mynormpath(path), exist_ok=True)

        # Automatically copy other project folders without clipping
        all_dirs = [d for d in os.listdir(old_proj) if os.path.isdir(os.path.join(old_proj, d))]
        exclude = {self.dir_0.strip('/'), self.dir_1.strip('/')}
        for d in all_dirs:
            if d not in exclude:
                src_path = os.path.join(old_proj, d)
                dst_path = os.path.join(new_proj, d)
                # Remove existing destination to avoid FileExistsError
                if os.path.exists(dst_path):
                    shutil.rmtree(dst_path)
                self.safe_copy_tree(src_path + '/', dst_path + '/')

        # === Clip the project file ===
        src = project.fileName()
        dst = os.path.join(
            new_proj,
            os.path.basename(src).replace('.qgz', '_clip.qgz')
        )
        # ——— Pre-flight: check this single file path ———
        full_path = os.path.abspath(self.mynormpath(dst))
        if len(full_path) > 256:
            self.iface.messageBar().pushMessage(
                f"ERROR: Output file path too long ({len(full_path)} chars): {full_path}. Select another repository with a shorter filepath to the current project.",
                level=Qgis.Critical,
                duration=15
            )
            # Clear directory field for next operation
            self.dlg.lineEdit_8.clear()
            return
        # Now safe to copy
        shutil.copyfile(src, full_path)

        # Clip each layer in both GeoPackages
        for src, dst in gpkg_paths:
            # Copy original GeoPackage
            self.safe_copy_file(src, dst)
            ds = ogr.Open(src)
            if not ds:
                continue
            # Ensure tables are empty before writing
            for i in range(ds.GetLayerCount()):
                lyr = ds.GetLayer(i)
                self.drop_layer_contents(dst, lyr.GetName())
            # Perform clipping
            for i in range(ds.GetLayerCount()):
                lyr = ds.GetLayer(i)
                name = lyr.GetName()
                input_uri = f"{src}|layername={name}"
                output_uri = f"ogr:dbname='{dst}' table=\"{name}\" (geom)"
                processing.run(
                    "native:clip",
                    {"INPUT": input_uri, "OVERLAY": clip_layer, "OUTPUT": output_uri}
                )

        # Clear input fields for next operation
        self.dlg.lineEdit_3.clear()
        self.dlg.lineEdit_8.clear()

        self.iface.messageBar().pushMessage(
            f"Project with field data clipped to selected extent saved in {new_proj}", level=Qgis.Success, duration=15
        )

    ### CSV TOOLS ###
    def get_csv_items(self, layer_name):
        """
        Returns a list of all 'Valeur' entries from the CSV dictionary corresponding to layer_name.
        Assumes the CSV is stored in self.dictionaries_path and is accessible via pandas.
        """
        try:
            ds = ogr.Open(self.dictionaries_path)
            if ds is None:
                raise Exception("Could not open geopackage: " + self.dictionaries_path)
            lyr = ds.GetLayer(layer_name)
            if lyr is None:
                raise Exception("Layer '{}' not found in geopackage.".format(layer_name))
            items = []
            for feat in lyr:
                value = feat.GetField("Valeur")
                if value is not None:
                    items.append(str(value))
            return items

        except Exception as e:
            print("Error reading dictionary for {}: {}".format(layer_name, e))
            return []

    def add_row_to_csv_layer(self, gpkg_path, layer_name, new_row):
        """
        Add a row to a CSV layer in a GeoPackage.

        Parameters:
            gpkg_path (str): Path to the GeoPackage file.
            layer_name (str): The name of the CSV layer in the GeoPackage.
            new_row (dict): A dictionary representing the new row to add.
                            The keys must match the column names of the layer.

        Returns:
            None
        """
        import fiona

        try:
            # Open the GeoPackage layer in read mode to retrieve metadata
            with fiona.open(gpkg_path, layer=layer_name, mode="r") as src:
                layer_crs = src.crs
                layer_schema = src.schema
                features = list(src)  # Load existing features

            # Remove existing 'id' keys from all features so new IDs are generated
            for feat in features:
                if "fid" in feat:
                    del feat["fid"]

            # Validate new_row keys match the schema's properties
            for key in new_row.keys():
                if key not in layer_schema["properties"]:
                    raise ValueError(
                        f"Invalid column name '{key}'. Column does not exist in the layer."
                    )

            # Create a new feature from the new_row
            new_feature = {
                "type": "Feature",
                "geometry": None,  # CSV layers typically don't have geometries
                "properties": new_row,
            }

            # Append the new feature to the features list
            features.append(new_feature)

            """# Create a backup of the original GeoPackage
            backup_path = f"{gpkg_path}.bak"
            os.rename(gpkg_path, backup_path)
            """
            # Write the updated features back to the GeoPackage
            with fiona.open(
                    gpkg_path,
                    mode="w",
                    driver="GPKG",
                    layer=layer_name,
                    schema=layer_schema,
                    crs=layer_crs,
            ) as dst:
                dst.writerecords(features)

            # print(f"Row added successfully to the layer '{layer_name}'.")
            # print(f"A backup of the original GeoPackage was created at: {backup_path}")

        except Exception as e:
            print(f"Error adding row to the GeoPackage layer: {e}")

    def delete_row_from_csv_layer(
        self, gpkg_path, layer_name, column_name, value_to_delete
    ):
        """
        Delete rows with a specific value from a CSV layer in a GeoPackage.

        Parameters:
            gpkg_path (str): Path to the GeoPackage file.
            layer_name (str): The name of the CSV layer in the GeoPackage.
            column_name (str): The name of the column to search for the value.
            value_to_delete (str): The value in the column to identify rows for deletion.

        Returns:
            None
        """
        import fiona

        # Open the GeoPackage layer
        try:
            with fiona.open(gpkg_path, layer=layer_name, mode="r") as src:
                # Get metadata and features from the layer
                layer_crs = src.crs
                layer_schema = src.schema
                features = list(src)

            # Filter out rows where the specified column matches the value to delete
            updated_features = [
                feature
                for feature in features
                if feature["properties"][column_name] != value_to_delete
            ]

            # Remove 'id' keys from the updated features so new IDs are assigned
            for feat in updated_features:
                if "fid" in feat:
                    del feat["fid"]

            # Create a backup of the original GeoPackage
            # backup_path = f"{gpkg_path}.bak"
            # os.rename(gpkg_path, backup_path)

            # Write the updated features back to the GeoPackage
            with fiona.open(
                gpkg_path,
                mode="w",
                driver="GPKG",
                layer=layer_name,
                schema=layer_schema,
                crs=layer_crs,
            ) as dst:
                dst.writerecords(updated_features)

        except Exception as e:
            print(f"Error processing the GeoPackage layer: {e}")

    def reload_csv(self, dictionaries_path, layer_csv, current_layer):
        '''#Clear all features in the existing layer
        layer_csv.startEditing()
        layer_csv.dataProvider().truncate()  # Deletes all features efficiently
        layer_csv.commitChanges()
        print(layer_csv, ' is cleared of all existing values')'''

        # Load new data from the CSV file
        new_table = QgsVectorLayer(
            f"{dictionaries_path}|layername={current_layer}", current_layer, "ogr"
        )

        if new_table.featureCount() == 0:
            print('New table is empty, issue!')

        if new_table.isValid():
            # Ensure the schema matches (field names and types)
            new_fields = new_table.fields()
            existing_fields = layer_csv.fields()

            if new_fields.names() != existing_fields.names() and current_layer!='General__Rock Type (Supergene, Sedimentary, Volcanoclastic,...)-Lithologies Table':
                print(
                    "The schema of the new data does not match the existing layer. Update failed."
                )
            else:
                # Add new features to the existing layer
                layer_csv.startEditing()
                features = new_table.getFeatures()

                # Retrieve and print the header (field names) --> For checking
                headers = [field.name() for field in new_table.fields()]
                print(" | ".join(headers))

                # Iterate over each feature and print the attribute values --> For checking
                for feature in features:
                    # Convert each attribute to string and join using a separator
                    values = [str(feature[field]) for field in headers]
                    print(" | ".join(values))

                for feature in features:
                    # Dealing with fid for new entries
                    if feature.attribute(0) >= -1:
                        feature.setAttribute(0, None)

                    layer_csv.dataProvider().addFeatures([feature])

                layer_csv.commitChanges()

        else:
            print(f"Failed to load new data from {dictionaries_path}.")

    ### Option 1 :  ADD a single value/description pair to any CSV file in the WAXI QFIELD template
    def addCsvItem(self):
        """
        Adds a new item to a CSV layer and updates the corresponding layer in QGIS.
        If the selected dictionary name contains 'List of lithologies', extra rows are added to the
        'General__List of all lithologies' and 'General__Rock Type (Supergene, Sedimentary, Volcanoclastic,...)-Lithologies Table'.
        Prevents duplicate entries and resets the lineEdit_38 to its default appearance after adding.
        """
        current_layer = self.dlg.comboBox.currentText()
        new_value = str(self.dlg.lineEdit_38.text()).strip()
        if new_value == "":
            self.iface.messageBar().pushMessage("Error: No value provided", level=Qgis.Warning, duration=45)
            self.dlg.lineEdit_38.clear()
            return

        # Check for duplicates in the current dictionary
        existing_items = self.get_csv_items(current_layer)
        if new_value in existing_items:
            self.iface.messageBar().pushMessage(
                "Duplicate entry: '{}' already exists in {} dictionary.".format(new_value, current_layer),
                level=Qgis.Warning, duration=45)
            self.dlg.lineEdit_38.clear()
            return

        new_row = {"Valeur": new_value, "Description": new_value}

        # Add the new row to the currently selected CSV layer
        self.add_row_to_csv_layer(self.dictionaries_path, current_layer, new_row)

        # If the dictionary is related to lithologies, update the two general dictionaries
        if "List of lithologies" in current_layer:
            self.add_row_to_csv_layer(self.dictionaries_path, "General__List of all lithologies", new_row)
            rock_type = current_layer.split(" lithologies__")[0]
            rock_row = {"Key": rock_type, "Value": new_value}
            self.add_row_to_csv_layer(self.dictionaries_path,
                                      "General__Rock Type (Supergene, Sedimentary, Volcanoclastic,...)-Lithologies Table",
                                      rock_row)

        # Reload CSV layer based on naming conventions
        if current_layer.startswith("General__"):
            layer_csv = QgsProject.instance().mapLayersByName(current_layer.replace("__", " // "))[0]
        else:
            layer_csv = QgsProject.instance().mapLayersByName(current_layer.replace("__", "/"))[0]


        if layer_csv.isValid():
            self.reload_csv(self.dictionaries_path, layer_csv, current_layer)
            if "List of lithologies" in current_layer:
                self.reload_csv(self.dictionaries_path, layer_csv, "General__List of all lithologies")
                self.reload_csv(self.dictionaries_path, layer_csv,
                                "General__Rock Type (Supergene, Sedimentary, Volcanoclastic,...)-Lithologies Table")
        else:
            print("Could not reload the csv file: error related to retrieving of the name of the csv dictionary layer")

        layer_csv.triggerRepaint()
        self.iface.messageBar().pushMessage(
            "Item '{}' added to {} dictionary. Update symbology if needed.".format(new_value, current_layer),
            level=Qgis.Success, duration=45
        )
        self.dlg.lineEdit_38.clear()
        self.update_combobox_delete()

    ### Option 2 :  DELETE a single value to any CSV file in the WAXI QFIELD template
    def deleteCsvItem(self):
        """
        Deletes an item from a CSV dictionary and updates the corresponding layer in QGIS.
        If the current dictionary relates to lithologies, the deletion is also applied to the
        general lithologies list and rock type table. Resets the comboBox_delete to its default appearance after deletion.
        """
        current_layer = str(self.dlg.comboBox.currentText())
        delete_item = self.dlg.comboBox_delete.currentText()

        # Delete the row from the currently selected CSV layer
        self.delete_row_from_csv_layer(self.dictionaries_path, current_layer, "Valeur", delete_item)

        if "List of lithologies" in current_layer:
            print("list of lithologies dictionary? YES")
            self.delete_row_from_csv_layer(self.dictionaries_path, "General__List of all lithologies", "Valeur",
                                           delete_item)
            self.delete_row_from_csv_layer(self.dictionaries_path,
                                           "General__Rock Type (Supergene, Sedimentary, Volcanoclastic,...)-Lithologies Table",
                                           "Value", delete_item)

        if current_layer.startswith("General__"):
            layer_csv = QgsProject.instance().mapLayersByName(current_layer.replace("__", " // "))[0]
        else:
            layer_csv = QgsProject.instance().mapLayersByName(current_layer.replace("__", "/"))[0]

        self.reload_csv(self.dictionaries_path, layer_csv, current_layer)

        self.iface.messageBar().pushMessage(
            "Item '{}' removed from {} dictionary. Update symbology if needed.".format(delete_item, current_layer),
            level=Qgis.Success, duration=15
        )
        self.update_combobox_delete()

    ### Update project name ###
    def updateProjectTitle(self):
        if self.dlg.lineEdit_9.text() and self.dlg.lineEdit_10.text():
            project = QgsProject.instance()
            new_title = self.dlg.lineEdit_9.text() + "/" + self.dlg.lineEdit_10.text()
            project.setTitle(new_title)
            project.write()
            self.iface.messageBar().pushMessage(
                "Project title updated to " + new_title, level=Qgis.Success, duration=45
            )

    def apply_qml_style(self, layer, qml_path):
        """
        Apply a QML style file to a given layer.

        Parameters:
            layer (QgsMapLayer): The layer to which the style will be applied.
            qml_path (str): Path to the QML style file.

        Returns:
            bool: True if the style was successfully applied, False otherwise.
        """
        if not layer.isValid():
            print(f"Layer {layer.name()} is not valid.")
            return False

        result = layer.loadNamedStyle(qml_path)
        if not result:
            print(f"Failed to apply QML style: {qml_path} {result}")
            return False

        # Refresh the layer to apply the style changes
        layer.triggerRepaint()

        return True

    ### Set user by default ###                   #ADD

    def set_user_by_default(self):

        from fuzzywuzzy import fuzz

        if self.dlg.lineEdit_39.text():

            default_value_user_csv = str(self.dlg.lineEdit_39.text())
            liste_users = self.get_first_column_text(
                self.dictionaries_path, "General__List of Users"
            )
            ## User.csv file location
            # WAXI_projet_path = os.path.abspath(QgsProject.instance().fileName())
            # emplacement_User_file = self.templateCSV_path + "/User list.csv"
            # user_file = pd.read_csv(emplacement_User_file, sep=";", encoding="latin-1")

            ## Test if the user name is already present in the User.csv file
            list_score = []
            # liste_users = list(user_file["Valeur"])

            for test in liste_users:
                new_score = fuzz.token_set_ratio(str(test), default_value_user_csv)
                list_score.append(new_score)

            # If the user name is not in the CSV file, we add it:
            if 100 not in list_score:
                new_row = {
                    "Valeur": default_value_user_csv,
                    "Description": default_value_user_csv,
                }
                self.add_row_to_csv_layer(
                    self.dictionaries_path, "General__List of Users", new_row
                )

                # Updates the layer in QGIS
                layer_user = QgsProject.instance().mapLayersByName(
                    "General // List of Users"
                )[0]

                self.reload_csv(
                    self.dictionaries_path, layer_user, "General__List of Users"
                )
                # QgsProject.instance().reloadAllLayers()

                default_value_user = "'" + str(self.dlg.lineEdit_39.text()) + "'"

            # If the user name is in the CSV file, we choose it:
            else:
                for test in liste_users:
                    new_score = fuzz.token_set_ratio(str(test), default_value_user_csv)
                    if new_score == 100:
                        default_value_user = "'" + test + "'"
                        break

            ## Modification of the User field in QGIS template layers
            self.dlg.button_group = QButtonGroup()
            self.dlg.button_group.addButton(self.dlg.radioButton_All)
            self.dlg.button_group.addButton(self.dlg.radioButton_Some)

            # If the "Change for one layer" radiobutton is checked
            if self.dlg.radioButton_Some.isChecked():
                layer_selected = QgsProject.instance().mapLayersByName(
                    str(self.dlg.comboBox_layers_user.currentText())
                )[0]

                # Find 'User' field index
                field_index = layer_selected.fields().indexFromName("User")

                # Create default value
                default_value = QgsDefaultValue(default_value_user)

                # Update default field value
                layer_selected.setDefaultValueDefinition(field_index, default_value)
                QgsProject.instance().write()

                self.iface.messageBar().pushMessage(
                    str(default_value_user)
                    + " is now the default user for "
                    + str(self.dlg.comboBox_layers_user.currentText()),
                    level=Qgis.Success,
                    duration=15,
                )

            # If the "Change for all layers" radiobutton is checked
            if self.dlg.radioButton_All.isChecked():

                layers = QgsProject.instance().mapLayers()

                for layerId, layer in layers.items():

                    # Select all non CSV layers of the QGIS project
                    if isinstance(
                        layer, QgsVectorLayer
                    ) and not layer.dataProvider().dataSourceUri().lower().endswith(
                        ".csv"
                    ):

                        # Find 'User' field index
                        field_index = layer.fields().indexFromName("User")

                        # Create default value
                        default_value = QgsDefaultValue(default_value_user)

                        # Update default field value
                        layer.setDefaultValueDefinition(field_index, default_value)
                        QgsProject.instance().write()

                self.iface.messageBar().pushMessage(
                    str(default_value_user)
                    + " is now the default user for ALL the layers in the project",
                    level=Qgis.Success,
                    duration=15,
                )

    # set dip/dip direction vs dip/strike RHR

    def set_orientation_style(self):
        if self.dlg.structure_style_on_pushButton.isChecked():
            value = "'Dip - dip direction'"
        else:
            value = "'Strike (right-hand rule) - dip'"

        # Create default value
        default_value = QgsDefaultValue(value)

        shp_list = self.mynormpath(
            os.path.dirname(os.path.realpath(__file__)) + "/shp.csv"
        )

        shps = pd.read_csv(shp_list, names=["name", "dir_code"])
        shps = shps.set_index("name")
        planes = [
            "Dikes-Sills_PT",
            "Folds_PT",
            "Foliation-cleavage_PT",
            "Bedding-Lava flow-S0_PT",
            "Lithological contacts_PT",
            "Shear zones and faults_PT",
            "Veins_PT",
            "Fractures_PT",
            "contacts_LN",
            "Planar structures_LN",
        ]
        for name, data in shps.iterrows():
            if name in planes:
                layer = QgsProject.instance().mapLayersByName(name)[0]
                # Find 'User' field index
                field_index = layer.fields().indexFromName("Measure")

                # Update default field value
                layer.setDefaultValueDefinition(field_index, default_value)

        QgsProject.instance().write()

        self.iface.messageBar().pushMessage(
            str(value) + " is now the default structural style ",
            level=Qgis.Success,
            duration=15,
        )

    ###############################################################################
    ################       Page 3 : Field Data Management           ###############
    ###############################################################################

    ### Update Project to the Latest Version ###
    def select_old_project_folder(self):
        """Let the user pick an existing GEOL-QMAPS project folder."""
        folder = QFileDialog.getExistingDirectory(
            self.dlg, "Select old GEOL-QMAPS project folder", ""
        )
        if folder:
            self.dlg.lineEdit_15.setText(folder)

    def rejig_project(self):
        """Main entry – validate old project, fetch template, assemble updated copy."""
        old = Path(self.dlg.lineEdit_15.text().strip())
        parent = old.parent

        # 1. Validate it’s a GEOL-QMAPS project
        gpkg = old / "1_EXISTING_FIELD_DATABASE" / "COMPILATION.gpkg"
        ver_txt = old / "99_COMMAND_FILES_PLUGIN" / "Version.txt"
        if not gpkg.exists() or not ver_txt.exists():
            self.iface.messageBar().pushMessage(
                "ERROR: Selected folder is either not a valid GEOL-QMAPS project, or too old to be automatically updated to the latest version (version < 3.1.0).",
                level=Qgis.Critical, duration=10
            )
            # Clear the input field once done
            self.dlg.lineEdit_15.clear()
            return

        else:
            print (f"{old} is a GEOL-QMAPS project")

        # 2. Check version > 3.1.0
        with open(ver_txt, 'r') as f:
            raw = f.readline().lstrip('v').strip()
        parts = [int(x) for x in raw.split('.')]
        if parts < [3, 1, 0]:
            self.iface.messageBar().pushMessage(
                f"ERROR: Project version {raw} is older than 3.1.0; cannot be updated to the latest version automatically.",
                level=Qgis.Critical, duration=10
            )
            # Clear the input field once done
            self.dlg.lineEdit_15.clear()
            return
        else:
            print (f"{old} is version {raw} and can be be updated to the latest version automatically.")

        # 3. Download + unpack latest template --> TO BE UPDATED FOR EVERY NEW RELEASE --> v3.1.4 at the moment
        # 3a) Quick connectivity check (DNS socket to 8.8.8.8:53)
        import socket
        try:
            socket.create_connection(("8.8.8.8", 53), timeout=5)
        except OSError:
            #No internet connection
            self.iface.messageBar().pushMessage(
                "No internet connection detected. Please check your network and try again.",
                level=Qgis.Critical, duration=10
            )
            # Clear the input field once done
            self.dlg.lineEdit_15.clear()
            return

        # 3b) Attempt download with 60 s timeout
        tmpzip = Path(tempfile.gettempdir()) / "QGIS_TEMPLATE.zip"
        url = "https://zenodo.org/records/15460411/files/GEOL-QMAPS_v3.1.4.zip?download=1" #TO BE UPDATED AT EVERY RELEASE
        from urllib.request import urlopen
        try:
            with urlopen(url, timeout=60) as resp:
                data = resp.read()
            local_path = str(tmpzip)
            with open(local_path, 'wb') as f:
                f.write(data)

        except socket.timeout:
            self.iface.messageBar().pushMessage(
                "Download timed out after 60 seconds. Please try again later with a more stable connection.",
                level=Qgis.Critical, duration=10
            )
            # Clear the input field once done
            self.dlg.lineEdit_15.clear()
            return

        except Exception as e:
            self.iface.messageBar().pushMessage(
                f"An unexpected error occurred while downloading:\n{e}",
                level=Qgis.Critical, duration=10
            )
            # Clear the input field once done
            self.dlg.lineEdit_15.clear()
            return

        # 3c) Unzip the downloaded archive into the parent folder
        import zipfile
        with zipfile.ZipFile(local_path, 'r') as zf:
            zf.extractall(str(parent))

        # 4a) Define template_src as the QGIS_TEMPLATE subfolder of the new release
        template_src = parent / "GEOL-QMAPS_v3.1.4" / "QGIS_TEMPLATE" #TO BE UPDATED AT EVERY RELEASE


        # 4b) Prepare the destination name and remove any stale copy
        rejigged = parent / f"{old.name}_updatedversion"
        if rejigged.exists():
            shutil.rmtree(str(rejigged))

        # PRE-FLIGHT: simulate all future dest paths and catch any >256 chars
        max_len = 256
        bad = []
        for root, dirs, files in os.walk(str(template_src)):
            for name in dirs + files:
                # path in the template
                src_path = os.path.join(root, name)
                # corresponding dest path under `rejigged`
                rel = os.path.relpath(src_path, str(template_src))
                dest_full = os.path.abspath(os.path.join(str(rejigged), rel))
                if len(dest_full) > max_len:
                    bad.append(dest_full)

        if bad:
            # write the list to temp for download
            log = os.path.join(tempfile.gettempdir(), "geol_qmaps_rejig_bad_paths.txt")
            with open(log, "w", encoding="utf-8") as f:
                for p in bad:
                    f.write(p + "\n")

            url = log.replace("\\", "/")
            if not url.startswith("/"):
                url = "/" + url

            self.iface.messageBar().pushMessage(
                f"{len(bad)} paths would exceed {max_len} chars. "
                f"<a href='file://{url}'>Download list</a>. Shorten filepaths to proceed further.",
                level=Qgis.Critical,
                duration=0
            )
            # Clear the input field once done
            self.dlg.lineEdit_15.clear()
            return

        # 5. Copy the template into projects folder and rename in one go
        shutil.copytree(str(template_src), str(rejigged))
        # ————— Validate all new paths —————
        if not self._check_max_filename_length(str(rejigged)):
            return
        self.iface.messageBar().pushMessage(
            f"Template unpacked and renamed → '{rejigged.name}'",
            level=Qgis.Success, duration=6
        )
        print(f"QGIS_TEMPLATE renamed and copied at {rejigged}")


        # 6. Copy the old .qgz into the new folder (deleting any existing .qgz)
        for f in rejigged.glob("*.qgz"):
            f.unlink()
        old_qgz = next(old.glob("*.qgz"), None)
        if old_qgz:
            shutil.copy2(str(old_qgz), str(rejigged))
        # ————— Re-validate after .qgz copy —————
        if not self._check_max_filename_length(str(rejigged)):
            return
        print(f"Old .qgz project file copied in: {rejigged}")

        # 7. Copy array of subfolders except for 0_, 1_ and 99_
        exclude = {"0_FIELD_DATA", "1_EXISTING_FIELD_DATABASE", "99_COMMAND_FILES_PLUGIN"}
        for src_path in old.iterdir():
            if not src_path.is_dir() or src_path.name in exclude:
                continue

            dst_path = rejigged / src_path.name
            shutil.copytree(str(src_path), str(dst_path), dirs_exist_ok=True)
        # ————— Validate again after subfolder copies —————
        if not self._check_max_filename_length(str(rejigged)):
            return
        print(f"Folders copied in: {rejigged}")

        # copy of DCIM sub-folders
        for branch in ["0_FIELD_DATA/DCIM", "1_EXISTING_FIELD_DATABASE/DCIM"]:
            src = old / Path(branch)
            dst = rejigged / Path(branch)
            if dst.exists():
                shutil.rmtree(str(dst))
            if src.exists():
                shutil.copytree(str(src), str(dst), dirs_exist_ok=True)
        # ————— Final DCIM check —————
        if not self._check_max_filename_length(str(rejigged)):
            return
        print(f"Folders and DCIM copied in: {rejigged}")

        # 8. Copy features from the two GeoPackages using OGR
        from osgeo import ogr

        driver = ogr.GetDriverByName("GPKG")
        raw_version = raw  # the version string you read earlier, e.g. "3.1.0"

        for pkg_rel in ["0_FIELD_DATA/CURRENT_MISSION.gpkg",
                        "1_EXISTING_FIELD_DATABASE/COMPILATION.gpkg"]:
            old_pkg = str(old / pkg_rel)
            new_pkg = str(rejigged / pkg_rel)

            # open source (read) and destination (update) datasets
            src_ds = ogr.Open(old_pkg, 0)
            dst_ds = ogr.Open(new_pkg, 1)
            if src_ds is None or dst_ds is None:
                self.iface.messageBar().pushMessage(
                    f"WARNING: Could not open {pkg_rel} for copying.", level=Qgis.Warning, duration=5
                )
                continue

            # loop through each layer in the source
            for i in range(src_ds.GetLayerCount()):
                src_layer = src_ds.GetLayerByIndex(i)
                # skip non-spatial tables
                if src_layer.GetGeomType() in (ogr.wkbNone, ogr.wkbUnknown):
                    continue

                # special rename for old v3.1.0
                old_name = src_layer.GetName()
                new_name = old_name
                if raw_version == "3.1.0" and old_name == "Compilation_Deformation_zones_PG":
                    new_name = "Compilation_Deformation zones_PG"

                dst_layer = dst_ds.GetLayerByName(new_name)
                if dst_layer is None:
                    # skip layers that aren't in the new template
                    continue

                dst_def = dst_layer.GetLayerDefn()
                # start transaction for speed
                dst_layer.StartTransaction()
                src_layer.ResetReading()
                feat = src_layer.GetNextFeature()
                while feat:
                    # build a new feature matching the dst schema
                    out_feat = ogr.Feature(dst_def)
                    # copy only fields that exist in dst
                    for fi in range(dst_def.GetFieldCount()):
                        fld_def = dst_def.GetFieldDefn(fi)
                        fld_name = fld_def.GetName()
                        if feat.GetFieldIndex(fld_name) != -1:
                            out_feat.SetField(fld_name, feat.GetField(fld_name))
                    # copy geometry
                    geom = feat.GetGeometryRef()
                    if geom:
                        out_feat.SetGeometry(geom.Clone())
                    # insert into dst
                    dst_layer.CreateFeature(out_feat)
                    # cleanup
                    out_feat = None
                    feat = src_layer.GetNextFeature()
                dst_layer.CommitTransaction()

            # explicitly close datasets to release file locks
            src_ds = None
            dst_ds = None

        self.iface.messageBar().pushMessage(
            f"Version update complete: '{rejigged.name}' created.", level=Qgis.Success, duration=10
        )

        # Clear the input field once done
        self.dlg.lineEdit_15.clear()


    # --------------------------------------------------------------------------
    # QField ↔ QGIS Sync Tool
    # --------------------------------------------------------------------------
    def select_QFieldPackage(self):
        """
        Slot to browse and select a QField package (ZIP or folder) with custom buttons.
        """
        # Build a message box with custom button labels
        msg = QtWidgets.QMessageBox(self.iface.mainWindow())
        msg.setWindowTitle("Select QField Package to synchronise to QGIS")
        msg.setText("Choose QField package type:")
        zip_btn = msg.addButton("ZIP archive", QtWidgets.QMessageBox.ActionRole)
        folder_btn = msg.addButton("Folder", QtWidgets.QMessageBox.ActionRole)
        msg.exec_()

        # Determine which button was clicked
        if msg.clickedButton() == zip_btn:
            path, _ = QFileDialog.getOpenFileName(
                self.iface.mainWindow(),
                "Select QField ZIP package",
                "",
                "ZIP archives (*.zip)"
            )
        else:
            path = QFileDialog.getExistingDirectory(
                self.iface.mainWindow(),
                "Select QField package folder",
                "",
                QFileDialog.ShowDirsOnly | QFileDialog.ReadOnly
            )

        if path:
            self.dlg.lineEdit_QFieldPackage.setText(path)

    def select_QGISFolder(self):
        """
        Slot to browse and select a GEOL‑QMAPS QGIS project folder.
        """
        folder = QFileDialog.getExistingDirectory(
            self.iface.mainWindow(),
            "Select the master GEOL‑QMAPS project folder to update with new data collected in the field",
            "",
            QFileDialog.ShowDirsOnly | QFileDialog.ReadOnly
        )
        if folder:
            self.dlg.lineEdit_QGISFolder.setText(folder)

    def SyncQFieldToQGIS(self):
        """
        Sync data from a QField package into a copy of the QGIS project,
        overwriting existing field data with the QField version.
        """
        # Retrieve paths
        qfield_path = self.dlg.lineEdit_QFieldPackage.text().strip()
        qgis_folder = self.dlg.lineEdit_QGISFolder.text().strip()

        # Basic validation
        if not qfield_path or not qgis_folder:
            self.iface.messageBar().pushMessage(
                "Please select both a QField package and a GEOL-QMAPS QGIS project folder.",
                level=Qgis.Warning,
                duration=10
            )
            return

        # Confirm overwrite
        reply = QtWidgets.QMessageBox.warning(
            self.iface.mainWindow(),
            "Confirm Sync",
            ("This operation will create a copy of the QGIS project folder and "
             "overwrite its current field data with that from the QField package. "
             "Ensure both sources are the latest versions. Continue?"),
            QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel,
            QtWidgets.QMessageBox.Cancel
        )
        if reply != QtWidgets.QMessageBox.Ok:
            return

        # Drive check (C: or other non-removable)
        drive = os.path.splitdrive(qfield_path)[0]
        if drive.upper().startswith('\\') or not drive:
            self.iface.messageBar().pushMessage(
                "Please place the QField package on a local or fixed external drive.",
                level=Qgis.Critical,
                duration=10
            )
            if temp_dir:
                temp_dir.cleanup()
            return

        # Step 1: Handle QField package (ZIP or folder)
        temp_dir = None
        if qfield_path.lower().endswith('.zip'):
            temp_dir = tempfile.TemporaryDirectory()
            with zipfile.ZipFile(qfield_path, 'r') as zf:
                zf.extractall(temp_dir.name)
            package_root = temp_dir.name
        else:
            package_root = qfield_path

        # Step 1b: Locate .qgs file and set root for CURRENT_MISSION.gpkg
        qgs_files = []
        for root, dirs, files in os.walk(package_root):
            for f in files:
                if f.lower().endswith('.qgs'):
                    qgs_files.append(os.path.join(root, f))
        if not qgs_files:
            self.iface.messageBar().pushMessage(
                "No .qgs project file found in the QField package: may be corrupted.",
                level=Qgis.Critical,
                duration=5
            )
            if temp_dir:
                temp_dir.cleanup()
            return
        qgs_path = qgs_files[0]
        pkg_root = os.path.dirname(qgs_path)

        # Check for CURRENT_MISSION.gpkg in QField root
        qfield_pkg = os.path.join(pkg_root, 'CURRENT_MISSION.gpkg')
        if not os.path.isfile(qfield_pkg):
            self.iface.messageBar().pushMessage(
                "No CURRENT_MISSION.gpkg found in the QField package: corrupted, or not packaged from a GEOL-QMAPS QGIS project.",
                level=Qgis.Critical,
                duration=10
            )
            if temp_dir:
                temp_dir.cleanup()
            return

        # Step 2: Validate QGIS project via existing function
        if not self.is_valid_geol_qmaps_project(qgis_folder):
            self.iface.messageBar().pushMessage(
                "Selected QGIS folder is not a valid GEOL‑QMAPS project.",
                level=Qgis.Critical,
                duration=10
            )
            if temp_dir:
                temp_dir.cleanup()
            return
            return

        # Step 3: Copy QGIS folder
        base_dir, name = os.path.split(qgis_folder.rstrip(os.sep))
        synced_name = f"{name}_Synced"
        dest_folder = os.path.join(base_dir, synced_name)
        if os.path.exists(dest_folder):
            shutil.rmtree(dest_folder)
        shutil.copytree(qgis_folder, dest_folder)
        # Validate full-path lengths in the synced project
        if not self._check_max_filename_length(dest_folder):
            # abort if any path >256 chars
            return

        # Step 4: Overwrite CURRENT_MISSION.gpkg
        dest_pkg = os.path.join(dest_folder, self.dir_0, 'CURRENT_MISSION.gpkg')
        shutil.copy2(qfield_pkg, dest_pkg)

        # Step 5: Copy DCIM (if present)
        qfield_dcim = os.path.join(pkg_root, 'DCIM')
        dest_dcim = os.path.join(dest_folder, self.dir_0, 'DCIM')
        if os.path.isdir(qfield_dcim):
            os.makedirs(dest_dcim, exist_ok=True)
            for fname in os.listdir(qfield_dcim):
                src_file = os.path.join(qfield_dcim, fname)
                dst_file = os.path.join(dest_dcim, fname)
                shutil.copy2(src_file, dst_file)
                #Validate full-path lengths in the synced DCIM
                if not self._check_max_filename_length(dest_dcim):
                    # abort if any DCIM path >256 chars
                    return

        # Clean up temporary directory
        if temp_dir:
            temp_dir.cleanup()

        self.iface.messageBar().pushMessage(
            f"Synchronisation complete: '{dest_folder}' created.",
            level=Qgis.Success,
            duration=5
        )

        # Clear the input field once done
        self.dlg.lineEdit_QFieldPackage.clear()
        self.dlg.lineEdit_QGISFolder.clear()


    ### Merge Projects ###
    def list_csv_files(directory):
        files = []
        for filename in os.listdir(directory):
            path = os.path.join(directory, filename)
            if os.path.isfile(path) and path.split(".")[-1] == "csv":
                files.append(filename)
        return files

    ### Remove multiple instances of the same feature in current project based on their unique ID (UUID field)

    def removeDuplicates(self):

        project = (
            QgsProject.instance()
        )  # assumes one of the projects is actually open!  Could use copy stored in plugin?

        # set up directory structure and load filename lists

        shp_list = self.mynormpath(
            os.path.dirname(os.path.realpath(__file__)) + "/shp.csv"
        )

        shps = pd.read_csv(shp_list, names=["name", "dir_code"])
        shps = shps.set_index("name")

        # Specify the field name based on which duplicates should be identified
        field_name = "UUID"

        # remove objects with duplicate UUIDs
        for layer in project.mapLayers().values():
            # Check if the layer name matches the target name
            # if layer.name() in shps.index.tolist():
            if (
                layer.name().startswith("Compilation_")
                or layer.name() in shps.index.tolist()
            ):

                # Start editing the layer
                layer.startEditing()

                # Initialize a set to track unique values
                unique_values = set()

                # List to store IDs of features to be deleted
                features_to_delete = []

                # Loop through features in the layer
                for feature in layer.getFeatures():
                    field_value = feature[field_name]

                    # Check if the field value has been encountered before
                    if field_value in unique_values:
                        # If it's a duplicate, mark the feature for deletion
                        features_to_delete.append(feature.id())
                    else:
                        # If it's unique, add the value to the set
                        unique_values.add(field_value)

                # Delete the duplicate features
                if features_to_delete:
                    layer.deleteFeatures(features_to_delete)

                # Commit the changes
                if layer.commitChanges():
                    pass

                # Refresh the layer to see changes
            layer.triggerRepaint()

            self.iface.messageBar().pushMessage(
                "Duplicate UUIDs removed, saved in current project",
                level=Qgis.Success,
                duration=5,
            )

    # get list of csv files in the csv directory on the fly
    def get_csv_list_old(self, path):
        extensions = [".csv"]
        # Get the list of matching files
        file_list = self.FM_Import.find_files_with_extensions(path, extensions)
        names = []
        for file in file_list:
            file_prefix = str(os.path.basename(file)).replace(".csv", "")
            names.append(file_prefix)
        return names

    def get_first_column_text(self, gpkg_path, layer_name):
        """
        Get the first column of text values from a specified layer in a GeoPackage.

        Parameters:
            gpkg_path (str): Path to the GeoPackage file.
            layer_name (str): The name of the layer to extract data from.

        Returns:
            list: A list of text values from the first column, or an empty list if no text column is found.
        """
        import fiona

        try:
            # Open the specified layer in the GeoPackage
            with fiona.open(gpkg_path, layer=layer_name) as layer:
                # Get the schema (field names and types)
                fields = layer.schema["properties"]

                # Find the first text column
                first_text_column = next(
                    (
                        field
                        for field, field_type in fields.items()
                        if field_type.startswith("str")
                    ),
                    None,
                )

                if not first_text_column:
                    print(f"No text column found in layer: {layer_name}")
                    return []

                # Extract values from the first text column
                text_values = [
                    feature["properties"][first_text_column]
                    for feature in layer
                    if feature["properties"][first_text_column] is not None
                ]

                return text_values
        except Exception as e:
            print(f"Error reading layer '{layer_name}' from GeoPackage: {e}")
            return []

    # get list of csv files in the csv directory on the fly
    def get_csv_list(self, gpkg_path):
        import fiona

        """
        Get a list of layer names in a GeoPackage file.

        Parameters:
            gpkg_path (str): Path to the GeoPackage file.

        Returns:
            list: A list of layer names in the GeoPackage.
        """

        try:
            # Open the GeoPackage and retrieve its layer names
            layers = fiona.listlayers(gpkg_path)
            return layers
        except Exception as e:
            print(f"Error reading GeoPackage: {e}")
            return []

    # copy files to new directory checking for duplicates
    def recursive_overwrite(self, src, dest, ignore=None):
        if os.path.isdir(src):
            if not os.path.isdir(dest):
                os.makedirs(dest)
            files = os.listdir(src)
            if ignore is not None:
                ignored = ignore(src, files)
            else:
                ignored = set()
            for f in files:
                if f not in ignored:
                    self.recursive_overwrite(
                        os.path.join(src, f), os.path.join(dest, f), ignore
                    )
        else:
            shutil.copyfile(src, dest)

    def merge_text_tables_remove_duplicates(
        self,
        gpkg1_path,
        layer1_name,
        gpkg2_path,
        layer2_name,
        output_gpkg_path,
        output_layer_name,
    ):
        """
        Merge two text table layers (without geometry) from different GeoPackages into a new GeoPackage, removing duplicates.

        Parameters:
            gpkg1_path (str): Path to the first GeoPackage.
            layer1_name (str): Name of the text table layer in the first GeoPackage.
            gpkg2_path (str): Path to the second GeoPackage.
            layer2_name (str): Name of the text table layer in the second GeoPackage.
            output_gpkg_path (str): Path to the output GeoPackage.
            output_layer_name (str): Name of the output layer.
        """
        import fiona

        try:
            # Open the first layer
            with fiona.open(gpkg1_path, layer=layer1_name) as layer1:
                layer1_schema = layer1.schema
                features1 = list(layer1)

            # Open the second layer
            with fiona.open(gpkg2_path, layer=layer2_name) as layer2:
                # Ensure the schema matches between layers
                if layer1.schema != layer2.schema:
                    raise ValueError("Schema mismatch between layers.")

                features2 = list(layer2)

            # Merge features and remove duplicates
            seen = set()
            unique_features = []

            for feature in features1 + features2:
                # Use properties (attributes) as the unique identifier
                properties = tuple(feature["properties"].items())

                if properties not in seen:
                    seen.add(properties)
                    unique_features.append(feature)

            ## Create a new GeoPackage for the output layer
            # if os.path.exists(output_gpkg_path):
            #    os.remove(output_gpkg_path)  # Overwrite if the file exists

            with fiona.open(
                output_gpkg_path,
                mode="w",
                driver="GPKG",
                layer=output_layer_name,
                schema=layer1_schema,
            ) as output_layer:
                output_layer.writerecords(unique_features)
            # print(output_gpkg_path, output_layer_name)
            # print(f"Successfully merged text table layers into {output_gpkg_path} (layer: {output_layer_name}).")

        except Exception as e:
            print(f"Error merging text table layers: {e}")

    def list_layers_from_gpkg(self, gpkg_path):
        """
        Returns a list of layer names contained in a GeoPackage.

        :param gpkg_path: Path to the GeoPackage file.
        :return: List of layer names.
        """
        layer = QgsVectorLayer(gpkg_path, "temp_layer", "ogr")
        if not layer.isValid():
            print(f"Failed to open GeoPackage: {gpkg_path}")
            return []

        sub_layers = layer.dataProvider().subLayers()
        return [subLayer.split("!!::!!")[1] for subLayer in sub_layers]

    def merge_current_to_existing(self):
        """
        Combines layers from source geopackage with matching 'Compilation_' prefixed layers
        in the target geopackage. Updates target layers with combined data and then clears
        the source layers while preserving their structure.
        """
        shp_list = self.mynormpath(
            os.path.dirname(os.path.realpath(__file__)) + "/shp.csv"
        )

        project = QgsProject.instance()
        proj_file_path = project.fileName()

        head_tail = os.path.split(proj_file_path)
        main_project_path = head_tail[0] + "/"

        shps = pd.read_csv(shp_list, names=["name", "dir_code"])
        shps = shps.set_index("name")

        source_gpkg_path = self.mynormpath(
            main_project_path + self.dir_0 + "CURRENT_MISSION.gpkg"
        )

        # Get list of layers from source geopackage
        source_layers = []
        source_gpkg = QgsVectorLayer(source_gpkg_path, "source_gpkg", "ogr")
        for child in source_gpkg.dataProvider().subLayers():
            layer_name = child.split("!!::!!")[1]  # Get layer name from sublayer string
            source_layers.append(layer_name)

        print(f"Found {len(source_layers)} layers in source geopackage")

        # Process each layer
        for source_layer_name in source_layers:
            if source_layer_name in shps.index.tolist():
                print("Processing source layer:", source_layer_name)
                try:
                    # Get the layers by name
                    source_layer = QgsProject.instance().mapLayersByName(
                        source_layer_name
                    )[0]
                    compilation_layer = QgsProject.instance().mapLayersByName(
                        "Compilation_" + source_layer_name
                    )[0]

                    # Find the maximum FID in the compilation layer
                    max_fid = -1
                    for feat in compilation_layer.getFeatures():
                        fid = feat.id()
                        if fid > max_fid:
                            max_fid = fid

                    # Start new FIDs from max_fid + 1000000 to ensure no conflicts
                    next_fid = max_fid + 1000000

                    # Start editing the compilation layer
                    if not compilation_layer.startEditing():
                        print(f"Failed to start editing {compilation_layer.name()}")
                        continue

                    # Add features one by one with new FIDs
                    features_added = 0
                    errors = []
                    new_features = []  # Collect features before adding them

                    for feature in source_layer.getFeatures():
                        new_feature = QgsFeature()
                        new_feature.setFields(compilation_layer.fields())

                        # Set attributes
                        attrs = {}
                        for field in compilation_layer.fields():
                            field_name = field.name()
                            if field_name.lower() == "fid":
                                attrs[field_name] = next_fid
                            else:
                                try:
                                    attrs[field_name] = feature[field_name]
                                except KeyError:
                                    attrs[field_name] = None

                        new_feature.setAttributes(
                            [
                                attrs[field.name()]
                                for field in compilation_layer.fields()
                            ]
                        )
                        new_feature.setGeometry(feature.geometry())
                        new_features.append(new_feature)
                        next_fid += 1

                    # Add all features at once
                    if compilation_layer.addFeatures(new_features):
                        features_added = len(new_features)
                    else:
                        errors.append("Failed to add features batch")

                    # Commit the changes to compilation layer
                    if compilation_layer.commitChanges():
                        compilation_layer.updateExtents()
                        compilation_layer.triggerRepaint()
                        print(
                            f"Successfully added {features_added} features to {compilation_layer.name()}"
                        )

                        # Now clear the source layer
                        if source_layer.startEditing():
                            # Delete all features while preserving structure
                            feature_ids = [
                                feature.id() for feature in source_layer.getFeatures()
                            ]
                            source_layer.deleteFeatures(feature_ids)

                            if source_layer.commitChanges():
                                print(f"Successfully cleared {source_layer.name()}")
                                source_layer.updateExtents()
                                source_layer.triggerRepaint()
                            else:
                                print(
                                    f"Failed to clear {source_layer.name()}. Errors:",
                                    source_layer.commitErrors(),
                                )
                                source_layer.rollBack()
                        else:
                            print(
                                f"Failed to start editing {source_layer.name()} for clearing"
                            )

                        if errors:
                            print("Errors occurred during merge:")
                            for error in errors:
                                print(f"  {error}")
                    else:
                        print(
                            f"Failed to commit changes to {compilation_layer.name()}. Errors:",
                            compilation_layer.commitErrors(),
                        )
                        compilation_layer.rollBack()

                except IndexError:
                    print(
                        f"Could not find layer {source_layer_name} or its compilation counterpart"
                    )
                except Exception as e:
                    print(f"Error processing {source_layer_name}: {str(e)}")

    def delete_gpkg_layer(self, geopackage_path, layer_name):
        from osgeo import ogr
        import os

        # Open the GeoPackage using OGR
        gpkg_ds = ogr.Open(geopackage_path, 1)  # 1 = Open in update mode

        if gpkg_ds:
            # Execute the DROP TABLE command
            sql = f'DROP TABLE "{layer_name}"'
            gpkg_ds.ExecuteSQL(sql)
            gpkg_ds = None  # Close the connection
            print(
                f"✅ Layer '{layer_name}' successfully removed from {geopackage_path}."
            )
        else:
            print(f"⚠️ Error: Unable to open GeoPackage '{geopackage_path}'!")

    def refresh_layer(self, layer_name):
        layer = QgsProject.instance().mapLayersByName(layer_name)[0]

        if layer:
            # Refresh the layer to update feature count in the browser
            layer.triggerRepaint()
            layer.commitChanges()

            # print(f"Feature count for layer '{layer.name()}' updated.")
        else:
            print("Layer not found.")

    #Merge Projects
    def mergeProjects(self):
        """Merge a main and a sub QGIS project into a single output project folder."""
        # Retrieve inputs
        main_qgz = self.dlg.lineEdit_11.text().strip()
        sub_qgz = self.dlg.lineEdit_26.text().strip()
        out_dir = self.dlg.lineEdit_37.text().strip()
        if not all([main_qgz, sub_qgz, out_dir]):
            self.iface.messageBar().pushMessage(
                "Please specify main project, sub project, and output directory.",
                level=Qgis.Warning, duration=10
            )
            return

        # Define folders
        main_folder = os.path.dirname(main_qgz) + os.sep
        sub_folder = os.path.dirname(sub_qgz) + os.sep
        merge_root = self.mynormpath(out_dir.rstrip('/\\')) + os.sep

        # PRE-FLIGHT: simulate every file/dir that would end up under merge_root,
        # and abort if any full path would exceed 256 characters.
        # ─────────────────────────────────────────────────────────────────────────
        bad = []
        max_len = 256

        for base in (main_folder, sub_folder):
            for root, dirs, files in os.walk(base):
                for name in dirs + files:
                    rel_path = os.path.relpath(os.path.join(root, name), base)
                    dest_path = os.path.abspath(os.path.join(merge_root, rel_path))
                    if len(dest_path) > max_len:
                        bad.append(dest_path)

        if bad:
            # Write offending paths to a temp report
            report = os.path.join(tempfile.gettempdir(), "geol_qmaps_merge_bad_paths.txt")
            with open(report, 'w', encoding='utf-8') as f:
                for p in bad:
                    f.write(p + "\n")

            # Format hyperlink
            url = report.replace("\\", "/")
            if not url.startswith("/"):
                url = "/" + url

            # Critical message with download link
            self.iface.messageBar().pushMessage(
                f"{len(bad)} paths would exceed {max_len} chars. "
                f"<a href='file://{url}'>Download full list</a>. Shorten folders and/or file paths before proceeding further.",
                level=Qgis.Critical,
                duration=0
            )
            # Clear input fields for next operation
            self.dlg.lineEdit_11.clear()
            self.dlg.lineEdit_26.clear()
            self.dlg.lineEdit_37.clear()
            return

        # Unload any existing layers from prior merges
        proj = QgsProject.instance()
        for lyr in list(proj.mapLayers().values()):
            if lyr.source().startswith(merge_root):
                proj.removeMapLayer(lyr.id())

        # Flush caches and GC
        try:
            from osgeo import gdal
            if hasattr(gdal, 'GDALFlushCache'): gdal.GDALFlushCache()
        except:
            pass
        import gc; gc.collect()

        # Remove old output directory
        def _rm_readonly(func, path, exc):
            import stat
            os.chmod(path, stat.S_IWRITE)
            func(path)
        if os.path.isdir(merge_root):
            shutil.rmtree(merge_root, onerror=_rm_readonly)

        # Copy main project tree, ignoring project files
        shutil.copytree(
            main_folder, merge_root,
            ignore=shutil.ignore_patterns('*.qgs','*.qgz','*.bak')
        )

        # Helpers
        def merge_folder(src, dst):
            if not os.path.isdir(src): return
            os.makedirs(dst, exist_ok=True)
            for root, dirs, files in os.walk(src):
                rel = os.path.relpath(root, src)
                odir = os.path.join(dst, rel)
                os.makedirs(odir, exist_ok=True)
                for f in files:
                    sf = os.path.join(root, f)
                    df = os.path.join(odir, f)
                    if not os.path.exists(df): shutil.copy2(sf, df)

        def append_gpkg(src_gpkg, dst_gpkg):
            # Append features from src_gpkg into dst_gpkg without editing layer state
            ds_src = ogr.Open(src_gpkg)
            ds_dst = ogr.Open(dst_gpkg, 1)  # open for update
            if not ds_src or not ds_dst:
                return
            for i in range(ds_src.GetLayerCount()):
                layer = ds_src.GetLayer(i)
                name = layer.GetName()
                # Prepare URIs
                dst_uri = f"{dst_gpkg}|layername={name}"
                src_uri = f"{src_gpkg}|layername={name}"
                dst_vl = QgsVectorLayer(dst_uri, name, 'ogr')
                src_vl = QgsVectorLayer(src_uri, name, 'ogr')
                # Identify PK fields to null out
                pk_indices = dst_vl.dataProvider().pkAttributeIndexes()
                to_add = []
                for feat in src_vl.getFeatures():
                    new_feat = QgsFeature(dst_vl.fields())
                    new_feat.setGeometry(feat.geometry())
                    attrs = feat.attributes()
                    for idx in pk_indices:
                        if 0 <= idx < len(attrs):
                            attrs[idx] = None
                    new_feat.setAttributes(attrs)
                    new_feat.setId(-1)
                    to_add.append(new_feat)
                if to_add:
                    dst_vl.dataProvider().addFeatures(to_add)
            ds_src = None
            ds_dst = None

        # Merge GPKGs and DCIM
        for repo in (self.dir_0, self.dir_1):
            merge_rep = os.path.join(merge_root, repo)
            sub_rep = os.path.join(sub_folder, repo)
            #main_rep = os.path.join(main_folder, repo)
            for gpkg in ('CURRENT_MISSION.gpkg','COMPILATION.gpkg'):
                if (repo == self.dir_0 and gpkg == 'CURRENT_MISSION.gpkg') or (repo == self.dir_1 and gpkg == 'COMPILATION.gpkg'):
                    sub_g = os.path.join(sub_rep,gpkg)
                    #main_g = os.path.join(main_rep,gpkg)
                    merge_g = os.path.join(merge_rep,gpkg)
                    #append_gpkg(main_g, merge_g)
                    append_gpkg(sub_g,merge_g)
            #merge_folder(os.path.join(main_rep, 'DCIM'), os.path.join(merge_rep, 'DCIM'))
            merge_folder(os.path.join(sub_rep,'DCIM'), os.path.join(merge_rep,'DCIM'))

        # Merge other directories
        excl = {self.dir_0.strip('/\\'),self.dir_1.strip('/\\'),'DCIM'}
        for d in os.listdir(sub_folder):
            if d in excl: continue
            sp = os.path.join(sub_folder,d)
            if os.path.isdir(sp): merge_folder(sp, os.path.join(merge_root,d))

        # Determine merged project name based on longest common substring of input names
        def longest_common_substring(s1, s2):
            """Return the longest common substring between s1 and s2 (case-insensitive)."""
            s1_low, s2_low = s1.lower(), s2.lower()
            m = [[0] * (len(s2_low) + 1) for _ in range(len(s1_low) + 1)]
            longest, x_longest = 0, 0
            for i in range(1, len(s1_low) + 1):
                for j in range(1, len(s2_low) + 1):
                    if s1_low[i-1] == s2_low[j-1]:
                        m[i][j] = m[i-1][j-1] + 1
                        if m[i][j] > longest:
                            longest = m[i][j]
                            x_longest = i
            # extract substring from original-cased s1
            return s1[x_longest-longest: x_longest]

        main_base = os.path.splitext(os.path.basename(main_qgz))[0]
        sub_base = os.path.splitext(os.path.basename(sub_qgz))[0]
        common = longest_common_substring(main_base, sub_base)
        if len(common) >= 5:
            prefix = common
            merged_filename = f"{prefix}_merged.qgz"
        else:
            merged_filename = "Merged.qgz"
        merged_path = os.path.join(merge_root, merged_filename)
        shutil.copyfile(main_qgz, merged_path)

        # Rewrite QGZ internals to use relative paths
        import zipfile
        import xml.etree.ElementTree as ET
        # use the module‐level tempfile
        tmp = tempfile.mkdtemp()

        with zipfile.ZipFile(merged_path,'r') as zin: zin.extractall(tmp)
        qgs = os.path.join(tmp,os.path.splitext(os.path.basename(main_qgz))[0]+'.qgs')

        # find the first .qgs we laid down
        import glob
        qgs_files = glob.glob(os.path.join(tmp, '*.qgs'))
        if not qgs_files:
            self.iface.messageBar().pushMessage(
                "Could not find the embedded .qgs inside the merged QGZ.",
                level=Qgis.Critical, duration=10
            )
            shutil.rmtree(tmp)
            # Clear input fields for next operation
            self.dlg.lineEdit_11.clear()
            self.dlg.lineEdit_26.clear()
            self.dlg.lineEdit_37.clear()
            return

        qgs = qgs_files[0]
        # now parse & rewrite relative paths
        tree = ET.parse(qgs); root = tree.getroot()
        for ds in root.findall('.//datasource'):
            p = ds.text
            if p and os.path.isabs(p):
                ds.text = os.path.relpath(p, merge_root)
        tree.write(qgs,encoding='UTF-8')

        # repack and clean up
        with zipfile.ZipFile(merged_path,'w',zipfile.ZIP_DEFLATED) as zout:
            for fn in os.listdir(tmp): zout.write(os.path.join(tmp,fn),arcname=fn)
        shutil.rmtree(tmp)

        # Final GC
        try:
            from osgeo import gdal
            if hasattr(gdal,'GDALFlushCache'): gdal.GDALFlushCache()
        except: pass
        import gc; gc.collect()

        # Clear input fields for next operation
        self.dlg.lineEdit_11.clear()
        self.dlg.lineEdit_26.clear()
        self.dlg.lineEdit_37.clear()

        self.iface.messageBar().pushMessage(
            f"Projects merged into {merged_path}", level=Qgis.Success, duration=15
        )

    ### Export Data ###
    def exportData(self):
        # Combines sets of lithology, structure and zoneal layers into 3 shapefiles
        if os.path.exists(self.mynormpath(self.dlg.lineEdit_7.text())):
            proj = QgsProject.instance()

            for name in self.layers_names:
                layer = proj.mapLayersByName(name)[0]
                caps = layer.dataProvider().capabilities()

                # Get the list of fields in the layer
                existing_fields = [field.name() for field in layer.fields()]

                # Check if 'src_layer' field exists, if not, add it
                if "Layer" not in existing_fields:
                    # Add Fields if the provider supports it
                    if caps & QgsVectorDataProvider.AddAttributes:
                        res = layer.dataProvider().addAttributes(
                            [QgsField("Layer", QVariant.String)]
                        )
                        layer.updateFields()

                src_layer_idx = layer.fields().lookupField("Layer")

                # Start editing mode to modify attributes
                layer.startEditing()
                # Change attribute values
                for f in layer.getFeatures():
                    layer.changeAttributeValue(f.id(), src_layer_idx, name)

                # Commit changes
                layer.commitChanges()

            project = QgsProject.instance()
            proj_file_path = project.fileName()

            # Build the export folder
            export_folder = self.mynormpath(self.dlg.lineEdit_7.text())
            os.makedirs(export_folder, exist_ok=True)

            # ─── Pre-flight #1: check whole export folder ───
            if not self._check_max_filename_length(export_folder):
                self.dlg.lineEdit_7.clear()
                return

            # Define the GeoPackage path
            newGeopackagePath = self.mynormpath(
                os.path.join(self.dlg.lineEdit_7.text(), "export.gpkg")
            )

            # ─── Pre-flight #2: check the GeoPackage path itself ───
            full_gpkg = os.path.abspath(newGeopackagePath)
            if len(full_gpkg) > 256:
                self.iface.messageBar().pushMessage(
                    f"ERROR: Export GeoPackage path too long ({len(full_gpkg)} chars): {full_gpkg}",
                    level=Qgis.Critical,
                    duration=10
                )
                self.dlg.lineEdit_7.clear()
                return

            file = []


            # first create temp layer and new geopackage
            temp_layer = QgsVectorLayer("Point?crs=EPSG:4326", "temp_layer", "memory")

            options = QgsVectorFileWriter.SaveVectorOptions()
            options.driverName = "GPKG"  # Specify the GeoPackage format

            transform_context = QgsCoordinateTransformContext()

            QgsVectorFileWriter.writeAsVectorFormatV3(
                temp_layer, newGeopackagePath, transform_context, options
            )

            # Zonal Data
            file1 = (
                self.geopackage_file_path
                + "|layername=Compilation_Deformation zones_PG"
            )
            file2 = (
                self.geopackage_file_path + "|layername=Compilation_Alteration zones_PG"
            )
            file3 = (
                self.geopackage_file_path + "|layername=Compilation_Lithology zones_PG"
            )

            newLayer = (
                "ogr:dbname='"
                + newGeopackagePath
                + "' table=\""
                + "zonal_data"
                + '" (geom)'
            )

            params = {
                "LAYERS": [file1, file2, file3],
                "OUTPUT": newLayer,
            }

            processing.run("native:mergevectorlayers", params)


            # Lithology Data
            file1 = (
                self.geopackage_file_path
                + "|layername=Compilation_Local lithologies_PT"
            )
            file2 = (
                self.geopackage_file_path
                + "|layername=Compilation_Supergene lithologies_PT"
            )
            file3 = (
                self.geopackage_file_path
                + "|layername=Compilation_Sedimentary lithologies_PT"
            )
            file4 = (
                self.geopackage_file_path
                + "|layername=Compilation_Volcanoclastic lithologies_PT"
            )
            file5 = (
                self.geopackage_file_path
                + "|layername=Compilation_Igneous extrusive lithologies_PT"
            )
            file6 = (
                self.geopackage_file_path
                + "|layername=Compilation_Igneous intrusive lithologies_PT"
            )
            file7 = (
                self.geopackage_file_path
                + "|layername=Compilation_Metamorphic lithologies_PT"
            )

            newLayer = (
                "ogr:dbname='"
                + newGeopackagePath
                + "' table=\""
                + "litho_data"
                + '" (geom)'
            )

            params = {
                "LAYERS": [file1, file2, file3, file4, file5, file6, file7],
                "OUTPUT": newLayer,
            }

            processing.run("native:mergevectorlayers", params)

            # Structural Data
            file1 = (
                self.geopackage_file_path
                + "|layername=Compilation_Bedding-Lava flow-S0_PT"
            )
            file2 = (
                self.geopackage_file_path
                + "|layername=Compilation_Foliation-cleavage_PT"
            )
            file3 = (
                self.geopackage_file_path
                + "|layername=Compilation_Shear zones and faults_PT"
            )
            file4 = (
                self.geopackage_file_path
                + "|layername=Compilation_Folds_PT"
            )
            file5 = (
                    self.geopackage_file_path
                    + "|layername=Compilation_Fractures_PT"
            )
            file6 = (self.geopackage_file_path
                     + "|layername=Compilation_Veins_PT"
            )
            file7 = (
                    self.geopackage_file_path
                    + "|layername=Compilation_Dikes-Sills_PT"
                    )
            file8 = (
                    self.geopackage_file_path
                    + "|layername=Compilation_Lithological contacts_PT"
            )
            file9 = (
                    self.geopackage_file_path
                    + "|layername=Compilation_Lineations_PT"
            )

            newLayer = (
                "ogr:dbname='"
                + newGeopackagePath
                + "' table=\""
                + "structure_data"
                + '" (geom)'
            )

            params = {
                "LAYERS": [
                    file1,
                    file2,
                    file3,
                    file4,
                    file5,
                    file6,
                    file7,
                    file8,
                    file9
                ],
                "OUTPUT": newLayer,
            }

            processing.run("native:mergevectorlayers", params)

            # Geophysical Data
            file1 = (
                self.geopackage_file_path
                + "|layername=Compilation_Density_PT"
            )
            file2 = (
                self.geopackage_file_path
                + "|layername=Compilation_Magnetic susceptibility_PT"
            )

            newLayer = (
                "ogr:dbname='"
                + newGeopackagePath
                + "' table=\""
                + "geophy_data"
                + '" (geom)'
            )

            params = {
                "LAYERS": [file1, file2],
                "OUTPUT": newLayer,
            }

            processing.run("native:mergevectorlayers", params)

            # Stops-Samples-Photographs-Comments Data
            file1 = (
                self.geopackage_file_path
                + "|layername=Compilation_Stops_PT"
            )
            file2 = (
                self.geopackage_file_path
                + "|layername=Compilation_Sampling_PT"
            )
            file3 = (
                self.geopackage_file_path
                + "|layername=Compilation_Photographs_PT"
            )
            file4 = (
                self.geopackage_file_path
                + "|layername=Compilation_Observations_PT"
            )

            newLayer = (
                "ogr:dbname='"
                + newGeopackagePath
                + "' table=\""
                + "stops-sampling-photographs-commentsPT_data"
                + '" (geom)'
            )

            params = {
                "LAYERS": [file1, file2, file3, file4],
                "OUTPUT": newLayer,
            }

            processing.run("native:mergevectorlayers", params)

            # Linear Data
            file1 = (
                self.geopackage_file_path
                + "|layername=Compilation_Observations_LN"
            )
            file2 = (
                self.geopackage_file_path
                + "|layername=Compilation_GPS Tracks_LN"
            )
            file3 = (
                self.geopackage_file_path
                + "|layername=Compilation_Lithological contacts_LN"
            )
            file4 = (
                self.geopackage_file_path
                + "|layername=Compilation_Planar structures_LN"
            )

            newLayer = (
                "ogr:dbname='"
                + newGeopackagePath
                + "' table=\""
                + "linear_data"
                + '" (geom)'
            )

            params = {
                "LAYERS": [file1, file2, file3, file4],
                "OUTPUT": newLayer,
            }

            processing.run("native:mergevectorlayers", params)

            # ————— Validate the full export.gpkg path length —————
            full_path = os.path.abspath(newGeopackagePath)
            if len(full_path) > 256:
                self.iface.messageBar().pushMessage(
                    f"ERROR: Export geopackage path too long ({len(full_path)} chars): {full_path}",
                    level=Qgis.Critical,
                    duration=10
                )
                self.dlg.lineEdit_7.clear()
                return

            self.iface.messageBar().pushMessage(
                f"Data successfully exported to {newGeopackagePath}",
                level=Qgis.Success,
                duration=10
            )


        else:
            self.iface.messageBar().pushMessage(
                "Directory not found: " + self.dlg.lineEdit_7.text(),
                level=Qgis.Warning,
                duration=45,
            )

    def mynormpath(self, path):
        return r"" + os.path.normpath(path).replace("\\", "/")

    def virtualStops(self):
        try:
            distance = self.dlg.lineEdit_53.text()
            from .dbscan import Basic_DBSCAN
            from datetime import datetime

            if not self.dlg.lineEdit_53.text():
                self.iface.messageBar().pushMessage(
                    "Please enter a distance value", level=Qgis.Warning, duration=5
                )
                return

            project = QgsProject.instance()
            file = []
            self.geopackage_file_path = self.mynormpath(self.geopackage_file_path)

            layer_names = [
                "Compilation_Lineations_PT",
                "Compilation_Folds_PT",
                "Compilation_Bedding-Lava flow-S0_PT",
                "Compilation_Foliation-cleavage_PT",
                "Compilation_Shear zones and faults_PT",
                "Compilation_Fractures_PT",
                "Compilation_Veins_PT",
                "Compilation_Dikes-Sills_PT",
                "Compilation_Local lithologies_PT",
                "Compilation_Supergene lithologies_PT",
                "Compilation_Sedimentary lithologies_PT",
                "Compilation_Volcanoclastic lithologies_PT",
                "Compilation_Igneous extrusive lithologies_PT",
                "Compilation_Igneous intrusive lithologies_PT",
                "Compilation_Metamorphic lithologies_PT",
                "Compilation_Lithological contacts_PT",
                "Compilation_Magnetic susceptibility_PT",
                "Compilation_Density_PT",
            ]

            for layer_name in layer_names:
                file.append(f"{self.geopackage_file_path}|layername={layer_name}")

            print("Starting layer merging process...")

            all_points = []
            all_features = []
            field_names = set()

            # Step 1: Find common fields across all layers
            for i, f in enumerate(file):
                test_layer = QgsVectorLayer(f, f"Layer_{i}", "ogr")
                if not test_layer.isValid():
                    print(f"Warning: Layer {i} is not valid, skipping")
                    continue

                # print(f"Processing layer {i}: {f}")

                layer_fields = set([field.name() for field in test_layer.fields()])
                if i == 0:
                    field_names = layer_fields  # First layer sets the common fields
                else:
                    field_names.intersection_update(
                        layer_fields
                    )  # Keep only common fields

                for feature in test_layer.getFeatures():
                    geom = feature.geometry()
                    if geom is None or geom.isEmpty():
                        print(f"Skipping empty geometry in layer {i}")
                        continue

                    if geom.wkbType() == QgsWkbTypes.Point:
                        point = geom.asPoint()
                        all_points.append([point.x(), point.y()])
                        all_features.append(feature)

            print(f"Total points collected from all layers: {len(all_points)}")
            print(f"Common fields for all layers: {list(field_names)}")

            if not all_points:
                self.iface.messageBar().pushMessage(
                    "No points found", level=Qgis.Warning, duration=45
                )
                return

            import numpy as np

            X = np.array(all_points)

            canvas = self.iface.mapCanvas()
            if canvas.mapUnits() == 6:
                distance = float(distance) / 111139.0
            else:
                distance = float(distance)

            scanner = Basic_DBSCAN(eps=distance, minPts=1)
            clusters = scanner.fit_predict(X)

            print(f"Clusters assigned: {set(clusters)}")

            # Step 2: Create memory layer with only common fields
            merged_layers = QgsVectorLayer(
                "Point?crs=EPSG:4326", "Virtual_Stops", "memory"
            )
            provider = merged_layers.dataProvider()

            # Add only common fields to the new layer
            new_fields = [
                QgsField(field_name, QVariant.String) for field_name in field_names
            ]
            new_fields.append(QgsField("v_stop", QVariant.String))  # Add v_stop field
            provider.addAttributes(new_fields)
            merged_layers.updateFields()

            # Step 3: Insert features dynamically handling missing fields
            merged_layers.startEditing()

            for i, feature in enumerate(all_features):
                new_feature = QgsFeature(
                    merged_layers.fields()
                )  # Ensure correct field structure
                geometry = feature.geometry()

                if geometry is None or geometry.isEmpty():
                    print(f"Skipping feature {i} due to missing geometry.")
                    continue

                new_feature.setGeometry(geometry)  # Ensure geometry is set

                # Extract only common field values dynamically
                feature_attributes = []
                for field_name in field_names:
                    try:
                        field_index = feature.fields().indexFromName(field_name)
                        if field_index != -1 and field_index < len(
                            feature.attributes()
                        ):
                            feature_attributes.append(feature.attributes()[field_index])
                        else:
                            raise IndexError
                    except IndexError:
                        print(
                            f"⚠ Warning: Field '{field_name}' missing for feature {i}, setting to None"
                        )
                        feature_attributes.append(None)

                feature_attributes.append(str(clusters[i]))  # Add v_stop value

                # Debugging: Print the extracted attributes for each feature
                # print(f"Feature {i} attributes: {feature_attributes}")

                new_feature.setAttributes(feature_attributes)

                # Debugging to check if feature is added correctly
                if provider.addFeature(new_feature):
                    pass
                    # print(f"✅ Successfully added feature {i}")
                else:
                    print(f"❌ Failed to add feature {i}")

            merged_layers.startEditing()
            if merged_layers.dataProvider().fieldNameIndex("Virtual_ID") == -1:
                merged_layers.dataProvider().addAttributes(
                    [QgsField("Virtual_ID", QVariant.String)]
                )
                merged_layers.updateFields()

            id_new_col = merged_layers.dataProvider().fieldNameIndex("Virtual_ID")

            for i, feature in enumerate(merged_layers.getFeatures()):
                if clusters[i] > 0:
                    merged_layers.changeAttributeValue(
                        feature.id(), id_new_col, str(clusters[i])
                    )

            print(
                f"Before commit: merged_layers feature count: {merged_layers.featureCount()}"
            )
            merged_layers.commitChanges()
            merged_layers.updateExtents()

            print(
                f"After commit: merged_layers feature count: {merged_layers.featureCount()}"
            )

            if merged_layers.featureCount() == 0:
                print("❌ ERROR: No features were added to the memory layer.")
                return

            output_path = QgsProject.instance().readPath("./") + "/"
            datestamp = datetime.now().strftime("%d-%b-%Y_%H_%M_%S")
            params = {
                "INPUT": merged_layers,
                "OPTIONS": "-update -nln " + "Virtual_Stops_" + datestamp,
                "OUTPUT": output_path + self.dir_0 + "CURRENT_MISSION.gpkg",
            }
            print("params2", params)

            processing.run("gdal:convertformat", params)

            virtual_path = self.mynormpath(
                output_path
                + self.dir_0
                + "/CURRENT_MISSION.gpkg|layername=Virtual_Stops_"
                + datestamp
            )
            self.iface.addVectorLayer(virtual_path, "Virtual_Stops_" + datestamp, "ogr")
            self.iface.messageBar().pushMessage(
                "Virtual Stop layer created", level=Qgis.Success, duration=5
            )

        except Exception as e:
            print(f"General error: {str(e)}")
            self.iface.messageBar().pushMessage(
                f"General error: {str(e)}", level=Qgis.Critical, duration=5
            )

    def virtualStops_old(self):
        distance = self.dlg.lineEdit_53.text()
        from .dbscan import Basic_DBSCAN
        from datetime import datetime

        if self.dlg.lineEdit_53.text():

            # Defines pseudo stop numbers based on proximity
            project = QgsProject.instance()
            proj_file_path = project.fileName()

            file = []
            self.geopackage_file_path = self.mynormpath(self.geopackage_file_path)
            file.append(
                self.geopackage_file_path + "|layername=Compilation_Lineations_PT"
            )
            file.append(self.geopackage_file_path + "|layername=Compilation_Folds_PT")

            file.append(
                self.geopackage_file_path
                + "|layername=Compilation_Bedding-Lava flow-S0_PT"
            )
            file.append(
                self.geopackage_file_path
                + "|layername=Compilation_Foliation-cleavage_PT"
            )
            file.append(
                self.geopackage_file_path
                + "|layername=Compilation_Shear zones and faults_PT"
            )
            file.append(
                self.geopackage_file_path + "|layername=Compilation_Fractures_PT"
            )
            file.append(self.geopackage_file_path + "|layername=Compilation_Veins_PT")
            file.append(
                self.geopackage_file_path + "|layername=Compilation_Dikes-Sills_PT"
            )

            file.append(
                self.geopackage_file_path
                + "|layername=Compilation_Local lithologies_PT"
            )
            file.append(
                self.geopackage_file_path
                + "|layername=Compilation_Supergene lithologies_PT"
            )
            file.append(
                self.geopackage_file_path
                + "|layername=Compilation_Sedimentary lithologies_PT"
            )
            file.append(
                self.geopackage_file_path
                + "|layername=Compilation_Volcanoclastic lithologies_PT"
            )
            file.append(
                self.geopackage_file_path
                + "|layername=Compilation_Igneous extrusive lithologies_PT"
            )
            file.append(
                self.geopackage_file_path
                + "|layername=Compilation_Igneous intrusive lithologies_PT"
            )
            file.append(
                self.geopackage_file_path
                + "|layername=Compilation_Metamorphic lithologies_PT"
            )
            file.append(
                self.geopackage_file_path
                + "|layername=Compilation_Lithological contacts_PT"
            )

            file.append(
                self.geopackage_file_path
                + "|layername=Compilation_Magnetic susceptibility_PT"
            )

            file.append(
                self.geopackage_file_path
                + "|layername=Compilation_Density_PT"
            )

            # Merge two shapefiles
            params = {"LAYERS": [file[0], file[1]], "OUTPUT": "memory:"}

            merged_layers = processing.run("native:mergevectorlayers", params)["OUTPUT"]

            if not merged_layers.isValid():
                print("Processing failed.")
            else:
                print("Processing succeeded.")

            for i, f in enumerate(file):

                if i > 1:  # ignore first two as already merged
                    # merge two shapefiles
                    params = {"LAYERS": [merged_layers, f], "OUTPUT": "memory:"}
                    print("params", params)

                    merged_layers = processing.run("native:mergevectorlayers", params)[
                        "OUTPUT"
                    ]

            points = []
            feats = merged_layers.getFeatures()
            for i, f in enumerate(feats):
                point = f.geometry()
                points.append([point.asPoint().x(), point.asPoint().y()])
            canvas = self.iface.mapCanvas()

            if (
                canvas.mapUnits() == 6
            ):  # if lat/long convert to metres, anything else is assuemd to be metres already (not a good idea)
                distance = float(distance) / 111139.0
            scanner = Basic_DBSCAN(eps=float(distance), minPts=1)
            """
            SIP_MONKEYPATCH_COMPAT_NAME 	0        Meters.
            SIP_MONKEYPATCH_COMPAT_NAME 	1        Kilometers.
            SIP_MONKEYPATCH_COMPAT_NAME 	2        Imperial feet.
            SIP_MONKEYPATCH_COMPAT_NAME 	3        Nautical miles.
            SIP_MONKEYPATCH_COMPAT_NAME 	4        Imperial yards.
            SIP_MONKEYPATCH_COMPAT_NAME 	5        Terrestrial miles.
            SIP_MONKEYPATCH_COMPAT_NAME 	6        Degrees, for planar geographic CRS distance measurements.
            SIP_MONKEYPATCH_COMPAT_NAME 	7        Centimeters.
            SIP_MONKEYPATCH_COMPAT_NAME 	8        Millimeters.
            Inches 	9        Inches (since QGIS 3.32)
            SIP_MONKEYPATCH_COMPAT_NAME 10        Unknown distance unit.
            """
            if len(points) > 0:
                X = np.array(points)
                X[0] = (X[0] - X[:, 0].mean()) / X[:, 0].std()
                X[1] = (X[1] - X[:, 1].mean()) / X[:, 1].std()
                # X = StandardScaler().fit_transform(X)

                clusters = scanner.fit_predict(X)

                merged_layers.startEditing()
                if merged_layers.dataProvider().fieldNameIndex("v_stop") == -1:
                    merged_layers.dataProvider().addAttributes(
                        [QgsField("v_stop", QVariant.String)]
                    )
                    merged_layers.updateFields()

                id_new_col = merged_layers.dataProvider().fieldNameIndex("v_stop")

                for i, feature in enumerate(merged_layers.getFeatures()):
                    if clusters[i] > 0:
                        merged_layers.changeAttributeValue(
                            feature.id(), id_new_col, str(clusters[i])
                        )

                merged_layers.commitChanges()
                if not merged_layers.isValid():
                    print("Layer failed to build!")
                else:
                    QgsProject.instance().addMapLayer(merged_layers, False)
                    output_path = QgsProject.instance().readPath("./") + "/"
                    datestamp = datetime.now().strftime("%d-%b-%Y_%H_%M_%S")
                    params = {
                        "INPUT": merged_layers,
                        "OPTIONS": "-update -nln " + "Virtual_Stops_" + datestamp,
                        "OUTPUT": output_path + self.dir_0 + "/CURRENT_MISSION.gpkg",
                    }
                    print("params2", params)

                    processing.run("gdal:convertformat", params)

                    virtual_path = self.mynormpath(
                        output_path
                        + self.dir_0
                        + "/CURRENT_MISSION.gpkg|layername=Virtual_Stops_"
                        + datestamp
                    )
                    self.iface.addVectorLayer(virtual_path, "", "ogr")
                    self.iface.messageBar().pushMessage(
                        "Virtual Stop layer created", level=Qgis.Success, duration=5
                    )
            else:
                self.iface.messageBar().pushMessage(
                    "No points found", level=Qgis.Warning, duration=45
                )

    def rmvLyr(lyrname):
        qinst = QgsProject.instance()

        qinst.removeMapLayer(qinst.mapLayersByName(lyrname)[0].id())

    ### Stereographic projection settings ###

    def set_stereoConfig(self):

        WAXI_projet_path = os.path.abspath(QgsProject.instance().fileName())
        stereoConfigPath = os.path.join(
            os.path.dirname(WAXI_projet_path), self.dir_99 + "/stereonet.json"
        )
        stereoConfig = {
            "showGtCircles": True,
            "showContours": True,
            "showKinematics": True,
            "linPlanes": True,
            "roseDiagram": True,
        }

        if os.path.exists(stereoConfigPath):
            with open(stereoConfigPath, "r") as json_file:
                stereoConfig = json.load(json_file)

        stereoConfig = {
            "showGtCircles": self.dlg.gtCircles_checkBox.isChecked(),
            "showContours": self.dlg.contours_checkBox.isChecked(),
            "showKinematics": self.dlg.kinematics_checkBox.isChecked(),
            "linPlanes": self.dlg.linPlanes_checkBox.isChecked(),
            "roseDiagram": self.dlg.rose_checkBox.isChecked(),
        }

        with open(stereoConfigPath, "w") as outfile:
            json.dump(stereoConfig, outfile, indent=4)

    def merge_2_layers(self):
        name1 = self.dlg.comboBox_merge1_2.currentText()
        name2 = self.dlg.comboBox_merge2_2.currentText()
        list1 = QgsProject.instance().mapLayersByName(name1)
        list2 = QgsProject.instance().mapLayersByName(name2)
        if not list1 or not list2:
            self.iface.messageBar().pushMessage(
                "ERROR: Could not find one of the selected layers.",
                level=Qgis.Critical, duration=10
            )
            return

        src = list1[0]  # scratch layer
        dst = list2[0]  # compilation layer

        if src == dst:
            self.iface.messageBar().pushMessage(
                "Please select two different layers.",
                level=Qgis.Warning, duration=10
            )
            return

        # Start an edit session on the destination
        if not dst.isEditable():
            dst.startEditing()

        # Figure out which field index holds your JSON back-reference
        idx_json = dst.fields().indexFromName("Existing databases - raw data")

        new_feats = []
        for f in src.getFeatures():
            nf = QgsFeature(dst.fields())
            nf.setGeometry(f.geometry())
            # copy every attribute (including UUID, Azimuth, Plunge, etc.)
            nf.setAttributes(f.attributes())

            # now inject your JSON text up‐front, if it exists
            try:
                uuid = f["UUID"]
                if uuid in self.sheetHashUUID:
                    nf.setAttribute(idx_json, self.sheetHashUUID[uuid][0])
            except (KeyError, IndexError):
                # source had no UUID or you didn’t map it—ignore
                pass

            new_feats.append(nf)

        # Add them all in one go
        ok = dst.addFeatures(new_feats)
        if not ok:
            dst.rollBack()
            self.iface.messageBar().pushMessage(
                "ERROR: Failed to merge features.",
                level=Qgis.Critical, duration=10
            )
            return

        # Commit and clean up
        if not dst.commitChanges():
            dst.rollBack()
            self.iface.messageBar().pushMessage(
                f"ERROR committing merge: {dst.commitErrors()}",
                level=Qgis.Critical, duration=15
            )
            return

        dst.triggerRepaint()
        # remove the scratch layer
        QgsProject.instance().removeMapLayer(src.id())
        self.iface.messageBar().pushMessage(
            f"Merged {len(new_feats)} features into '{dst.name()}'",
            level=Qgis.Success, duration=5
        )

    # Delete contents of a geopackage layer
    def drop_layer_contents(self, gpkg_path, layer_name):

        # Load the layer from the GeoPackage
        layer = QgsVectorLayer(f"{gpkg_path}|layername={layer_name}", layer_name, "ogr")

        # Check if the layer loaded successfully
        if not layer.isValid():
            print(f"Error: Failed to load layer '{layer_name}' from '{gpkg_path}'.")
        else:
            # Start an editing session
            if not layer.isEditable():
                layer.startEditing()

            # Get all feature IDs in the layer
            feature_ids = [feature.id() for feature in layer.getFeatures()]

            # Delete all features
            res = layer.dataProvider().deleteFeatures(feature_ids)

            if res:
                # Commit changes to save deletions
                layer.commitChanges()
            else:
                # Rollback if deletion failed
                layer.rollback()
                print(f"Error: Failed to delete features from layer '{layer_name}'.")

    # code to deisplay feature attirbutes and field names
    def print_feature_details(self, feature):
        # Print the feature ID
        # print(f"Feature ID: {feature.id()}")

        # Print attribute values
        attributes = feature.attributes()
        # print("Attributes:")
        fields = feature.fields()
        for index, attribute in enumerate(attributes):
            field_name = fields[index].name()  # Correct way to get field name
            # print(f"  {field_name}: {attribute}")

        # Print geometry as WKT (Well-Known Text)
        geometry = feature.geometry()
        if not geometry.isEmpty():
            pass
            # print(f"Geometry (WKT): {geometry.asWkt()}")
        else:
            print("Feature has no geometry")

    ### Update the source path of pictures in Photographs_PT and Sampling_PT layers ###

    def update_source_photo(self):  # ADD

        new_source_path = str(self.dlg.lineEdit_14.text())

        if os.path.exists(self.mynormpath(new_source_path)):

            layer_photographs_PT = QgsProject.instance().mapLayersByName(
                "Photographs_PT"
            )[0]
            layer_sampling_PT = QgsProject.instance().mapLayersByName("Sampling_PT")[0]
            source_field_index_photo = layer_photographs_PT.fields().indexFromName(
                "Source"
            )
            source_field_index_sampling = layer_sampling_PT.fields().indexFromName(
                "Source"
            )

            ## Option 1
            if self.dlg.option1_ckeckbox.isChecked():

                layer_photographs_PT.startEditing()

                for feature in layer_photographs_PT.getFeatures():
                    feature.setAttribute(source_field_index_photo, new_source_path)
                    layer_photographs_PT.updateFeature(feature)

                layer_photographs_PT.commitChanges()

                layer_sampling_PT.startEditing()

                for feature in layer_sampling_PT.getFeatures():
                    feature.setAttribute(source_field_index_sampling, new_source_path)
                    layer_sampling_PT.updateFeature(feature)

                layer_sampling_PT.commitChanges()

            ## Option 2
            if self.dlg.option2_ckeckbox.isChecked():

                # Create default value
                new_source_path_default = "'" + str(new_source_path) + "'"
                default_value = QgsDefaultValue(new_source_path_default)

                # Update default field value
                layer_photographs_PT.setDefaultValueDefinition(
                    source_field_index_photo, default_value
                )
                QgsProject.instance().write()

                # Update default field value
                layer_sampling_PT.setDefaultValueDefinition(
                    source_field_index_sampling, default_value
                )
                QgsProject.instance().write()

            self.iface.messageBar().pushMessage(
                (new_source_path + " is now the default directory for pictures"),
                level=Qgis.Success,
                duration=15,
            )

        else:
            self.iface.messageBar().pushMessage(
                "The path doesn't exist", level=Qgis.Warning, duration=45
            )

    def get_value_default(self, layer, field):  # ADD
        field_index = layer.fields().indexFromName(str(field))
        value = layer.defaultValueDefinition(field_index)
        defaut_value = value.expression()

        return defaut_value

    ### Save a new CURRENT_MISSION+DICTIONARIES.qlr file and a new COMPILATION.qlr file ###
    def save_template_style(self):
        """
        Save the group tree structure and all layer styles to a QLR file.

        Parameters:
            group_name (str): The name of the group to start with (e.g., "FIELD DATA").
            output_qlr_path (str): Path to save the QLR file.
        """
        # Get the root of the layer tree
        root = QgsProject.instance().layerTreeRoot()
        group_name = "FIELD DATA"

        # Find the target group
        target_group = None
        for child in root.children():
            if isinstance(child, QgsLayerTreeGroup) and child.name().startswith(
                group_name
            ):
                target_group = child
                break

        if not target_group:
            raise ValueError(f"Group '{group_name}' not found in the project.")

        # Ensure all layers in the group are valid and have styles
        for node in target_group.findLayers():
            layer = node.layer()
            if not layer.isValid():
                pass
                # print(f"Warning: Layer {layer.name()} is not valid and will be skipped.")
            else:
                print(f"Including layer: {layer.name()} with styles.")

        # Save the group with its hierarchy and styles
        # Build and export to a QLR file
        qlr_path = self.mynormpath(os.path.join(self.dlg.lineEdit_18.text(), "FIELD_DATA.qlr"))
        QgsLayerDefinition.exportLayerDefinition(
            qlr_path,
            [target_group]
        )

        self.dlg.lineEdit_18.clear()

        # ————— Validate full‐path length of the new QLR —————
        if not self._check_max_filename_length(os.path.dirname(qlr_path)):
            # abort if any full path >256 chars
            return

        self.iface.messageBar().pushMessage(
            f"Layer Definition style file exported as {qlr_path}",
            level=Qgis.Success,
            duration=45,
        )

        return qlr_path


    # Builds the XML structure of the layer tree recursively
    def build_layer_tree_xml(self, layer, parent_element):  # ADD

        # If it's a group :
        if isinstance(layer, QgsLayerTreeGroup):
            group_element = ET.SubElement(parent_element, "layer-tree-group")
            group_element.set("expanded", "1")
            group_element.set("checked", "Qt::Checked")
            group_element.set("groupLayer", "")
            group_element.set("name", layer.name())
            custom_properties = ET.SubElement(group_element, "customproperties")
            option = ET.SubElement(custom_properties, "Option")
            for child in layer.children():
                self.build_layer_tree_xml(child, group_element)

        # If it's a layer :
        elif isinstance(layer, QgsLayerTreeLayer):
            layer_element = ET.SubElement(parent_element, "layer-tree-layer")
            layer_element.set("providerKey", "ogr")
            layer_element.set("expanded", "1")
            layer_element.set("checked", "Qt::Checked")
            layer_element.set("id", layer.layerId())
            layer_element.set("patch_size", "-1,-1")
            layer_element.set("legend_split_behavior", "0")
            layer_element.set("name", layer.name())
            layer_element.set("source", layer.layer().source())
            layer_element.set("legend_exp", "")
            custom_properties = ET.SubElement(layer_element, "customproperties")
            option = ET.SubElement(custom_properties, "Option")

    ###############################################################################
    ###                              Reset windows                              ###
    ###############################################################################

    def resetWindow_import_data(self):
        self.dlg.lineEdit_13.clear()
        self.dlg.lineEdit_FM_project_path.clear()
        self.dlg.tableWidget1.setRowCount(0)
        self.dlg.tableWidget2.setRowCount(0)
        self.dlg.tableWidget3.setRowCount(0)
        self.dlg.comboBox_merge1_2.setCurrentIndex(-1)
        self.dlg.comboBox_merge2_2.setCurrentIndex(-1)

    def resetWindow_fieldwork_preparation(self):
        self.dlg.lineEdit_8.clear()
        self.dlg.lineEdit_3.clear()
        self.dlg.lineEdit_39.clear()
        self.dlg.lineEdit_38.clear()
        self.dlg.lineEdit_18.clear()
        self.dlg.lineEdit_9.clear()
        self.dlg.lineEdit_10.clear()
        self.dlg.lineEdit_39.clear()
        self.dlg.comboBox_layers_user.setCurrentIndex(-1)
        self.dlg.comboBox.setCurrentIndex(-1)
        self.dlg.comboBox_delete.setCurrentIndex(-1)

    def resetWindow_data_management(self):
        self.dlg.lineEdit_11.clear()
        self.dlg.lineEdit_15.clear()
        self.dlg.lineEdit_14.clear()
        self.dlg.lineEdit_26.clear()
        self.dlg.lineEdit_37.clear()
        self.dlg.lineEdit_7.clear()
        self.dlg.lineEdit_53.clear()

    ###############################################################################
    #########      Connecting Pushbuttons to LineEdits content        #############
    ###############################################################################

    def select_dst_directory(self):
        filename = QFileDialog.getExistingDirectory(None, "Select Folder")

        self.dlg.lineEdit_3.setText(filename)

    def select_directory(self, widget, prompt):
        filename = QFileDialog.getExistingDirectory(None, prompt)

        widget.setText(filename)

    def select_main_project(self):
        filename, _filter = QFileDialog.getOpenFileName(
            None, "Select Main Project File"
        )

        self.dlg.lineEdit_11.setText(filename)

    def select_sub_project(self):
        filename, _filter = QFileDialog.getOpenFileName(None, "Select Sub-Project File")

        self.dlg.lineEdit_26.setText(filename)

    def select_merged_directory(self):
        filename = QFileDialog.getExistingDirectory(
            None, "Select Destination Merged Folder"
        )

        self.dlg.lineEdit_37.setText(filename)

    def import_FM_Project(self):
        projectDirectoryPath = QFileDialog.getExistingDirectory(
            None, "Select FM Project Folder"
        )

        if os.path.exists(projectDirectoryPath + "/main.db"):
            self.dlg.lineEdit_FM_project_path.setText(projectDirectoryPath)
            self.FM_Import.import_FM_data(self.basePath, projectDirectoryPath)

            self.iface.messageBar().pushMessage(
                "FieldMove project imported",
                level=Qgis.Success,
                duration=45,
            )
        else:
            if projectDirectoryPath != "":
                self.iface.messageBar().pushMessage(
                    "FieldMove project path incorrect",
                    level=Qgis.Critical,
                    duration=45,
                )

    def select_file_to_import(self):
        #unit tester for all functions if path is set manually to "test"
        if(self.dlg.lineEdit_13.text() == "test"):
            self.GEOL_QMAPS_tester()
            return

        filename, _filter = QFileDialog.getOpenFileName(None, "Select Import layer")

        self.dlg.lineEdit_13.setText(filename)
        if filename:
            self.click_import_data()

    def select_export_directory(self):
        filename = QFileDialog.getExistingDirectory(None, "Select Export Folder")

        self.dlg.lineEdit_7.setText(filename)

    def select_file_source_path_photo(self):  # ADD
        filename = QFileDialog.getExistingDirectory(
            None, "Select source path of your field pictures"
        )

        self.dlg.lineEdit_14.setText(filename)

    def select_file_export_template_style(self):  # ADD
        filename = QFileDialog.getExistingDirectory(
            None, "Select export path of your new project style template"
        )

        self.dlg.lineEdit_18.setText(filename)

    def select_clip_poly(self):
        filename, _filter = QFileDialog.getOpenFileName(None, "Select Clip Polygon")
        self.dlg.lineEdit_8.setText(filename)

    def unload(self):
        """Removes the plugin menu item and icon from QGIS GUI."""
        for action in self.actions:
            self.iface.removePluginMenu(self.tr("&GEOL_QMAPS"), action)
            self.iface.removeToolBarIcon(action)

    def update_combobox_delete(self):

        self.dlg.comboBox_delete.clear()

        list_combobox_delete = self.get_first_column_text(
            self.dictionaries_path, self.dlg.comboBox.currentText()
        )

        list_combobox_delete = [str(item) for item in list_combobox_delete]

        self.dlg.comboBox_delete.addItems(list_combobox_delete)

    def sorted_csv_combobBox(self):
        self.dlg.comboBox.clear()
        self.csv_layer_list = []
        for name in self.csvs:
            # Exclude the two General dictionaries from the comboBox selection
            if name not in ["General__List of all lithologies",
                            "General__Rock Type (Supergene, Sedimentary, Volcanoclastic,...)-Lithologies Table"]:
                self.csv_layer_list.append(name)
        self.csv_layer_list.sort()
        self.dlg.comboBox.addItems(self.csv_layer_list)

    def fill_ComboBox(self):
        # clear old entries
        self.dlg.comboBox_merge1_2.clear()
        self.dlg.comboBox_merge2_2.clear()

        # populate both dropdowns
        for layer in QgsProject.instance().mapLayers().values():
            if not isinstance(layer, QgsVectorLayer):
                continue
            uri = layer.dataProvider().dataSourceUri().lower()
            if uri.endswith(".csv") or layer.name() == "African borders_PG":
                continue

            name = layer.name()
            if "_PT" in name and name not in self.layers_names_all:
                if name.startswith("Compilation_"):
                    self.dlg.comboBox_merge2_2.addItem(name, layer.id())
                else:
                    self.dlg.comboBox_merge1_2.addItem(name, layer.id())

        # reconnect so we only ever listen to the *source* combobox
        try:
            self.dlg.comboBox_merge1_2.currentIndexChanged.disconnect(self.on_merge2_source_changed)
        except (TypeError, RuntimeError):
            pass
        self.dlg.comboBox_merge1_2.currentIndexChanged.connect(self.on_merge2_source_changed)

    def fill_ComboBox_layers_user(self):  # ADD
        self.dlg.comboBox_layers_user.clear()

        # List of the QGIS layers
        layers = QgsProject.instance().mapLayers()

        for layerId, layer in layers.items():

            # Select all non CSV layers of the QGIS project
            if isinstance(layer, QgsVectorLayer) and layer.name() in self.layers_names_all:
                self.dlg.comboBox_layers_user.addItem(layer.name(), layerId)

    def update_ComboBox(self):
        self.fill_ComboBox()
        self.fill_ComboBox_layers_user()  # ADD

    def on_merge2_source_changed(self, index):
        """
        When you pick "<LayerName>_PT_<ImportBase>" (or "_PG_<ImportBase>") in comboBox_merge1_2,
        automatically populate comboBox_merge2_2 with "Compilation_<LayerName>_PT" (or "_PG").
        """
        src = self.dlg.comboBox_merge1_2.currentText()
        print(f"source file name is: {src}")
        # look for the part ending in _PT
        m = re.match(r"(.+_(?:PT))_", src)
        print(f"matching part of the source filename is: {m}")
        if not m:
            # nothing to do if it doesn’t match
            self.dlg.comboBox_merge2_2.clear()
            self.dlg.comboBox_merge2_2.addItem("(no matching compilation layer)")
            return

        layer_name = m.group(1)  # e.g. "Shear zones and faults_PT"
        print(f"layer name should be: {layer_name}")
        target = f"Compilation_{layer_name}"  # e.g. "Compilation_Shear zones and faults_PT"
        print(f"target layer should be: {target}")

        # rebuild comboBox_merge2_2 to contain only that entry
        self.dlg.comboBox_merge2_2.blockSignals(True)
        self.dlg.comboBox_merge2_2.clear()
        if QgsProject.instance().mapLayersByName(target):
            print(f"target layer found in the project: {target}")
            self.dlg.comboBox_merge2_2.addItem(target)
            self.dlg.comboBox_merge2_2.setCurrentIndex(0)
        else:
            print(f"target layer not found in the project: {target}")
            self.dlg.comboBox_merge2_2.addItem("(no matching compilation layer)")
        self.dlg.comboBox_merge2_2.blockSignals(False)

    ###############################################################################
    ########                 Tooltips (just for information)           ############
    ###############################################################################

    def define_tips(self):
        #Reset the Window
        ResetWindow_tooltip = "Reset this window..."
        self.dlg.pushButton_13.setToolTip(ResetWindow_tooltip)
        self.dlg.pushButton_19.setToolTip(ResetWindow_tooltip)
        self.dlg.pushButton_22.setToolTip(ResetWindow_tooltip)

        #Browse
        Browse_tooltip = "Browse..."
        self.dlg.pushButton_7.setToolTip(Browse_tooltip)
        self.dlg.pushButton_FM_project_select.setToolTip(Browse_tooltip)
        self.dlg.pushButton_17.setToolTip(Browse_tooltip)
        self.dlg.pushButton.setToolTip(Browse_tooltip)
        self.dlg.pushButton_6.setToolTip(Browse_tooltip)
        self.dlg.pushButton_37.setToolTip(Browse_tooltip)
        self.dlg.pushButton_QFieldPackage.setToolTip(Browse_tooltip)
        self.dlg.pushButton_QGISFolder.setToolTip(Browse_tooltip)
        self.dlg.pushButton_29.setToolTip(Browse_tooltip)
        self.dlg.pushButton_20.setToolTip(Browse_tooltip)
        self.dlg.pushButton_27.setToolTip(Browse_tooltip)
        self.dlg.pushButton_5.setToolTip(Browse_tooltip)
        self.dlg.pushButton_15.setToolTip(Browse_tooltip)

        #IMPORT FIELD DATA
        # Import Legacy SHP
        ImportShp_tooltip = "<p>Reformat existing lithological and structural point .shp databases according to the architecture of the GEOL-QMAPS mapping project template.<p>"
        self.dlg.label_19.setToolTip(ImportShp_tooltip)

        ImportShpStep1_tooltip = "<p>Select the .shp file containing legacy field data (CRS = EPSG4326: WGS 84).<p>"
        self.dlg.groupBox.setToolTip(ImportShpStep1_tooltip)

        ImportShpStep2_tooltip = "<p>Review best-match assigned standard values field names, and then lithologies and/or structures, if appropriate.<p>"
        self.dlg.label_35.setToolTip(ImportShpStep2_tooltip)
        ImportShpStep2Fields_tooltip = "<p>Validate standard field names assigned to legacy database fields.<p>"
        self.dlg.pushButton_9.setToolTip(ImportShpStep2Fields_tooltip)
        ImportShpStep2Litho_tooltip = "<p>Validate standard lithology names assigned to legacy lithologies.<p>"
        self.dlg.pushButton_10.setToolTip(ImportShpStep2Litho_tooltip)
        ImportShpStep2Struct_tooltip = "<p>Validate standard structure types assigned to legacy structures.<p>"
        self.dlg.pushButton_26.setToolTip(ImportShpStep2Struct_tooltip)
        ImportShpStep2Undo_tooltip = "<p>Undo the last edit.<p>"
        self.dlg.pushButton_11.setToolTip(ImportShpStep2Undo_tooltip)
        self.dlg.pushButton_12.setToolTip(ImportShpStep2Undo_tooltip)
        self.dlg.pushButton_25.setToolTip(ImportShpStep2Undo_tooltip)

        ImportShpStep3_tooltip = "<p>Generate scratch layers containing standardised lithological and/or structural legacy datapoints. Do not forget to merge them to field data compilation layers (Step 4)!<p>"
        self.dlg.groupBox_3.setToolTip(ImportShpStep3_tooltip)

        ImportShpStep4_tooltip = "<p>Merge scratch layers containing standardised lithological and/or structural legacy datapoints to field data compilation layers.<p>"
        self.dlg.groupBox_15.setToolTip(ImportShpStep4_tooltip)

        #Import FieldMove Project
        ImportFM_tooltip = "<p>Convert existing FieldMove project into GEOL-QMAPS compatible files: photos are imported to the project, maps are loaded as temporary files and the lithologies added to Local Lithologies_PT layer.<p>"
        self.dlg.groupBox_19.setToolTip(ImportFM_tooltip)


        #FIELDWORK PREPARATION
        # Update Project ID and Location
        Update_tooltip = "<p>Enter the mapping project name and the targeted field region for the new field campaign, set as default metadata for future field data entries.<p>"
        self.dlg.groupBox_6.setToolTip(Update_tooltip)
        Proj_name_tooltip = "<p>Type the updated name of the project, e.g. Mission ID/Year.</p>"
        self.dlg.lineEdit_9.setToolTip(Proj_name_tooltip)
        Proj_region_tooltip = "<p>Type the name of the region the project applies to e.g. Sefwi Belt.</p>"
        self.dlg.lineEdit_10.setToolTip(Proj_region_tooltip)
        self.dlg.projName_pushButton.setToolTip("<p>Update the GEOL-QMAPS project name as Project ID/Location and related metadata for future field data entry.<p>")

        # Set User by Default
        DefaultUser_tooltip = "<p>Assign an existing or new user to be the default for one layer or all field data layers going forward.<p>"
        self.dlg.groupBox_18.setToolTip(DefaultUser_tooltip)
        DefaultUserOneLayer_tooltip = "<p>If toggled on, select the field data layer to update with a new default value for the User field.<p>"
        self.dlg.radioButton_Some.setToolTip(DefaultUserOneLayer_tooltip)
        DefaultUserOneLayerSelect_tooltip = "<p>Select in the dropdown list the field data layer to update with a new default value for the User field.<p>"
        self.dlg.comboBox_layers_user.setToolTip(DefaultUserOneLayerSelect_tooltip)
        DefaultUserAllLayers_tooltip = "<p>If toggled on, all field data layers will be updated with a new default value for the User field.<p>"
        self.dlg.radioButton_All.setToolTip(DefaultUserAllLayers_tooltip)
        self.dlg.pushButton_user_default.setToolTip("<p>Update the User metadata value for one or all field data layers.<p>")

        #Edit Dictionaries
        EditDico_tooltip = "<p>Select which dictionary to add or delete item to, and type the item to add or select the one to delete. New items become available in the GEOL-QMAPS field data dropdown menus.<p>"
        self.dlg.groupBox_5.setToolTip(EditDico_tooltip)
        Dico_tooltip = "<p>Select which dictionary to add or delete item to from the dropdown list.</p>"
        self.dlg.comboBox.setToolTip(Dico_tooltip)
        AddValue_tooltip = "<p>Type a new value to add to the selected dictionary. Update the layer's symbology accordingly to reflect the new items if depends on the modified dictionary.</p>"
        self.dlg.lineEdit_38.setToolTip(AddValue_tooltip)
        self.dlg.csv_pushButton.setToolTip("<p>Add new item to the selected dictionary. Update the layer's symbology accordingly to reflect the new items if depends on the modified dictionary.<p>")
        DeleteValue_tooltip = "<p>Select what item to delete from the selected dictionary.</p>"
        self.dlg.comboBox_delete.setToolTip(DeleteValue_tooltip)
        self.dlg.csv_pushButton_2.setToolTip("<p>Delete the selected item from the related dictionary. Update the layer's symbology accordingly to remove those referring to deleted items.<p>")

        #Define Default Structural Measurement for Planar Structures
        StructConv_tooltip = "<p>Toggle the preferred measurement convention for planar structures to establish it as the default for all layers where planar structural measurements are recorded.<p>"
        self.dlg.groupBox_20.setToolTip(StructConv_tooltip)
        DipDir_tooltip = "<p>If toggled on, planar measurements should be then entered as dip/dip direction by default.<p>"
        self.dlg.structure_style_on_pushButton.setToolTip(DipDir_tooltip)
        RHR_tooltip = "<p>If toggled on, planar measurements should be then entered as strike (right-hand rule)/dip by default.<p>"
        self.dlg.structure_style_off_pushButton.setToolTip(RHR_tooltip)

        #Save Changes Made to the Field Data Forms
        SaveQLR_tooltip = "<p>Enables to save a new .qlr QGIS layer definition file in a directory to be supplied. This file includes customised styles for empty field data layers and updated dictionaries. It guarantees to keep consistent mapping standards for different projects, which facilitates post-field data compilation and processing.<p>"
        self.dlg.groupBox_17.setToolTip(SaveQLR_tooltip)
        SaveButton_tooltip = "<p>Save the updated GEOL-QMAPS layer definition file to the specified folder.<p>"
        self.dlg.pushButton_save__project_template_style_2.setToolTip(SaveButton_tooltip)

        #Clip Field Data to Current Canvas
        ClipTool_tooltip = "<p>Clip GEOL-QMAPS-standardised field data layers to current QGIS canvas, or select a polygon shapefile to be the clipping polygon. Define a new directory to export the QGIS project containing the clipped legacy field data.<p>"
        self.dlg.groupBox_4.setToolTip(ClipTool_tooltip)
        Clip_path_tooltip = "<p>Path to new clipped GEOL-QMAPS project directory.</p>"
        self.dlg.lineEdit_3.setToolTip(Clip_path_tooltip)
        ClipPolygon_tooltip = "<p>Path to clipping polygon shapefile. Leave blank if you want to use the current QGIS Canvas rectangle.</p>"
        self.dlg.lineEdit_8.setToolTip(ClipPolygon_tooltip)
        ClippingButton_tooltip = "<p>Clip GEOL-QMAPS-standardised field data layers.<p>"
        self.dlg.clip_pushButton.setToolTip(ClippingButton_tooltip)


        #DATA MANAGEMENT
        #Rejig to the Latest GEOL-QMAPS Version
        RejigTool_tooltip = "<p>Select an existing GEOL-QMAPS project folder (version≥3.1.0) and convert it to be compatible with the latest release of the GEOL-QMAPS QGIS project template, available via the Zenodo repository.<p>"
        self.dlg.groupBox_22.setToolTip(RejigTool_tooltip)
        RejigValidate_tooltip = "<p>Create an updated project folder named OldQGISProjectFolderName_updatedversion at the same directory level as the original project folder.<p>"
        self.dlg.rejig_pushButton_4.setToolTip(RejigValidate_tooltip)

        #Synchronise a QField Package to the QGIS Master Project
        SyncTool_tooltip = "<p>Overwrite current field data layers updated in QField in the GEOL-QMAPS QGIS master project.<p>"
        self.dlg.groupBox_23.setToolTip(SyncTool_tooltip)
        SyncValidate_tooltip = "<p>Synchronise field data from the updated QField package folder to the GEOL-QMAPS QGIS master database.<p>"
        self.dlg.pushButton_SyncQFieldToQGIS.setToolTip(SyncValidate_tooltip)

        #Merge Projects
        MergeTool_tooltip = "<p>Merge two existing GEOL-QMAPS projects by selecting two existing project files, and a new repository to store newly merged projects, with duplicate layers removed.<p>"
        self.dlg.groupBox_8.setToolTip(MergeTool_tooltip)
        Merge_main_tooltip = "<p>Path to directory of the GEOL-QMAPS QGIS project from which the layer tree architecture will be copied.</p>"
        self.dlg.lineEdit_11.setToolTip(Merge_main_tooltip)
        Merge_sub_tooltip = "<p>Path to directory of the GEOL-QMAPS QGIS project to be merged to the latter. Layers specific to the second project are copied to the merged project folder but are not loaded in the QGIS project.</p>"
        self.dlg.lineEdit_26.setToolTip(Merge_sub_tooltip)
        Merge_output_tooltip = "<p>Path to directory of newly merged GEOL-QMAPS QGIS project.</p>"
        self.dlg.lineEdit_37.setToolTip(Merge_output_tooltip)
        MergeValidate_tooltip = "<p>Merge selected GEOL-QMAPS QGIS projects.<p>"
        self.dlg.merge_pushButton.setToolTip(MergeValidate_tooltip)

        #Archive Current Field Data
        ArchiveTool_tooltip = "<p>Transfer of all field data from the CURRENT_MISSION.gpkg geopackage (stored under the CURRENT MISSION group in the GEOL-QMAPS QGIS project) to the COMPILATION.gpkg geopackage, loaded in the EXISTING FIELD GEODATABASE sub-group in the GEOL-QMAPS QGIS project.<p>"
        self.dlg.groupBox_21.setToolTip(ArchiveTool_tooltip)
        self.dlg.merge_current_existing_pushButton_3.setToolTip(ArchiveTool_tooltip)

        #Remove Duplicate UUIDs from Project
        RemoveDuplicateTool_tooltip = "<p>After using the Merge Projects or Archive Current Field Data tools, ensure the integrity of generated field layers by identifying and deleting any duplicate entities with identical UUIDs.<p>"
        self.dlg.groupBox_9.setToolTip(RemoveDuplicateTool_tooltip)
        self.dlg.merge_pushButton_2.setToolTip("<p>Remove duplicates in current and compilation field data layers.<p>")

        #Export Compilation Layers to Common Themes
        ExportTool_tooltip = "<p>Specify a directory for exporting all point, polygon, and polyline entities of the compilation data layers. These entities will be grouped and combined into different thematic shapefile layers in a geopackage (polygon and line layers are merged by geometry, point layers are divided between lithologies, structures and stops-sampling-photographs-observations).<p>"
        self.dlg.groupBox_10.setToolTip(ExportTool_tooltip)
        ExportPath_tooltip = "<p>Provide an output path to combine similar layers into thematic shapefiles as part of a geopackage.<p>"
        self.dlg.lineEdit_7.setToolTip(ExportPath_tooltip)
        ExportValidate_tooltip = "<p>Export compilation layers to common themes.<p>"
        self.dlg.pushButton_5.setToolTip(ExportValidate_tooltip)
        self.dlg.export_pushButton.setToolTip("Export layers")

        #Create Virtual Stops
        VirtualStops_tooltip = "<p>Define clustering distance to add a cluster code for Virtual Stop ID to all different types of points observations according to locality, using a DBSCAN algorithm. This will create a new layer called Virtual_Stops_datestamp.<p>"
        self.dlg.groupBox_11.setToolTip(VirtualStops_tooltip)
        Epsilon_tooltip = "<p> Set the radius of the circle to be created around each data point to check the data density (in metres) for further clustering.<p>"
        self.dlg.lineEdit_53.setToolTip(Epsilon_tooltip)
        self.dlg.virtual_pushButton.setToolTip("<p>Create virtual stops.<p>")

        # Stereographic Projection
        self.dlg.stereonet_pushButton.setToolTip("<p>Control fork of custom Stereonet plugin display.<p>")
        gtCircles_tooltip = "<p>Select Checkbox to switch to Great Circle Display for Stereonet Plugin<p>"
        contours_tooltip = "<p>Select Checkbox to add Contour Display for Stereonet Plugin<p>"
        kinematics_tooltip = "<p>Select Checkbox to add kinematics for Lineation Display for Stereonet Plugin<p>"
        linPlanes_tooltip = "<p>Select Checkbox to add Associated Great Circles to Lineation Display for Stereonet Plugin<p>"
        rose_tooltip = "<p>Select Checkbox to display rose diagram instead of stereoplot in Stereonet Plugin<p>"
        stereonet_tooltip = "<p>Select Checkbox to control Display behaviour for Stereonet Plugin<p>"
        self.dlg.gtCircles_checkBox.setToolTip(gtCircles_tooltip)
        self.dlg.contours_checkBox.setToolTip(contours_tooltip)
        self.dlg.kinematics_checkBox.setToolTip(kinematics_tooltip)
        self.dlg.linPlanes_checkBox.setToolTip(linPlanes_tooltip)
        self.dlg.rose_checkBox.setToolTip(rose_tooltip)

        #Picture Management
        PictureManagement_tooltip = "<p>Allows a new directory to be defined for the storage of field and sampling pictures (to enable the display of Map Tips miniatures for field and sample photographs in QGIS), and retrieve EXIF metadata for image orientation if available.<p>"
        self.dlg.groupBox_16.setToolTip(PictureManagement_tooltip)
        PictureFolder_tooltip = "<p>Browse to the folder that contains field and sample photographs.<p>"
        self.dlg.lineEdit_14.setToolTip(PictureFolder_tooltip)
        FilepathExisting_tooltip = "<p>If toogled on, the filepath of existing photographs will be updated to the selected folder.<p>"
        self.dlg.option1_ckeckbox.setToolTip(FilepathExisting_tooltip)
        FilepathDefault_tooltip = "<p>If toogled on, the default value for filepath of future photographs loaded in the GEOL-QMAPS project will be updated to the selected folder.<p>"
        self.dlg.option2_ckeckbox.setToolTip(FilepathDefault_tooltip)
        FilepathValidate_tooltip = "<p>Update photograph repository information.<p>"
        self.dlg.option2_ckeckbox.setToolTip(FilepathValidate_tooltip)
        EXIF_tooltip = "<p>Once picture filepaths have been updated if required, retrieve image direction from EXIF metadata.<p>"
        self.dlg.pushButton_use_exif_azimuth.setToolTip(EXIF_tooltip)


    ###############################################################################
    ####################                   RUN                   ##################
    ###############################################################################
    def extract_exif_azimuth(self, full_path):
        """
        Returns the GPSImgDirection in degrees as a float, or None if missing/broken.
        """
        try:
            img = Image.open(full_path)
            info = img._getexif()
            print(f"")
            if not info:
                return None

            # find the GPSInfo block
            gps_info = info.get(34853)  # 34853 is the tag id for GPSInfo
            if not gps_info:
                return None

            # decode GPS sub-tags
            decoded = {}
            for tag, val in gps_info.items():
                name = GPSTAGS.get(tag, tag)
                decoded[name] = val

            direction = decoded.get('GPSImgDirection')
            if direction is None:
                return None

            # direction may be an (num,den) tuple
            if isinstance(direction, tuple) and len(direction) >= 2:
                num, den = direction[0], direction[1]
                return float(num) / float(den) if den else None

            return float(direction)
        except Exception:
            return None


    def use_exif_azimuth(self):
        from qgis.core import (
            QgsExpression,
            QgsExpressionContext,
            QgsExpressionContextUtils,
            Qgis
        )
        # Names of the two photo layers to process
        layer_names = ["Photographs_PT", "Compilation_Photographs_PT"]
        for layer_name in layer_names:
            layers = QgsProject.instance().mapLayersByName(layer_name)
            if not layers:
                self.iface.messageBar().pushMessage(
                    f"Layer '{layer_name}' not found or invalid.",
                    level=Qgis.Warning,
                    duration=15
                )
                continue
            layer = layers[0]

            # Start editing
            if not layer.isEditable():
                layer.startEditing()

            # Prepare the EXIF expression once
            exif_expr = QgsExpression(
                "exif(\"Full_Path\", 'Exif.GPSInfo.GPSImgDirection')"
            )

            # Field indices
            idx_full = layer.fields().indexFromName("Full_Path")
            idx_exif = layer.fields().indexFromName("EXIF_Azimuth")
            idx_az = layer.fields().indexFromName("Azimut")
            if idx_full < 0 or idx_exif < 0 or idx_az < 0:
                self.iface.messageBar().pushMessage(
                    f"Layer '{name}' missing one of Full_Path, EXIF_Azimuth or Azimut attributes.",
                    level=Qgis.Critical, duration=10
                )
                layer.rollback()
                continue

            # Loop features
            for feat in layer.getFeatures():
                fid = feat.id()
                full_path = feat["Full_Path"]
                exif_val = feat["EXIF_Azimuth"]
                az_val = feat["Azimut"]
                new_exif = None

                # If EXIF_Azimuth is null/empty, try to compute it
                if exif_val in [None, "", 0]:
                    new_exif = self.extract_exif_azimuth(full_path)
                    if new_exif is None:
                        self.iface.messageBar().pushMessage(
                            "Warning",
                            f"Feature {fid}: cannot read EXIF from '{full_path}'; Entered image direction (0, by default) retained.",
                            level=Qgis.Warning, duration=5
                        )
                        continue
                    exif_val = new_exif
                    layer.changeAttributeValue(fid, idx_exif, exif_val)

                # At this point exif_val is valid: compute declination
                # Extract date from "Date" field
                date = feat["Date"]
                date = date.split("/")

                day = int(date[2].split(" ")[0])
                month = int(date[1])
                year = int(date[0])
                date = datetime(year, month, day)

                # Extract latitude and longitude from the feature's geometry
                geometry = feat.geometry()
                if geometry.isEmpty():
                    print(f"Feature {fid} in {layer_name}: has no geometry.")
                    continue

                # Get the point geometry as lat/lon (assuming the layer has point geometry)
                if geometry.type() == QgsWkbTypes.PointGeometry:
                    lat, lon = geometry.asPoint().y(), geometry.asPoint().x()
                else:
                    print(f"Feature ID {feature_id} does not have point geometry.")
                    continue

                # calculate IGRF compnents and  convert to Inc, Dec, Int
                Be, Bn, Bu = igrf(lon, lat, 0, date)
                inc, dec = get_inclination_declination(Be, Bn, Bu, degrees=True)
                corrected = exif_val + dec.item()
    
                # Final corrected azimuth
                if new_exif is not None or exif_val is not None:
                    layer.changeAttributeValue(fid, idx_az, corrected)

            # Commit all changes on this layer
            if not layer.commitChanges():
                self.iface.messageBar().pushMessage(
                    f"Failed to commit Image Direction updates on '{layer_name}'.",
                    level=Qgis.Critical,
                    duration=10
                )
            else:
                self.iface.messageBar().pushMessage(
                    f"Layer '{layer_name}': Image directions updated from EXIF and corrected from declination.",
                    level=Qgis.Success,
                    duration=5
                )



################################################################################
###                              TESTING code                            #######
################################################################################


    def GEOL_QMAPS_tester(self):
        print("\n\nGEOL_QMAPS_tester started")
        # path reference to loaded project
        project = QgsProject.instance()
        proj_file_path = project.fileName()
        head_tail = os.path.split(proj_file_path)
        self.basePath = head_tail[0] + "/"
        grandparent_directory = os.path.dirname(self.basePath)
        greatgrandparent_directory = os.path.dirname(grandparent_directory)
        print(greatgrandparent_directory)  

        FMDirectory=greatgrandparent_directory+"/FieldMove/"
        Project2=greatgrandparent_directory+"/Project2_initial/Project2.qgz"
        
        mergeDirectory=greatgrandparent_directory+"/test_merge/"
        clipDirectory=greatgrandparent_directory+"/test_clip/"
        exportDirectory=greatgrandparent_directory+"/test_export/"
        qlmDirectory=greatgrandparent_directory+"/test_qlm/"
        phtotDirectory=greatgrandparent_directory+"/test_photo/"

        newDirs=[mergeDirectory,clipDirectory,exportDirectory,qlmDirectory,phtotDirectory]
        for directory in newDirs:
            # Check if directory exists
            if os.path.exists(directory) and os.path.isdir(directory):
                # Remove directory and all its contents
                try:
                    shutil.rmtree(directory)
                except:
                    print(f"Directory '{directory}' couldn't be removed.")
            os.makedirs(directory,exist_ok=True) 


        #run through all funcitonality (except importing shp legacy data)

        try:
            self.dlg.lineEdit_9.setText("Name")
            self.dlg.lineEdit_10.setText("Place")
            self.updateProjectTitle()
            print("updateProjectTitle")
        except:
            print("*** updateProjectTitle failed")

        try:
            self.merge_current_to_existing()
            print("merge_current_to_existing")
        except:
            print("*** merge_current_to_existing failed")

        try:
            self.FM_Import = FM_Import(None)
            self.FM_Import.import_FM_data(self.basePath, FMDirectory)
            print("FM_Import.import_FM_data")
        except:
            print("*** FM_Import.import_FM_data failed")

        try:
            self.dlg.lineEdit_11.setText(proj_file_path)
            self.dlg.lineEdit_26.setText(Project2)
            self.dlg.lineEdit_37.setText(mergeDirectory)
            self.mergeProjects()
            print("mergeProjects")
        except:
            print("*** mergeProjects failed")

        try:
            self.dlg.lineEdit_7.setText(exportDirectory)
            self.exportData()
            print("exportData")
        except:
            print("*** exportData failed")

        try:
            self.dlg.lineEdit_3.setText(clipDirectory)
            self.clipToCanvas()
            print("clipToCanvas")
        except:
            print("*** clipToCanvas failed")

        try:
            self.removeDuplicates()
            print("removeDuplicates")
        except:
            print("*** removeDuplicates failed")

        try:
            self.dlg.lineEdit_18.setText(qlmDirectory)
            self.save_template_style()
            print("save_template_style")
        except:
            print("*** save_template_style failed")

        try:
            self.dlg.lineEdit_53.setText("500")
            self.virtualStops()
            print("virtualStops")
        except:
            print("*** virtualStops failed")

        try:
            self.dlg.lineEdit_39.setText("UnitTestUser")
            self.set_user_by_default()
            print("set_user_by_default")
        except:
            print("*** set_user_by_default failed")

        try:
            self.use_exif_azimuth()
            print("use_exif_azimuth")
        except:
            print("*** use_exif_azimuth failed")

        try:
            self.dlg.lineEdit_14.setText(phtotDirectory)
            self.update_source_photo()
            print("update_source_photo")
        except:
            print("*** update_source_photo failed")

        try:
            self.dlg.rose_checkBox.setChecked(True)
            self.set_stereoConfig()
            print("set_stereoConfig")
        except:
            print("*** set_stereoConfig failed")

        try:
            current_layer = "General__List of Users"
            self.dlg.comboBox.setCurrentText(current_layer)
            self.dlg.lineEdit_38.setText("TestUserAdd1")
            self.addCsvItem()
            self.dlg.lineEdit_38.setText("TestUserAdd2")
            self.addCsvItem()
            self.dlg.comboBox_delete.setCurrentText("TestUserAdd2")
            self.deleteCsvItem()
            print("addCsvItem and deleteCsvItem")
        except:
            print("*** addCsvItem and deleteCsvItem failed")

        try:
            # Test “on” style
            self.dlg.structure_style_on_pushButton.click()
            # either rely on clicked.connect or call explicitly
            self.set_orientation_style()
            layer = QgsProject.instance().mapLayersByName("Veins_PT")[0]
            idx = layer.fields().indexFromName("Measure")
            expr = layer.defaultValueDefinition(idx).expression()
            if expr == "'Dip - dip direction'":
                print("structure_style_on: PASS")
            else:
                print(f"*** structure_style_on: FAIL (got {expr})")
        except Exception as e:
            print("*** structure_style_on: ERROR", e)

        try:
            # Test “off” style
            self.dlg.structure_style_off_pushButton.click()
            self.set_orientation_style()
            layer = QgsProject.instance().mapLayersByName("Veins_PT")[0]
            idx = layer.fields().indexFromName("Measure")
            expr = layer.defaultValueDefinition(idx).expression()
            if expr == "'Strike (right-hand rule) - dip'":
                print("structure_style_off: PASS")
            else:
                print(f"*** structure_style_off: FAIL (got {expr})")
        except Exception as e:
            print("*** structure_style_off: ERROR", e)

        try:
            current_proj_folder = head_tail[0]
            self.dlg.lineEdit_15.setText(current_proj_folder)
            self.rejig_project()
            print("rejig_project")
        except:
            print("*** rejig_project failed")

        print("GEOL_QMAPS_tester finished")
