#! python3  # noqa: E265

# standard
import os

# extensions
import site
from functools import partial
from pathlib import Path

# PyQGIS
from qgis.core import QgsApplication, QgsExpression, QgsProject, QgsSettings
from qgis.gui import QgisInterface
from qgis.PyQt.Qt import QUrl
from qgis.PyQt.QtCore import QCoreApplication, Qt, QThread, QTimer, pyqtSignal, pyqtSlot
from qgis.PyQt.QtGui import QDesktopServices, QGuiApplication, QIcon
from qgis.PyQt.QtWidgets import QAction, QDockWidget, QFileDialog, QMenu, QMessageBox

# project
from openlog.__about__ import DIR_PLUGIN_ROOT, __title__, __uri_documentation__
from openlog.gui.utils.check_versions import is_openlog_up_to_date
from openlog.gui.welcome.welcome import WelcomeScreen
from openlog.processing.provider import OpenLogProvider
from openlog.resources.styles.length3d_qgis_style import (
    get_3d_distance_from_n_vertices,
    get_geometry_with_m_dimension,
    get_projected_length_of_3D_line,
)
from openlog.toolbelt import PlgLogger
from openlog.toolbelt.check_paths import check_paths
from openlog.toolbelt.sync_inspector_row_worker import SyncInspectorRowWorker

check_paths()
site.addsitedir(DIR_PLUGIN_ROOT / "extensions")
# import faulthandler
# with open("/home/vincent/openlog/faulthandler.txt", "w") as f:
#     faulthandler.enable(file=f)
# Check dependency by loading plugin with external dependency
dependency_valid = True
import_error = ""
try:
    from openlog.core.desurveying import minimum_curvature_method
    from openlog.datamodel.assay.generic_assay import AssayDomainType
    from openlog.datamodel.connection.openlog_connection import OpenLogConnection
    from openlog.datamodel.connection.openlog_connection_factory import (
        OpenLogConnectionFactory,
    )
    from openlog.datamodel.connection.spatialite.spatialite_connection import (
        SpatialiteConnection,
    )
    from openlog.datamodel.connection.xplordb.xplordb_connection import (
        XplordbConnection,
    )
    from openlog.gui.assay_visualization.assay_widget import AssayWidget
    from openlog.gui.collar_creation.collar_creation_dialog import CollarCreationDialog
    from openlog.gui.collar_edition.collar_edition_dialog import CollarEditionDialog
    from openlog.gui.connection.connection_dialog import ConnectionDialog
    from openlog.gui.create_database.database_creation import DatabaseCreationWizard
    from openlog.gui.dlg_settings import PlgOptionsFactory
    from openlog.gui.import_assay.import_assay import AssayImportWizard
    from openlog.gui.import_data.database_import import (
        DatabaseImportWizard,
        ImportPages,
    )
    from openlog.gui.local_grid.local_grid_creation_dialog import (
        LocalGridCreationDialog,
    )
    from openlog.gui.survey_creation.survey_creation_dialog import SurveyCreationDialog
    from openlog.plugins.manager import get_plugin_manager
except ImportError:
    import site

    site.addsitedir(DIR_PLUGIN_ROOT / "embedded_external_libs")
    try:
        from openlog.core.desurveying import minimum_curvature_method
        from openlog.datamodel.assay.generic_assay import AssayDomainType
        from openlog.datamodel.connection.openlog_connection import OpenLogConnection
        from openlog.datamodel.connection.openlog_connection_factory import (
            OpenLogConnectionFactory,
        )
        from openlog.datamodel.connection.spatialite.spatialite_connection import (
            SpatialiteConnection,
        )
        from openlog.datamodel.connection.xplordb.xplordb_connection import (
            XplordbConnection,
        )
        from openlog.gui.assay_visualization.assay_widget import AssayWidget
        from openlog.gui.collar_creation.collar_creation_dialog import (
            CollarCreationDialog,
        )
        from openlog.gui.collar_edition.collar_edition_dialog import CollarEditionDialog
        from openlog.gui.connection.connection_dialog import ConnectionDialog
        from openlog.gui.create_database.database_creation import DatabaseCreationWizard
        from openlog.gui.dlg_settings import PlgOptionsFactory
        from openlog.gui.import_assay.import_assay import AssayImportWizard
        from openlog.gui.import_data.database_import import (
            DatabaseImportWizard,
            ImportPages,
        )
        from openlog.gui.local_grid.local_grid_creation_dialog import (
            LocalGridCreationDialog,
        )
        from openlog.gui.survey_creation.survey_creation_dialog import (
            SurveyCreationDialog,
        )
        from openlog.plugins.manager import get_plugin_manager

    except ImportError as exc:
        import_error = str(exc)
        dependency_valid = False

        OpenLogConnectionFactory = None

        XplordbConnection = None
        OpenLogConnection = None
        AssayDomainType = None

        ConnectionDialog = None

        AssayImportWizard = None
        AssayWidget = None

        SurveyCreationDialog = None
        DatabaseCreationWizard = None
        DatabaseImportWizard = None
        ImportPages = None
        CollarCreationDialog = None
        CollarEditionDialog = None
        LocalGridCreationDialog = None

        minimum_curvature_method = None
        get_plugin_manager = None
        PlgOptionsFactory = None

BASE_SETTINGS_KEY = "/OpenLog/plugin"

PROJECT_CONNECTION_TYPE_KEY = "/project_connection/type"

# store method that will be monkeypatched for sqlalchemy .pyd issues when updating
try:
    from pyplugin_installer.installer import QgsPluginInstaller

    _original_install_method = QgsPluginInstaller.installPlugin
    _original_uninstall_method = QgsPluginInstaller.uninstallPlugin
except ImportError:
    _original_install_method = lambda self, key, quiet, stable: None
    _original_uninstall_method = lambda self, key, quiet: None


def move_sqlalchemy_pyd_files():
    """
    Move sqlalchemy .pyd files in a temporary folder.
    In that way, files in plugin folder will be deleted without issues during upgrade.
    It means sqlalchemy will still use these files until next QGIS restart.
    """
    import tempfile

    import sqlalchemy

    # create folder in temp path
    tmp = tempfile.mkdtemp()
    modules = [
        sqlalchemy.cimmutabledict,
        sqlalchemy.cprocessors,
        sqlalchemy.cresultproxy,
    ]
    for module in modules:
        file = module.__file__
        if os.path.exists(file):
            os.rename(file, tmp + "/" + module.__name__ + ".pyd")


def install_plugin(self, key, quiet=False, stable=True):
    """
    On Windows we need to release lock on sqlalchemy library and unload plugin before
    performing an update. See https://github.com/MerginMaps/qgis-mergin-plugin/issues/504
    and https://github.com/MerginMaps/geodiff/issues/205
    """
    if key == "openlog" and os.name == "nt":
        try:
            move_sqlalchemy_pyd_files()
        except:
            pass

    _original_install_method(self, key, quiet, stable)


def uninstall_plugin(self, key, quiet=False):

    if key == "openlog" and os.name == "nt":
        try:
            move_sqlalchemy_pyd_files()
        except:
            pass

    _original_uninstall_method(self, key, quiet)


class CheckVersionThread(QThread):
    finished = pyqtSignal(bool)

    def __init__(self, parent):
        super().__init__(parent)
        self.result = None

    @pyqtSlot()
    def run(self):
        try:
            self.result = is_openlog_up_to_date()
            self.finished.emit(True)
        except Exception as e:
            pass


class OpenlogPlugin:
    """
    OpenlogPlugin. Can be used to access data from current openlog connection

    """

    def __init__(self, iface: QgisInterface):
        """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
        """
        self.iface = iface
        self.provider = None
        self.menu = None
        self.log = PlgLogger().log
        self.options_factory = None
        self.action_help = None
        self.action_settings = None

        self.database_action = None

        self.import_action_collar = None
        self.import_action_survey = None

        self.import_assay_action = None

        self.xplordb_create_action = None
        self.spatialite_create_action = None

        self.connect_action = None

        self.local_grid_action = None
        self.add_collar_action = None
        self.selected_colar_desurv_action = None
        self.selected_colar_survey_definition_action = None
        self.depth_assay_dock_action = None
        self.time_assay_dock_action = None
        self._plugin_actions = []

        self.depth_assay_visu_dock = None
        self.depth_assay_visu_widget = None
        self.time_assay_visu_dock = None
        self.time_assay_visu_widget = None
        self.manage_assay_enabled = False
        self.ags_file_enabled = False

        # translation
        self.collar_creation_dialog = None
        self.local_grid_creation_dialog = None
        self.openlog_connection = None

        self.context_menu_connect = None
        self.project_loaded_connect = None
        self.project_cleared_connect = None
        self.project_filename_changed_connect = None
        self.project_load_timer = None

        self.splash_active = True

        # collar attribute table
        self.collar_attributes = None
        self.plugin_manager = None

        # check version in another thread
        self.version_checker = CheckVersionThread(None)

        self.inspector_row_timer = QTimer(self.iface.mainWindow())
        self.inspector_row_timer.setSingleShot(True)
        self.inspector_row_timer.timeout.connect(
            lambda: self.sync_inspector_row_thread.start()
        )

        self.manage_profile_wizard = None

        # synchronization between inspector and row selection is done inside a thread
        self.sync_inspector_row_thread = QThread()
        self.sync_inspector_row_worker = SyncInspectorRowWorker(None, self)
        self.sync_inspector_row_worker.finished.connect(self._highlight_assay_row)
        self.sync_inspector_row_worker.moveToThread(self.sync_inspector_row_thread)
        self.sync_inspector_row_thread.started.connect(
            self.sync_inspector_row_worker.run
        )

    def get_openlog_connection(self) -> OpenLogConnection:
        """
        Returns current openlog connection.

        Can be used to access data from connection

        Returns: (OpenLogConnection)

        """
        return self.openlog_connection

    def check_dependencies(self) -> bool:
        """Check if all dependencies are satisfied. If not, warn the user and disable plugin.
        Return True if all dependencies are ok.
        """
        # if import failed
        if not dependency_valid:
            self.log(
                message="Error importing dependencies : {}.\n Plugin disabled.".format(
                    import_error
                ),
                log_level=2,
                push=True,
                button=True,
                duration=0,
            )

            # add tooltip over menu
            msg_disable = "OpenLog Plugin disabled. Please install all dependencies and then restart QGIS."

            self.menu.setEnabled(False)
            self.menu.setToolTip(msg_disable)

            # disable plugin actions
            for widget in self._plugin_actions:
                widget.setEnabled(False)
                widget.setToolTip(msg_disable)
            return False
        else:
            self.log(message="Dependencies satisfied", log_level=3)
            return True

    def initGui(self):
        """Set up plugin UI elements."""

        self.menu = QMenu("OpenLog", self.iface.mainWindow())
        self.iface.mainWindow().menuBar().addMenu(self.menu)

        # No need to go further if the dependencies are not installed
        if not self.check_dependencies():
            return

        # settings page within the QGIS preferences menu
        self.options_factory = PlgOptionsFactory()
        self.iface.registerOptionsWidgetFactory(self.options_factory)

        # -- Actions
        self.action_help = QAction(
            QIcon(":/images/themes/default/mActionHelpContents.svg"),
            "Help",
            self.iface.mainWindow(),
        )
        self.action_help.triggered.connect(
            partial(QDesktopServices.openUrl, QUrl(__uri_documentation__))
        )

        self.action_settings = QAction(
            QgsApplication.getThemeIcon("console/iconSettingsConsole.svg"),
            "Settings",
            self.iface.mainWindow(),
        )
        self.action_settings.triggered.connect(
            lambda: self.iface.showOptionsDialog(
                currentPage="mOptionsPage{}".format(__title__)
            )
        )

        # Database section
        self.database_action = QAction(
            QgsApplication.getThemeIcon("mIconDbSchema.svg"),
            "Database",
        )
        menu = QMenu(self.iface.mainWindow())
        self.database_action.setMenu(menu)

        ## Create section
        self.create_action = QAction(
            QgsApplication.getThemeIcon("mIconDbSchema.svg"),
            "Create",
        )
        menu = QMenu(self.iface.mainWindow())
        self.create_action.setMenu(menu)

        #  CREATE ACTIONS

        self.xplordb_create_action = QAction(
            QgsApplication.getThemeIcon("mActionAddPostgisLayer.svg"),
            "Xplordb database",
            self.iface.mainWindow(),
        )
        self.xplordb_create_action.triggered.connect(self._xplordb_create)
        self.create_action.menu().addAction(self.xplordb_create_action)

        self.spatialite_create_action = QAction(
            QgsApplication.getThemeIcon("mActionAddSpatiaLiteLayer.svg"),
            "Spatialite database",
            self.iface.mainWindow(),
        )
        self.spatialite_create_action.triggered.connect(self._spatialite_create)
        self.create_action.menu().addAction(self.spatialite_create_action)

        self.database_action.menu().addAction(self.create_action)

        # Connect section
        self.connect_action = QAction(
            QgsApplication.getThemeIcon("mIconConnect.svg"),
            "Connect",
        )
        menu = QMenu(self.iface.mainWindow())
        self.connect_action.setMenu(menu)

        #  CONNECT ACTION through plugins
        if get_plugin_manager is not None:
            self.plugin_manager = get_plugin_manager()

        if self.plugin_manager is not None:
            for plugin_connection in self.plugin_manager.get_connections():
                action = plugin_connection.connection_action(self)
                self.connect_action.menu().addAction(action)

        self.database_action.menu().addAction(self.connect_action)

        # Data section
        self.data_action = QAction(
            QgsApplication.getThemeIcon("mIconFieldBinary.svg"),
            "Data",
        )
        menu = QMenu(self.iface.mainWindow())
        self.data_action.setMenu(menu)

        # Import section
        self.import_action = QAction(
            QgsApplication.getThemeIcon("mActionSharingImport.svg"),
            "Import",
        )
        menu = QMenu(self.iface.mainWindow())
        self.import_action.setMenu(menu)
        self.data_action.menu().addAction(self.import_action)

        # import generic data section
        self.import_generic_action = QAction(
            QgsApplication.getThemeIcon("propertyicons/meshframe.svg"),
            "Tabular data",
        )
        menu = QMenu(self.iface.mainWindow())
        self.import_generic_action.setMenu(menu)
        self.import_action.menu().addAction(self.import_generic_action)

        #  IMPORT ACTIONS
        self.import_action_collar = QAction(
            QIcon(str(DIR_PLUGIN_ROOT / "resources" / "images" / "import_collar.svg")),
            "Import collar data",
            self.iface.mainWindow(),
        )
        self.import_action_collar.setEnabled(False)
        self.import_action_collar.triggered.connect(self._import_collar)

        self.import_generic_action.menu().addAction(self.import_action_collar)

        self.import_action_survey = QAction(
            QIcon(str(DIR_PLUGIN_ROOT / "resources" / "images" / "import_survey.svg")),
            "Import survey data",
            self.iface.mainWindow(),
        )
        self.import_action_survey.setEnabled(False)
        self.import_action_survey.triggered.connect(self._import_survey)

        self.import_generic_action.menu().addAction(self.import_action_survey)

        self.import_assay_action = QAction(
            QIcon(str(DIR_PLUGIN_ROOT / "resources" / "images" / "import_assay.svg")),
            "Import downhole data",
            self.iface.mainWindow(),
        )
        self.import_assay_action.setEnabled(False)
        self.import_assay_action.triggered.connect(self._import_assay)

        self.import_generic_action.menu().addAction(self.import_assay_action)

        # AGS files
        self.ags_action = QAction(
            QgsApplication.getThemeIcon("mActionSharingImport.svg"),
            "AGS (Premium only)",
        )
        self.import_action.menu().addAction(self.ags_action)
        self.ags_action.setEnabled(False)
        if self.plugin_manager is not None:
            self.ags_file_enabled = self.plugin_manager.get_ags_reader_plugin().enable
            if self.ags_file_enabled:
                self.ags_action.setText("AGS")

        self.ags_action.triggered.connect(self._import_ags)
        # Manage section
        self.manage_action = QAction(
            QgsApplication.getThemeIcon("propertyicons/settings.svg"),
            "Manage",
        )
        menu = QMenu(self.iface.mainWindow())
        self.manage_action.setMenu(menu)
        self.data_action.menu().addAction(self.manage_action)

        #  Data management ACTION through plugins
        if self.plugin_manager is not None:
            for plugin_data_manager in self.plugin_manager.get_data_managers():
                action = plugin_data_manager.data_management_action(self)
                self.manage_action.menu().addAction(action)

        # Manage assay tables (premium)
        self.manage_assay_action = QAction(
            QIcon(str(DIR_PLUGIN_ROOT / "resources" / "images" / "admin_db.svg")),
            "Downhole data (Premium only)",
            self.iface.mainWindow(),
        )
        self.manage_assay_action.setEnabled(False)
        self.manage_assay_action.triggered.connect(self._manage_assays)
        self.manage_action.menu().addAction(self.manage_assay_action)
        if self.plugin_manager is not None:
            self.manage_assay_enabled = (
                self.plugin_manager.get_manager_assay_plugin().enable
            )
            if self.manage_assay_enabled:
                self.manage_assay_action.setText("Downhole data")

        # manage profile action
        self.profile_manager_action = QAction(
            QIcon(":/images/themes/default/mIconFieldDateTime.svg"),
            "Import profiles (Premium only)",
            self.iface.mainWindow(),
        )
        self.profile_manager_action.setEnabled(False)

        if self.plugin_manager is not None:
            self.advanced_imports_enabled = (
                self.plugin_manager.get_advanced_imports_plugin().enable
            )
            if self.advanced_imports_enabled:
                self.profile_manager_action.setText("Import profiles")

        self.profile_manager_action.triggered.connect(self._manage_profiles)
        self.manage_action.menu().addAction(self.profile_manager_action)

        # add collar action
        self.add_collar_action = QAction(
            QIcon(str(DIR_PLUGIN_ROOT / "resources" / "images" / "add_collar.svg")),
            "Add collar",
            self.iface.mainWindow(),
        )
        self.add_collar_action.triggered.connect(self._add_collar)
        self.add_collar_action.setEnabled(False)
        self.manage_action.menu().addAction(self.add_collar_action)

        # edit survey action
        self.selected_colar_survey_definition_action = QAction(
            QIcon(str(DIR_PLUGIN_ROOT / "resources" / "images" / "define_survey.svg")),
            "Edit surveys",
            self.iface.mainWindow(),
        )
        self.selected_colar_survey_definition_action.triggered.connect(
            self._selected_collar_survey_definition
        )
        self.selected_colar_survey_definition_action.setEnabled(False)
        self.manage_action.menu().addAction(
            self.selected_colar_survey_definition_action
        )
        # collar edition
        self.selected_colar_edition_action = QAction(
            QIcon(str(DIR_PLUGIN_ROOT / "resources" / "images" / "define_collar.svg")),
            "Edit collars",
            self.iface.mainWindow(),
        )
        self.selected_colar_edition_action.triggered.connect(
            self._selected_collar_edition
        )
        self.selected_colar_edition_action.setEnabled(False)
        self.manage_action.menu().addAction(self.selected_colar_edition_action)
        # TOOL ACTION
        self.local_grid_action = QAction(
            QIcon(":/images/themes/default/grid.svg"),
            "Local grid",
            self.iface.mainWindow(),
        )
        self.local_grid_action.triggered.connect(self._local_grid)

        self.selected_colar_desurv_action = QAction(
            QIcon(str(DIR_PLUGIN_ROOT / "resources" / "images" / "desurvey.svg")),
            "Desurvey",
            self.iface.mainWindow(),
        )
        self.selected_colar_desurv_action.triggered.connect(
            lambda: self._selected_collar_desurveying()
        )
        self.selected_colar_desurv_action.setEnabled(False)

        # -- Menu
        self._add_plugin_actions(self.action_settings)
        self._add_plugin_actions(self.action_help)
        self._add_plugin_actions(self.database_action)
        self._add_plugin_actions(self.data_action)
        self._add_plugin_actions(self.local_grid_action)
        self._add_plugin_actions(self.selected_colar_desurv_action)

        # Custom context menu for mapcanvas
        self.context_menu_connect = (
            self.iface.mapCanvas().contextMenuAboutToShow.connect(
                lambda menu_, event_: self.canvas_menu_about_to_show(menu_, event_)
            )
        )

        # register custom expression for layer style
        QgsExpression.registerFunction(get_projected_length_of_3D_line)
        QgsExpression.registerFunction(get_3d_distance_from_n_vertices)
        QgsExpression.registerFunction(get_geometry_with_m_dimension)

        if dependency_valid:
            # -- Dock widgets
            self.depth_assay_visu_dock = QDockWidget(
                "Display depth data", self.iface.mainWindow()
            )
            self.depth_assay_visu_widget = AssayWidget(
                AssayDomainType.DEPTH, self.iface.mainWindow(), self.openlog_connection
            )
            self.depth_assay_visu_dock.setWidget(self.depth_assay_visu_widget)
            self.iface.addDockWidget(Qt.RightDockWidgetArea, self.depth_assay_visu_dock)
            self.depth_assay_visu_dock.close()
            self.depth_assay_dock_action = self.depth_assay_visu_dock.toggleViewAction()
            self.depth_assay_dock_action.setEnabled(True)
            self._add_plugin_actions(self.depth_assay_dock_action)
            self.depth_assay_visu_widget._plot_grid.inspectorEnabledSignal.connect(
                self._enable_inspector_table_sync
            )

            self.time_assay_visu_dock = QDockWidget(
                "Display time data", self.iface.mainWindow()
            )
            self.time_assay_visu_widget = AssayWidget(
                AssayDomainType.TIME, self.iface.mainWindow(), self.openlog_connection
            )
            self.time_assay_visu_dock.setWidget(self.time_assay_visu_widget)
            self.iface.addDockWidget(Qt.RightDockWidgetArea, self.time_assay_visu_dock)
            self.time_assay_visu_dock.close()
            self.time_assay_dock_action = self.time_assay_visu_dock.toggleViewAction()
            self.time_assay_dock_action.setEnabled(True)
            self._add_plugin_actions(self.time_assay_dock_action)
            self.time_assay_visu_widget._plot_grid.inspectorEnabledSignal.connect(
                self._enable_inspector_table_sync
            )

        self.project_loaded_connect = QgsProject.instance().readProject.connect(
            self._qgis_project_loaded
        )

        self.project_filename_changed_connect = (
            QgsProject.instance().fileNameChanged.connect(
                self._qgis_project_filename_changed
            )
        )

        self.project_cleared_connect = QgsProject.instance().cleared.connect(
            self._qgis_project_cleared
        )
        self.initProcessing()

        # monkeypatch for windows (locked dll issue when upgrading OpenLog)
        if os.name == "nt":
            QgsPluginInstaller.installPlugin = install_plugin
            QgsPluginInstaller.uninstallPlugin = uninstall_plugin

        # launch version check
        self.version_checker.finished.connect(self._check_openlog_version)
        self.version_checker.start()

    def _check_openlog_version(self) -> None:
        """
        Check QGIS and openlog version are up to date.
        """
        msg = self.version_checker.result
        if msg is not None:
            mb = QMessageBox()
            mb.setIcon(QMessageBox.Information)
            mb.setText(msg)
            mb.setWindowTitle("OpenLog warning")
            mb.setStandardButtons(QMessageBox.StandardButton.Ok)
            res = mb.exec()

    def initProcessing(self):
        self.provider = OpenLogProvider()
        QgsApplication.processingRegistry().addProvider(self.provider)

    def _qgis_project_loaded(self) -> None:
        """
        Restore connection if available in QgsProject settings

        """
        # Restore cursor to remove waiting cursor
        QGuiApplication.restoreOverrideCursor()

        connection_type = QgsProject.instance().readEntry(
            "OpenLog", BASE_SETTINGS_KEY + PROJECT_CONNECTION_TYPE_KEY
        )[0]
        if connection_type:
            connection = OpenLogConnectionFactory().create_connection_from_project(
                connection_type=connection_type,
                base_settings_key=BASE_SETTINGS_KEY,
                parent=self.iface.mainWindow(),
            )
            if connection:
                # Need to use timer for connection definition because we can't remove layer from project during
                # project load
                # If we remove layers a few millisecond later it's fine
                # QgsProject.instance().readProject signal seems to be fired too soon
                self.project_load_timer = QTimer(self.iface.mainWindow())
                self.project_load_timer.setSingleShot(True)
                self.project_load_timer.timeout.connect(
                    lambda: self._define_current_connection(connection)
                )
                self.project_load_timer.start(100)

    def _qgis_project_filename_changed(self) -> None:
        """
        Define new project title when filename is changed

        """
        if self.openlog_connection:
            new_title = f"<{QgsProject.instance().fileName()}> {self.openlog_connection.get_mainwindow_title()}"
            QgsProject.instance().setTitle(new_title)
            # Need to force dirty changed signal emission because there is a blocker implemented in QGIS
            # If not done, title is not updated
            QgsProject.instance().isDirtyChanged.emit(True)

    def _qgis_project_cleared(self) -> None:
        """
        Clear current openlog connection when project cleared

        """
        if self.openlog_connection:
            self._define_current_connection(None)

    def _add_plugin_actions(self, action: QAction):
        """
        Add action to plugin menu and store in actions list

        Args:
            action: QAction
        """
        self._plugin_actions.append(action)
        self.menu.addAction(action)

    def unload(self):
        """Cleans up when plugin is disabled/uninstalled."""

        # remove actions and clean up menu
        for action in self._plugin_actions:
            self.menu.removeAction(action)
            del action

        if self.menu:
            self.iface.mainWindow().menuBar().removeAction(self.menu.menuAction())
            del self.menu

        if self.depth_assay_visu_dock:
            self.iface.removeDockWidget(self.depth_assay_visu_dock)
            del self.depth_assay_visu_dock

        if self.time_assay_visu_dock:
            self.iface.removeDockWidget(self.time_assay_visu_dock)
            del self.time_assay_visu_dock

        self._unload_loaded_layer()

        # -- Clean up preferences panel in QGIS settings
        self.iface.unregisterOptionsWidgetFactory(self.options_factory)

        if self.context_menu_connect:
            self.iface.mapCanvas().contextMenuAboutToShow.disconnect(
                self.context_menu_connect
            )

        if self.project_loaded_connect:
            QgsProject.instance().readProject.disconnect(self.project_loaded_connect)

        if self.project_cleared_connect:
            QgsProject.instance().cleared.disconnect(self.project_cleared_connect)

        # -- Unregister processing
        QgsApplication.processingRegistry().removeProvider(self.provider)

        # unregister custom expression
        QgsExpression.unregisterFunction(get_projected_length_of_3D_line.name())
        QgsExpression.unregisterFunction(get_3d_distance_from_n_vertices.name())
        QgsExpression.unregisterFunction(get_geometry_with_m_dimension.name())

        # remove annotations
        for ann in QgsProject.instance().annotationManager().annotations():
            QgsProject.instance().annotationManager().removeAnnotation(ann)

    def _unload_loaded_layer(self):
        """
        Unload openlog layer from map

        """
        if self.openlog_connection:
            try:
                if self.openlog_connection.get_layers_iface().get_collar_layer():
                    QgsProject.instance().removeMapLayer(
                        self.openlog_connection.get_layers_iface().get_collar_layer()
                    )
                if self.openlog_connection.get_layers_iface().get_collar_trace_layer():
                    QgsProject.instance().removeMapLayer(
                        self.openlog_connection.get_layers_iface().get_collar_trace_layer()
                    )

                if (
                    self.openlog_connection.get_layers_iface().get_planned_collar_layer()
                ):
                    QgsProject.instance().removeMapLayer(
                        self.openlog_connection.get_layers_iface().get_planned_collar_layer()
                    )

                if self.openlog_connection.get_layers_iface().get_planned_trace_layer():
                    QgsProject.instance().removeMapLayer(
                        self.openlog_connection.get_layers_iface().get_planned_trace_layer()
                    )
                # remove groups
                root = QgsProject.instance().layerTreeRoot()
                group = root.findGroup("Effective")
                if group is not None:
                    root.removeChildNode(group)
                group = root.findGroup("Planned")
                if group is not None:
                    root.removeChildNode(group)

                self._unload_assay_views()

            except RuntimeError:
                self.log(message="OpenLog connection layers can't be removed.")

    def canvas_menu_about_to_show(self, menu: QMenu, event):
        """
        Add collars action to canvas custom menu

        Args:
            menu: QMenu for map canvas
            event:
        """
        menu.addAction(self.selected_colar_desurv_action)
        menu.addAction(self.selected_colar_survey_definition_action)
        menu.addAction(self.selected_colar_edition_action)
        menu.addAction(self.depth_assay_dock_action)
        menu.addAction(self.time_assay_dock_action)

    def _manage_profiles(self) -> None:
        """
        Execute profile manager widget.
        """
        if self.manage_profile_wizard is None:

            plugin_manager = get_plugin_manager()
            wizard_class = plugin_manager.get_advanced_imports_plugin().profile_manager

            self.manage_profile_wizard = wizard_class(
                self.openlog_connection, self, self.iface.mainWindow()
            )

        if self.manage_profile_wizard:
            self.manage_profile_wizard.show()

    def _manage_assays(self) -> None:
        """
        Execute AssayAdminWizard for assay management
        """
        plugin_manager = get_plugin_manager()
        wizard_class = plugin_manager.get_manager_assay_plugin().wizard

        self.manage_assay_wizard = wizard_class(
            self.openlog_connection, self, self.iface.mainWindow()
        )
        self.manage_assay_wizard.show()

    def remove_column_from_ui(
        self, variable: str, column: str, display_name: str = None
    ):
        """
        Remove column data displayed in openlog interface.
        This include logviewer, splitted layers, assay views.
        Args:
            - variable (str) : assay name
            - column (str) : column name
        """
        # first, remove from logviewer
        self.depth_assay_visu_widget._visualization_config.remove_column(column)
        self.time_assay_visu_widget._visualization_config.remove_column(column)

        # remove layers
        layers = QgsProject.instance().mapLayers()
        for layer in layers.values():
            name = layer.name()
            # splitted layers
            if f"{variable} - {display_name}" in name and (
                "[Trace]" in name or "[Planned trace]" in name
            ):
                QgsProject.instance().removeMapLayer(layer)

        # Refresh displayed visualization
        self.depth_assay_visu_widget._visualization_config_model.refresh()
        self.depth_assay_visu_widget._check_state_changed()
        self.time_assay_visu_widget._visualization_config_model.refresh()
        self.time_assay_visu_widget._check_state_changed()
        self.depth_assay_visu_widget._visualization_config.refresh_inspector_handler()

        # reinitialize assay views
        self._unload_assay_views()
        self._load_assay_views()

    def remove_assay_from_ui(self, variable: str):
        """
        Remove all assay data displayed in openlog interface.
        This include logviewer, splitted layers, assay views.
        Args:
            - variable (str) : assay name
        """
        # first, remove from logviewer
        self.depth_assay_visu_widget._visualization_config.remove_assay(variable)
        self.time_assay_visu_widget._visualization_config.remove_assay(variable)

        # remove layers
        layers = QgsProject.instance().mapLayers()
        for layer in layers.values():
            name = layer.name()
            # views
            if f"{variable} - [" in name:
                QgsProject.instance().removeMapLayer(layer)

            # splitted layers
            if f"{variable} -" in name and (
                "[Trace]" in name or "[Planned trace]" in name
            ):
                QgsProject.instance().removeMapLayer(layer)

            # barcharts layers
            if f"{variable} -" in name and (
                "[Barcharts]" in name or "[Planned barcharts]" in name
            ):
                QgsProject.instance().removeMapLayer(layer)

        # Refresh displayed visualization
        self.depth_assay_visu_widget._visualization_config_model.refresh()
        self.depth_assay_visu_widget._check_state_changed()
        self.time_assay_visu_widget._visualization_config_model.refresh()
        self.time_assay_visu_widget._check_state_changed()
        self.depth_assay_visu_widget._visualization_config.refresh_inspector_handler()

    def _import_assay(self) -> None:
        """
        Execute AssayImportWizard for assay import

        """
        wizard = AssayImportWizard(self.openlog_connection, self.iface.mainWindow())
        wizard.exec()

        # refresh assay views
        self._unload_assay_views()
        self._load_assay_views()

    def _import_ags(self) -> None:
        """
        Execute wizard for AGS import.
        """
        wizard = self.plugin_manager.get_ags_reader_plugin().wizard(
            self.openlog_connection, self.iface.mainWindow()
        )
        wizard.exec()

        # force refresh of collar attribute table
        for widget in QgsApplication.allWidgets():
            if self.collar_attributes is not None and widget == self.collar_attributes:
                widget.close()

        self._show_collar_attribute_table()

    def _import_collar(self) -> None:
        """
        Execute DatabaseImportWizard for collar import

        """
        wizard = DatabaseImportWizard(
            self.openlog_connection,
            self.iface.mainWindow(),
            import_pages=ImportPages.COLLAR,
        )
        wizard.exec()

        # force refresh of collar attribute table
        for widget in QgsApplication.allWidgets():
            if self.collar_attributes is not None and widget == self.collar_attributes:
                widget.close()

        self._show_collar_attribute_table()

        # update inspector handler EOHs with new collars
        collars = self.openlog_connection.get_read_iface().get_all_collars()
        self.depth_assay_visu_widget._visualization_config.set_inspector_handler_eohs(
            collars
        )
        self.time_assay_visu_widget._visualization_config.set_inspector_handler_eohs(
            collars
        )

    def _import_survey(self) -> None:
        """
        Execute DatabaseImportWizard for survey import

        """
        wizard = DatabaseImportWizard(
            self.openlog_connection,
            self.iface.mainWindow(),
            import_pages=ImportPages.SURVEY,
        )
        wizard.exec()

    def _xplordb_create(self) -> None:
        """
        Execute DatabaseCreationWizard for xplordb database creation

        """
        wizard = DatabaseCreationWizard(self.iface.mainWindow())
        wizard.exec()

    def _spatialite_create(self) -> None:
        """
        Select file for spatialite db creation

        """
        filename, filter_use = QFileDialog.getSaveFileName(
            self.iface.mainWindow(),
            "Select file",
            "openlog_spatialite.db",
            "Spatialite database (*.db)",
        )
        if filename:
            self._define_current_connection(
                SpatialiteConnection(Path(filename), new_file=True)
            )

    def _define_current_connection(self, connection: OpenLogConnection):
        """
        Define current openlog connection :
        - add open log layers
        - define import actions availability depending on connection

        Args:
            connection: OpenLogConnection
        """
        self._unload_loaded_layer()
        self.openlog_connection = connection

        if self.openlog_connection:
            # if user-defined project, don't override project name
            if QgsProject.instance().fileName() == "":
                new_title = f"<{QgsProject.instance().fileName()}> {self.openlog_connection.get_mainwindow_title()}"
                QgsProject.instance().setTitle(new_title)
                # Need to force dirty changed signal emission because there is a blocker implemented in QGIS
                # If not done, title is not updated
                QgsProject.instance().isDirtyChanged.emit(True)
                self.openlog_connection.save_to_qgis_project(BASE_SETTINGS_KEY)
                QgsProject.instance().writeEntry(
                    "OpenLog",
                    BASE_SETTINGS_KEY + PROJECT_CONNECTION_TYPE_KEY,
                    type(self.openlog_connection).__name__,
                )

        self._add_openlog_layers()
        if self.openlog_connection:
            import_collar_enabled = (
                self.openlog_connection.get_write_iface().can_import_collar()
            )
            self.add_collar_action.setEnabled(import_collar_enabled)
            self.import_action_collar.setEnabled(import_collar_enabled)
            self.import_action_survey.setEnabled(import_collar_enabled)

            self.import_assay_action.setEnabled(
                self.openlog_connection.get_assay_iface().can_import_assay()
            )
            self.manage_assay_action.setEnabled(
                self.openlog_connection.get_assay_iface().can_import_assay()
                and self.manage_assay_enabled
            )
            self.profile_manager_action.setEnabled(
                self.openlog_connection.get_assay_iface().can_import_assay()
                and self.advanced_imports_enabled
            )
            self.ags_action.setEnabled(
                self.openlog_connection.get_assay_iface().can_import_assay()
                and self.ags_file_enabled
            )
        else:
            self.add_collar_action.setEnabled(False)
            self.import_action_collar.setEnabled(False)
            self.import_action_survey.setEnabled(False)
            self.import_assay_action.setEnabled(False)
            self.manage_assay_action.setEnabled(False)

        self.depth_assay_visu_widget.set_openlog_connection(connection)
        self.time_assay_visu_widget.set_openlog_connection(connection)

    def _display_splash(self, count: int):
        if count in [1, 5, 10]:
            ws = WelcomeScreen()
            ws.exec()

    def _count_openlog_use(self) -> None:
        """
        Update the number of use of Openlog.
        It is stored in plugin root (use_count.txt)
        """
        if not self.splash_active:
            return

        path = str(DIR_PLUGIN_ROOT / "use_count.txt")
        if os.path.exists(path):
            with open(path, "r") as f:
                count = f.readline()
            count = int(count) + 1
            with open(path, "w") as f:
                f.write(str(count))
        else:
            with open(path, "w") as f:
                f.write(str(1))
            count = 1
        self._display_splash(count)

    def _planned_collar_feature_selected(self):
        """
        Synchronize selection to collar layer.
        """
        selected_ids = (
            self.openlog_connection.get_layers_iface().get_selected_planned_collar_from_layer()
        )
        self.openlog_connection.get_layers_iface().select_collar_by_id(selected_ids)

    def _add_openlog_layers(self) -> None:
        """
        Add openlog layers from current connection

        """
        self._count_openlog_use()
        self._load_openlog_layers()

    def _load_openlog_layers(self) -> None:
        # check if current project is a user-defined project
        is_project = QgsProject.instance().fileName() != ""
        collar_style = None
        planned_collar_style = None
        trace_style = None
        planned_trace_style = None
        root = QgsProject.instance().layerTreeRoot()
        if self.openlog_connection:

            # remove Assays group first
            self._unload_assay_views()

            # remove Collar and Trace layers if exists
            for layer in QgsProject.instance().mapLayersByName(
                self.openlog_connection.get_layers_iface().get_collar_layer_name()
            ):
                # get existing collar style
                collar_style = layer.styleManager().style(
                    layer.styleManager().currentStyle()
                )
                if (
                    layer
                    != self.openlog_connection.get_layers_iface().get_collar_layer()
                ):

                    QgsProject.instance().removeMapLayer(layer.id())

            for layer in QgsProject.instance().mapLayersByName(
                self.openlog_connection.get_layers_iface().get_planned_collar_layer_name()
            ):
                # get existing collar style
                planned_collar_style = layer.styleManager().style(
                    layer.styleManager().currentStyle()
                )
                if (
                    layer
                    != self.openlog_connection.get_layers_iface().get_planned_collar_layer()
                ):

                    QgsProject.instance().removeMapLayer(layer.id())

            for layer in QgsProject.instance().mapLayersByName(
                self.openlog_connection.get_layers_iface().get_collar_trace_layer_name()
            ):
                # get existing trace style
                trace_style = layer.styleManager().style(
                    layer.styleManager().currentStyle()
                )
                if (
                    layer
                    != self.openlog_connection.get_layers_iface().get_collar_trace_layer()
                ):
                    QgsProject.instance().removeMapLayer(layer.id())

            for layer in QgsProject.instance().mapLayersByName(
                self.openlog_connection.get_layers_iface().get_planned_trace_layer_name()
            ):
                # get existing trace style
                planned_trace_style = layer.styleManager().style(
                    layer.styleManager().currentStyle()
                )
                if (
                    layer
                    != self.openlog_connection.get_layers_iface().get_planned_trace_layer_name()
                ):
                    QgsProject.instance().removeMapLayer(layer.id())

            # create effective group
            effective_group = root.findGroup("Effective")
            if effective_group is None:
                effective_group = root.addGroup("Effective")
                effective_group.setExpanded(True)
            # add openlog_connection Collar and Trace layers
            if self.openlog_connection.get_layers_iface().get_collar_layer():

                default_style = (
                    self.openlog_connection.get_layers_iface().get_collar_layer_style_file()
                )
                layer = self.openlog_connection.get_layers_iface().get_collar_layer()

                if collar_style and is_project:
                    layer.styleManager().removeStyle(
                        layer.styleManager().currentStyle()
                    )
                    layer.styleManager().addStyle("def", collar_style)
                    layer.styleManager().setCurrentStyle("def")
                    layer.triggerRepaint()
                else:
                    layer.loadNamedStyle(str(default_style))
                QgsProject.instance().addMapLayer(layer, False)
                effective_group.addLayer(layer)
                # select this layer by default
                self.iface.layerTreeView().setCurrentLayer(layer)

                self.openlog_connection.get_layers_iface().get_collar_layer().selectionChanged.connect(
                    self._collar_feature_selected
                )

                self._show_collar_attribute_table()

            # add Trace layer
            if self.openlog_connection.get_layers_iface().get_collar_trace_layer():
                default_style = (
                    self.openlog_connection.get_layers_iface().get_collar_trace_layer_style_file()
                )
                layer = (
                    self.openlog_connection.get_layers_iface().get_collar_trace_layer()
                )

                layer.loadNamedStyle(str(default_style))
                QgsProject.instance().addMapLayer(layer, False)
                effective_group.addLayer(layer)

            # create planned group
            planned_group = root.findGroup("Planned")
            if planned_group is None:
                planned_group = root.addGroup("Planned")
                planned_group.setExpanded(True)
            # add planned collar layer
            if self.openlog_connection.get_layers_iface().get_planned_collar_layer():

                default_style = (
                    self.openlog_connection.get_layers_iface().get_planned_collar_layer_style_file()
                )
                layer = (
                    self.openlog_connection.get_layers_iface().get_planned_collar_layer()
                )

                if planned_collar_style and is_project:
                    layer.styleManager().removeStyle(
                        layer.styleManager().currentStyle()
                    )
                    layer.styleManager().addStyle("def", planned_collar_style)
                    layer.styleManager().setCurrentStyle("def")
                    layer.triggerRepaint()
                else:
                    layer.loadNamedStyle(str(default_style))
                QgsProject.instance().addMapLayer(layer, False)
                planned_group.addLayer(layer)
                layer_node = root.findLayer(layer.id())
                layer_node.setItemVisibilityChecked(False)
                self.openlog_connection.get_layers_iface().get_planned_collar_layer().selectionChanged.connect(
                    self._planned_collar_feature_selected
                )

            # add planned trace layer
            if self.openlog_connection.get_layers_iface().get_planned_trace_layer():
                default_style = (
                    self.openlog_connection.get_layers_iface().get_planned_trace_layer_style_file()
                )
                layer = (
                    self.openlog_connection.get_layers_iface().get_planned_trace_layer()
                )

                layer.loadNamedStyle(str(default_style))
                QgsProject.instance().addMapLayer(layer, False)
                planned_group.addLayer(layer)

            # remove planned group if empty
            if len(planned_group.findLayers()) == 0:
                root.removeChildNode(planned_group)

            self._load_assay_views()

    def _show_collar_attribute_table(self):
        """
        Display collar attribute table.
        """
        # Show attribute table
        settings = QgsSettings()
        prev_value = settings.value("qgis/dockAttributeTable")
        settings.setValue("qgis/dockAttributeTable", True)
        self.collar_attributes = self.iface.showAttributeTable(
            self.openlog_connection.get_layers_iface().get_collar_layer()
        )
        settings.setValue("qgis/dockAttributeTable", prev_value)

    def _unload_assay_views(self):
        """
        Remove assay views from QGIS layer tab.
        """
        # remove Assays group
        root = QgsProject.instance().layerTreeRoot()
        group = root.findGroup("Downhole data")
        if group is not None:
            for layer in group.findLayers():
                QgsProject.instance().removeMapLayer(layer.layerId())

            root.removeChildNode(group)

    def _highlight_assay_row(self):
        """
        Highlight assay view rows with inspector position.
        """
        self.sync_inspector_row_thread.quit()
        result = self.sync_inspector_row_worker.result
        for lyr, row_ids in result.items():
            lyr.selectByIds(row_ids)

    def _start_inspector_row_timer(self):

        self.inspector_row_timer.start(1000)

    def _enable_inspector_table_sync(self):
        """
        Slot for synchronization between inspector lines and assay views.
        """
        # unselect rows
        for lyr in self.assay_layers:
            lyr.selectByIds([])

        depth_checked = (
            self.depth_assay_visu_widget._plot_grid.enable_inspector_line_action.isChecked()
        )
        time_checked = (
            self.time_assay_visu_widget._plot_grid.enable_inspector_line_action.isChecked()
        )
        for config in self.depth_assay_visu_widget._visualization_config.list:
            for col_name, col_config in config.column_config.items():
                if col_config.plot_widget:
                    try:
                        if depth_checked:
                            col_config.plot_widget.inspector.sigPositionChanged.connect(
                                self._start_inspector_row_timer
                            )
                            # trigger manually for initial position
                            self._start_inspector_row_timer()
                        else:
                            col_config.plot_widget.inspector.sigPositionChanged.disconnect(
                                self._start_inspector_row_timer
                            )
                    except:
                        pass

        for config in self.time_assay_visu_widget._visualization_config.list:
            for col_name, col_config in config.column_config.items():
                if col_config.plot_widget:
                    try:
                        if time_checked:
                            col_config.plot_widget.inspector.sigPositionChanged.connect(
                                self._start_inspector_row_timer
                            )
                            self._start_inspector_row_timer()
                        else:
                            col_config.plot_widget.inspector.sigPositionChanged.disconnect(
                                self._start_inspector_row_timer
                            )
                    except:
                        pass

    def _load_assay_views(self):
        """
        Display assay views in QGIS layer tab.
        """
        # assay tables
        assays = (
            self.openlog_connection.get_assay_iface().get_all_available_assay_definitions()
        )
        self.assay_layers = self.openlog_connection.get_layers_iface().get_assay_layers(
            assays
        )

        if len(self.assay_layers) > 0:
            # assays group
            root = QgsProject.instance().layerTreeRoot()
            group = root.addGroup("Downhole data")
            group.setExpanded(False)

            for layer in self.assay_layers:
                if layer.isValid():
                    QgsProject.instance().addMapLayer(layer, False)
                    group.addLayer(layer)

    def _local_grid(self) -> None:
        """
        Show LocalGridCreationDialog for local grid definition

        """
        if not self.local_grid_creation_dialog:
            self.local_grid_creation_dialog = LocalGridCreationDialog(
                self.openlog_connection, self.iface.mainWindow()
            )
            # Connection to dialog finished for temporary layer remove and openlog layer repaint trigger
            self.local_grid_creation_dialog.finished.connect(
                self._local_grid_creation_finished
            )

        self.local_grid_creation_dialog.show()

    def _add_collar(self) -> None:
        """
        Show CollarCreationDialog for collar add

        """
        if not self.collar_creation_dialog:
            self.collar_creation_dialog = CollarCreationDialog(
                self.openlog_connection, self.iface.mainWindow()
            )
            # Connection to dialog finished for temporary layer remove and openlog layer repaint trigger
            self.collar_creation_dialog.finished.connect(self._collar_add_finished)

        self.collar_creation_dialog.show()

    def _local_grid_creation_finished(self) -> None:
        """
        Delete CollarCreationDialog use for collar add and remove temporary layer used.

        Trigger repaint for openlog layer for new collar

        """
        if self.local_grid_creation_dialog:
            self.local_grid_creation_dialog.remove_temp_layer()
            self.local_grid_creation_dialog.deleteLater()
            self.local_grid_creation_dialog = None

            self.iface.mapCanvas().redrawAllLayers()
            self.iface.mapCanvas().refresh()

    def _collar_add_finished(self) -> None:
        """
        Delete CollarCreationDialog use for collar add and remove temporary layer used.

        Trigger repaint for openlog layer for new collar

        """
        if self.collar_creation_dialog:
            self.collar_creation_dialog.remove_temp_layer()
            self.collar_creation_dialog.deleteLater()
            self.collar_creation_dialog = None

            self.iface.mapCanvas().redrawAllLayers()
            self.iface.mapCanvas().refresh()

    def _selected_collar_desurveying(self, hole_ids: list = []):
        """
        It takes the hole_ids of the selected features in the collar layer and uses them to get the
        surveys from the openlog database.
        Then it uses the minimum curvature method to calculate the desurveying of the hole.
        """
        if len(hole_ids) == 0:
            hole_ids = [
                f["hole_id"]
                for f in self.openlog_connection.get_layers_iface()
                .get_collar_layer()
                .selectedFeatures()
            ]

        self.openlog_connection.desurvey_collars(hole_ids)
        self.iface.mapCanvas().redrawAllLayers()
        self.iface.mapCanvas().refresh()

    def _selected_collar_edition(self) -> None:
        """
        If there are selected collar, display collar edition dialog
        """

        hole_ids = [
            f["hole_id"]
            for f in self.openlog_connection.get_layers_iface()
            .get_collar_layer()
            .selectedFeatures()
        ]

        collar_edition_dialog = CollarEditionDialog(
            self.openlog_connection, self.iface.mainWindow()
        )
        collar_edition_dialog.set_selected_collar(hole_ids)
        updated = collar_edition_dialog.exec()
        if (
            updated == 1
            and self.openlog_connection.selected_collar_surveying_available()
            and len(collar_edition_dialog.updated_collars) > 0
        ):
            # get only edited collars
            self._selected_collar_desurveying(
                [collar.hole_id for collar in collar_edition_dialog.updated_collars]
            )

    def _selected_collar_survey_definition(self) -> None:
        """
        If there are selected collar, display survey creation dialog and re-calculate the desurveying

        """
        hole_ids = [
            f["hole_id"]
            for f in self.openlog_connection.get_layers_iface()
            .get_collar_layer()
            .selectedFeatures()
        ]
        survey_creation_dialog = SurveyCreationDialog(
            self.openlog_connection, self.iface.mainWindow()
        )
        survey_creation_dialog.set_selected_collar(hole_ids)
        # survey_creation_dialog.exec()
        if (
            survey_creation_dialog.exec()
            and self.openlog_connection.selected_collar_surveying_available()
        ):
            self._selected_collar_desurveying()

    def _collar_feature_selected(self) -> None:
        """
        If there are selected collars with associated surveys, then enable the desurvey action.
        Synchronize current selection with planned collars.
        """
        self.selected_colar_desurv_action.setEnabled(False)
        self.selected_colar_survey_definition_action.setEnabled(False)
        self.selected_colar_edition_action.setEnabled(False)

        # enable/disable desurveying, collar edition, survey edition
        if (
            self.openlog_connection.collar_and_survey_edition_available()
            or self.openlog_connection.selected_collar_surveying_available()
        ):

            collar_id = [
                f["hole_id"]
                for f in self.openlog_connection.get_layers_iface()
                .get_collar_layer()
                .selectedFeatures()
            ]

            if self.openlog_connection.selected_collar_surveying_available():
                self.selected_colar_desurv_action.setEnabled(len(collar_id) != 0)

            if (
                self.openlog_connection.get_write_iface().selected_collar_survey_definition_available()
            ):
                self.selected_colar_survey_definition_action.setEnabled(
                    len(collar_id) != 0
                )

            if (
                self.openlog_connection.get_write_iface().selected_collar_edition_available()
            ):
                self.selected_colar_edition_action.setEnabled(len(collar_id) != 0)

        # synchronize selection of collar-planned collar
        if (
            self.openlog_connection.get_layers_iface().get_planned_collar_layer()
            is not None
        ):
            # sycnhronization with planned collar layer
            selected_ids = (
                self.openlog_connection.get_layers_iface().get_selected_collar_from_layer()
            )
            ## disconnect temporarily signal to avoid signal loop
            self.openlog_connection.get_layers_iface().get_planned_collar_layer().selectionChanged.disconnect(
                self._planned_collar_feature_selected
            )
            self.openlog_connection.get_layers_iface().select_planned_collar_by_id(
                selected_ids
            )
            self.openlog_connection.get_layers_iface().get_planned_collar_layer().selectionChanged.connect(
                self._planned_collar_feature_selected
            )

    def run(self):
        """Main process.

        :raises Exception: if there is no item in the feed
        """
        try:
            self.log(
                message="Everything ran OK.",
                log_level=3,
                push=False,
            )
        except Exception as err:
            self.log(
                message="Houston, we've got a problem: {}".format(err),
                log_level=2,
                push=True,
            )
