#! python3  # noqa: E265

"""
Main plugin module.
"""

import importlib
import json
import os
import shutil
from contextlib import suppress
from functools import partial
from math import pi
from pathlib import Path

import numpy as np
from PyQt5.QtCore import QRect, Qt
from PyQt5.QtWidgets import QDialogButtonBox, QMessageBox, QWidget

# PyQGIS
from qgis.core import (
    Qgis,
    QgsApplication,
    QgsFeature,
    QgsGeometry,
    QgsProject,
    QgsVectorLayerUtils,
)
from qgis.gui import QgisInterface
from qgis.PyQt.QtCore import QCoreApplication, QSettings, QUrl
from qgis.PyQt.QtGui import QDesktopServices, QIcon
from qgis.PyQt.QtWidgets import QAction, QDialog, QMenu
from qgis.utils import showPluginHelp

from topaze.__about__ import DIR_PLUGIN_ROOT, __title__, __uri_homepage__
from topaze.dlg_util import DlgUtil
from topaze.gui.additional_canvas_dialog import AdditionalCanvasDialog
from topaze.gui.dlg_settings import PlgOptionsFactory
from topaze.gui.edit_obs_dialog import EditObsDialog
from topaze.gui.free_station_dialog import FreeStationDialog
from topaze.gui.import_field_data_dialog import ImportFieldDataDialog
from topaze.gui.import_points_dialog import ImportPointsDialog
from topaze.gui.intersection_dialog import IntersectionDialog
from topaze.gui.new_survey_dialog import NewSurveyDialog
from topaze.gui.resection_dialog import ResectionDialog
from topaze.gui.topaze_about import TopazeAbout
from topaze.gui.traverse_dialog import TraverseDialog
from topaze.gui.trilateration_dialog import TrilaterationDialog
from topaze.helmert_group import HelmertGroup
from topaze.shared import shared_ptopo_array
from topaze.toolbelt import PlgLogger, PlgOptionsManager
from topaze.toolbelt.i18n import init_i18n, tr
from topaze.topaze_global import TopazeConst
from topaze.topaze_layers import TopazeLayers
from topaze.totalopenstation import formats
from topaze.totalopenstation.formats import BUILTIN_INPUT_FORMATS
from topaze.totalopenstation.output.tops_azimut import OutputFormat

# project
from . import resources
from .file_utils import FileUtils
from .observation import Observation
from .ptopo import Ptopo, PtopoConst
from .station import Station
from .topaze_calculator import TopazeCalculator
from .topaze_config import TopazeConfig
from .topaze_global import TopazeGlobal
from .topaze_utils import TopazeUtils

# ############################################################################
# ########## Classes ###############
# ##################################

# __topaze_global__ = TopazeGlobal()


class TopazePlugin:
    OBSERVATIONS_LAYER_NAME = "observations"

    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
        """
        # print(
        #    "++++++++++++++++++++++++++++++++ __init__++++++++++++++++++++++++++++++++ "
        # )
        # _init_translator()
        # initialize locale
        init_i18n()

        resources.__name__
        # global __topaze_global__
        self.dlg_helmert_canvas = None
        self.log = PlgLogger().log
        self.plg_settings = PlgOptionsManager()
        self.settings = self.plg_settings.get_plg_settings()
        self._ptopo_array = []
        self._ptopo_layer = None
        self._observations_layer = None
        self.work_on_survey = False
        self._obs_storage_mode = "file"  # "file" or "db"
        try:
            layers = QgsProject.instance().mapLayersByName(TopazeConst.PTOPO_LAYER_NAME)
            if layers:
                self._ptopo_layer = layers[0]
                self.work_on_survey = True
        except Exception as e:
            pass

        self.iface = iface
        self.actions = []
        self.sub_menu = None

        if self.settings.config_file:
            config_path = self.settings.config_file
        else:
            config_path = Path(__file__).parent / "resources/settings" / "topaze.yml"
        TopazeConfig.load_config(config_path)

        self.dlg_import_field_data = ImportFieldDataDialog()
        self.dlg_import_points = ImportPointsDialog()
        self.dlg_new_survey = NewSurveyDialog()
        self.dlg_edit_obs = EditObsDialog()
        self.dlg_edit_obs.pushButton_print.setVisible(False)
        self.dlg_edit_obs.pushButton_print.setEnabled(False)
        self.dlg_free_station = FreeStationDialog()
        self.dlg_free_station.set_iface(self.iface)
        self.dlg_resection = ResectionDialog()
        self.dlg_resection.set_iface(self.iface)
        self.dlg_intersection = IntersectionDialog()
        self.dlg_intersection.set_iface(self.iface)
        self.dlg_trilateration = TrilaterationDialog()
        self.dlg_trilateration.set_iface(self.iface)
        self.dlg_traverse = TraverseDialog()
        self.dlg_traverse.set_iface(self.iface)
        self.action_compute_point = None
        self.log = PlgLogger().log
        self._observations_layer = None
        self._ptopo_array = []
        self.iface.projectRead.connect(self.on_topaze_project_read)

    def _add_action(
        self,
        text,
        callback,
        icon=None,
        add_to_menu: bool = True,
        add_to_toolbar: bool = True,
        parent=None,
    ):
        """Create, register and return a QAction."""

        if icon is None:
            qicon = QIcon()
        elif isinstance(icon, QIcon):
            qicon = icon
        else:
            qicon = QIcon(icon)

        action = QAction(qicon, text, parent or self.iface.mainWindow())
        action.triggered.connect(callback)
        action.setEnabled(True)

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

        if add_to_toolbar:
            self.iface.addToolBarIcon(action)

        self.actions.append(action)
        return action

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

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

        if self.settings.is_first_use:
            self.settings.is_first_use = False
            PlgOptionsManager.save_from_object(self.settings)
            self.show_help_about()

        # Declare instance attributes
        self._current_obs = None
        self._previous_obs = None

        QgsProject.instance().aboutToBeCleared.connect(
            self.on_project_about_to_be_closed
        )

        self.action_new_survey_dialog = self._add_action(
            tr("New survey"),
            self.command_new_survey,
            ":/images/themes/default/mActionNewFolder.svg",
        )

        self.action_import_points_dialog = self._add_action(
            tr("Import points"),
            self.command_import_points,
            ":/oauth2method/svg/import.svg",
        )

        self.action_update_observations_dialog = self._add_action(
            tr("Update observations"),
            self.command_update_observations,
            ":/images/themes/default/mActionCopyProfileSettings.svg",
        )

        self.action_edit_observations = self._add_action(
            tr("Edit observations"),
            self.command_edit_observations,
            ":/images/themes/default/console/iconShowEditorConsole.svg",
        )

        self.sub_menu = QMenu(tr("Compute reference points"), self.iface.mainWindow())
        self.sub_menu.setIcon(
            QIcon(":/qt-project.org/styles/commonstyle/images/computer-32.png")
        )
        self.iface.addPluginToMenu(__title__, self.sub_menu.menuAction())

        self.action_compute_free_station = self._add_action(
            tr("Free station"),
            self.command_compute_free_station,
            add_to_menu=False,
            add_to_toolbar=False,
        )
        self.sub_menu.addAction(self.action_compute_free_station)

        self.action_compute_intersection = self._add_action(
            tr("Intersection"),
            self.command_compute_intersection,
            add_to_menu=False,
            add_to_toolbar=False,
        )
        self.sub_menu.addAction(self.action_compute_intersection)

        self.action_compute_resection = self._add_action(
            tr("Resection"),
            self.command_compute_resection,
            add_to_menu=False,
            add_to_toolbar=False,
        )
        self.sub_menu.addAction(self.action_compute_resection)

        self.action_compute_trilateration = self._add_action(
            tr("Trilateration"),
            self.command_compute_trilateration,
            add_to_menu=False,
            add_to_toolbar=False,
        )
        self.sub_menu.addAction(self.action_compute_trilateration)

        self.action_compute_traverse = self._add_action(
            tr("Traverse"),
            self.command_compute_traverse,
            add_to_menu=False,
            add_to_toolbar=False,
        )
        self.sub_menu.addAction(self.action_compute_traverse)

        self.action_compute_v0 = self._add_action(
            tr("Compute V0"),
            self.command_compute_v0,
            add_to_menu=False,
            add_to_toolbar=False,
        )
        self.sub_menu.addAction(self.action_compute_v0)

        self.action_compute_points = self._add_action(
            tr("Compute points"),
            self.command_compute_points,
            ":/plugins/topaze/resources/images/topaze.png",
        )

        self.action_process_field_codification = self._add_action(
            tr("Process field codification"),
            self.command_process_field_codification,
            ":/images/themes/default/console/iconFormatCode.svg",
        )

        self.action_purge_observations = self._add_action(
            tr("Purge observations"),
            self.command_purge_observations,
            ":/images/themes/default/mActionRemoveSelectedFeature.svg",
        )

        # -- Menu
        self.dlg_import_points.button_box.button(QDialogButtonBox.Open).clicked.connect(
            self.button_box_import_points_clicked
        )
        self.dlg_edit_obs.pushButton_add.clicked.connect(
            self.on_push_button_add_obs_clicked
        )
        self.dlg_edit_obs.pushButton_change.clicked.connect(
            self.on_push_button_change_obs_clicked
        )
        self.dlg_edit_obs.pushButton_delete.clicked.connect(
            self.on_push_button_delete_obs_clicked
        )
        self.dlg_edit_obs.pushButton_export_lsci.clicked.connect(
            self.on_push_button_export_lsci_clicked
        )
        self.dlg_edit_obs.pushButton_save.clicked.connect(
            self.on_push_button_save_obs_data_clicked
        )
        self.dlg_edit_obs.pushButton_print.clicked.connect(
            self.on_push_button_print_selected_obs_clicked
        )
        self.dlg_edit_obs.pushButton_cancel.clicked.connect(
            self.on_push_button_cancel_edition_clicked
        )
        self.dlg_edit_obs.closeEvent = self.on_edit_observations_close

        self.action_helmert = self._add_action(
            tr("Helmert transformation"),
            self.helmert_transformation,
            ":/images/themes/default/transformation.svg",
        )

        self.action_help = self._add_action(
            tr("{} - Documentation").format(__title__),
            partial(QDesktopServices.openUrl, QUrl(__uri_homepage__)),
            ":/images/themes/default/mActionHelpContents.svg",
            add_to_toolbar=False,
        )

        self.action_about = self._add_action(
            tr("{} - About").format(__title__),
            self.show_help_about,
            ":/images/themes/default/mActionHelpAbout.svg",
            add_to_toolbar=False,
        )

        self.action_settings = self._add_action(
            tr("Settings"),
            lambda: self.iface.showOptionsDialog(
                currentPage="mOptionsPage{}".format(__title__)
            ),
            QgsApplication.getThemeIcon("console/iconSettingsConsole.svg"),
            add_to_toolbar=False,
        )

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

        # Unregister settings page
        if getattr(self, "options_factory", None) is not None:
            with suppress(Exception):
                self.iface.unregisterOptionsWidgetFactory(self.options_factory)

            self.options_factory = None

        with suppress(Exception):
            QgsProject.instance().aboutToBeCleared.disconnect(
                self.on_project_about_to_be_closed
            )

        # Remove submenu from Plugins > Topaze
        if getattr(self, "sub_menu", None) is not None:
            with suppress(Exception):
                self.iface.removePluginMenu(__title__, self.sub_menu.menuAction())

            with suppress(Exception):
                self.sub_menu.clear()
                self.sub_menu.deleteLater()

            self.sub_menu = None

        # Remove regular actions from Plugins > Topaze and from the plugins toolbar
        for action in list(self.actions):
            with suppress(Exception):
                self.iface.removePluginMenu(__title__, action)

            with suppress(Exception):
                self.iface.removeToolBarIcon(action)

            with suppress(Exception):
                action.deleteLater()

        self.actions.clear()

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

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

    def show_help_about(self):
        dialog = TopazeAbout()
        dialog.exec_()

    def close_project_dialogs(self):
        """Close all plugin dialogs when the QGIS project is closed."""

        for attr_name in list(vars(self)):
            if not attr_name.startswith("dlg_"):
                continue

            widget = getattr(self, attr_name, None)

            if widget is None:
                continue

            if not isinstance(widget, QWidget):
                continue

            try:
                if widget.isVisible():
                    widget.hide()

            except RuntimeError:
                # Qt object already deleted
                pass

    def on_project_about_to_be_closed(self):
        """Called before the current QGIS project is cleared."""
        # print("on_project_about_to_be_closed")
        self.close_project_dialogs()
        self._ptopo_array = []
        self._ptopo_layer = None
        self.work_on_survey = False
        self._observations_layer = None

    def on_topaze_project_read(self):
        # print("on_topaze_project_read")
        self._ptopo_array = []
        self._ptopo_layer = TopazeLayers.ptopo_layer()
        if self._ptopo_layer:
            self.work_on_survey = True
        else:
            self.iface.messageBar().pushMessage(
                tr("PTOPO table missing"), "", Qgis.Info
            )
            self.work_on_survey = False
        self._observations_layer = TopazeLayers.field_layer()
        """
        if not self._observations_layer:
            self.iface.messageBar().pushMessage(
                tr("Observations table missing"),
                "",
                Qgis.Info,
            )
            self.work_on_survey = False
        """
        self.dlg_helmert_canvas = AdditionalCanvasDialog(self.iface)
        self.dlg_helmert_canvas.setWindowFlags(Qt.Window)  # Non-modal

    def helmert_transformation(self):
        """Helmert transformation"""
        # print("helmert_transformation")
        # self.dlg_helmert.showDialog()
        # results = compute_helmert_transformation()
        # print("results:", results)
        # Créer ou vider les couches temporaires "coordinates"
        if not self.dlg_helmert_canvas:
            self.dlg_helmert_canvas = AdditionalCanvasDialog(self.iface)
            self.dlg_helmert_canvas.setWindowFlags(Qt.Window)  # Non-modal
        self.helmert_group = HelmertGroup()
        self.dlg_helmert_canvas.set_helmert_group(self.helmert_group)
        self.dlg_helmert_canvas.show()

    def create_points(self, refresh: bool = True):
        """Create points"""
        # (print("create_points")
        try:
            nb_created = nb_updated = 0
            features = []
            config = TopazeConfig.config_dict()
            tol_xy = config["duplicated"]["xy_treatment"]["dup_tolerance_xy"]
            tol_z = config["duplicated"]["z_treatment"]["dup_tolerance_z"]
            self._ptopo_layer.startEditing()
            for ptopo in self._ptopo_array:
                feature = None
                if TopazeUtils.ptopo_exists_in_layer(
                    ptopo.matricule, self._ptopo_layer
                ):
                    feature = TopazeUtils.get_feature_by_matricule(
                        ptopo.matricule, ptopo_layer=self._ptopo_layer
                    )
                    old_ptopo = Ptopo.from_feature(feature)
                    x, y, z = TopazeUtils.process_duplicated(
                        old_ptopo, ptopo, tol_xy, tol_z
                    )
                    ptopo.x, ptopo.y, ptopo.z = x, y, z
                if feature:
                    Ptopo.to_feature(ptopo, feature)
                    ok = self._ptopo_layer.updateFeature(feature)
                    nb_updated = nb_updated + 1
                else:
                    if ptopo.z is not None:
                        wkt = f"Point({ptopo.x} {ptopo.y} {ptopo.z})"
                    else:
                        wkt = f"Point({ptopo.x} {ptopo.y} {PtopoConst.UNKNOWN_Z})"
                    geom = QgsGeometry.fromWkt(wkt)
                    feature_dict = {}
                    i = self._ptopo_layer.fields().indexFromName("matricule")
                    if i >= 0:
                        feature_dict.update({i: ptopo.matricule})
                    i = self._ptopo_layer.fields().indexFromName("type_pt")
                    if i >= 0:
                        feature_dict.update({i: ptopo.type})
                    i = self._ptopo_layer.fields().indexFromName("prec_xy")
                    if i >= 0:
                        feature_dict.update({i: ptopo.prec_xy})
                    i = self._ptopo_layer.fields().indexFromName("prec_z")
                    if i >= 0:
                        feature_dict.update({i: ptopo.prec_z})
                    i = self._ptopo_layer.fields().indexFromName("codes")
                    if i >= 0:
                        if ptopo.codes:
                            feature_dict.update({i: ptopo.codes})

                    feature = QgsVectorLayerUtils.createFeature(
                        self._ptopo_layer, geom, feature_dict
                    )
                    features.append(feature)
                    nb_created = nb_created + 1

            self._ptopo_layer.commitChanges()
            if len(features) > 0:
                self._ptopo_layer.dataProvider().addFeatures(features)

            if refresh:
                # If caching is enabled, a simple canvas refresh might not be sufficient
                # to trigger a redraw and you must clear the cached image for the layer
                if self.iface.mapCanvas().isCachingEnabled():
                    self._ptopo_layer.triggerRepaint()
                else:
                    self.iface.mapCanvas().refresh()

        except Exception as e:
            self.iface.messageBar().pushMessage(
                tr("Unable to add points"),
                str(e),
                Qgis.Warning,
            )

        else:
            return nb_created, nb_updated

    def compute_details_from_station(
        self, station, station_index, obs_array, prec_xy, prec_z
    ):
        if station.x is None or station.y is None:
            pt = TopazeUtils.get_ptopo_by_matricule(
                station.matricule, self._ptopo_array, self._ptopo_layer
            )
            try:
                station.x = pt.x
                station.y = pt.y
                station.z = pt.z
            except Exception as e:
                self.iface.messageBar().pushMessage(
                    tr("Undetermined station"),
                    "Import/input coordinates, please",
                    Qgis.MessageLevel.Critical,
                )
                return

        references, end_index = TopazeUtils.find_references_in_obs_array(
            obs_array, station_index + 1
        )
        TopazeUtils.get_coordinates_from_references(
            references, self._ptopo_array, self._ptopo_layer
        )
        if station.v0 is None:
            v0 = TopazeCalculator.compute_bearing_pq(station, references)
            station.v0 = v0
        if station.v0 is None:
            station.v0 = 0.0
        next_idx = station_index + 1
        to_compute = True
        config = TopazeConfig.config_dict()
        tol_xy = config["duplicated"]["xy_treatment"]["dup_tolerance_xy"]
        tol_z = config["duplicated"]["z_treatment"]["dup_tolerance_z"]
        for obs in obs_array[station_index + 1 : end_index]:
            next_idx = next_idx + 1
            if obs.type == "st":
                to_compute = False
                break
            elif obs.type in ["pnt", "xyz"]:
                x, y, z = TopazeCalculator.obs_to_xyz(station, obs)
                if x is not None and y is not None:
                    ptopo = Ptopo(
                        obs.matricule, "P", x, y, z, obs.codes, prec_xy, prec_z
                    )
                    try:
                        if obs_array[next_idx].type == "code":
                            ptopo.codes = obs_array[next_idx].codes
                    except Exception:
                        pass
                    if TopazeUtils.ptopo_exists_in_array(
                        ptopo.matricule, self._ptopo_array
                    ):
                        old_ptopo = TopazeUtils.get_ptopo_by_matricule(
                            ptopo.matricule, ptopo_array=self._ptopo_array
                        )
                        x, y, z = TopazeUtils.process_duplicated(
                            old_ptopo, ptopo, tol_xy, tol_z
                        )
                        status = TopazeUtils.update_xyz_in_array(
                            self._ptopo_array, ptopo.matricule, x, y, z
                        )
                        if status:
                            print(f"Ptopo {ptopo.matricule} updated in array")
                        else:
                            ptopo.x, ptopo.y, ptopo.z = x, y, z
                            self._ptopo_array.append(ptopo)
                    else:
                        self._ptopo_array.append(ptopo)
                        print(f"Ptopo {ptopo.matricule} added in array")

    def compute_points(self, type: str = "P", prec_xy: str = "A", prec_z: str = "A"):
        """Compute surveyed points"""
        # (print("compute_points")

        try:
            station = None

            nb_stations = 0
            station_index = -1
            while True:
                station, station_index = TopazeUtils.find_station_in_obs_array(
                    self.obs_array, station_index + 1
                )
                if station is None:
                    if nb_stations < 1:
                        self.iface.messageBar().pushMessage(
                            tr("No station found "),
                            tr("Check/modify observations"),
                            Qgis.MessageLevel.Critical,
                        )
                        return
                    break
                nb_stations += 1
                self.compute_details_from_station(
                    station, station_index, self.obs_array, prec_xy, prec_z
                )

            n_cre, n_upd = self.create_points()
            self.iface.messageBar().pushMessage(
                tr("{} surveyed point(s) computed").format(len(self._ptopo_array)),
                tr(" ({} created, {} updated)").format(n_cre, n_upd),
                Qgis.Info,
            )
        except Exception as e:
            self.iface.messageBar().pushMessage(
                tr("Error computing points"),
                str(e),
                Qgis.MessageLevel.Critical,
            )
        ...

    def command_compute_points(self):
        """Compute points"""
        if self.work_on_survey:
            self.obs_array = TopazeUtils.load_obs_array(self._obs_storage_mode)
            self.compute_points()
        else:
            PlgLogger.log(
                tr("Points can only be computed in a survey project"),
                log_level=2,
                push=True,
            )

    def command_compute_free_station(self):
        """Compute free_station"""
        if self.work_on_survey:
            self.obs_array = TopazeUtils.load_obs_array(self._obs_storage_mode)
            self.dlg_free_station.showDialog(
                self.obs_array, self._ptopo_array, self._ptopo_layer
            )
        else:
            PlgLogger.log(
                tr("Free station can only be computed in a survey project"),
                log_level=2,
                push=True,
            )
        # results = compute_free_station()
        # print("results:", results)

    def command_compute_intersection(self):
        """Compute intersection"""
        if self.work_on_survey:
            self.obs_array = TopazeUtils.load_obs_array(self._obs_storage_mode)
            self.dlg_intersection.showDialog(
                self.obs_array, self._ptopo_array, self._ptopo_layer
            )
        else:
            PlgLogger.log(
                tr("Intersection can only be computed in a survey project"),
                log_level=2,
                push=True,
            )

    def command_compute_resection(self):
        """Compute resection"""
        if self.work_on_survey:
            self.obs_array = TopazeUtils.load_obs_array(self._obs_storage_mode)
            self.dlg_resection.showDialog(
                self.obs_array, self._ptopo_array, self._ptopo_layer
            )
        else:
            PlgLogger.log(
                tr("Resection can only be computed in a survey project"),
                log_level=2,
                push=True,
            )

    def command_compute_trilateration(self):
        """Compute trilateration"""
        if self.work_on_survey:
            self.obs_array = TopazeUtils.load_obs_array(self._obs_storage_mode)
            self.dlg_trilateration.showDialog(
                self.obs_array, self._ptopo_array, self._ptopo_layer
            )
        else:
            PlgLogger.log(
                tr("Trilateration can only be computed in a survey project"),
                log_level=2,
                push=True,
            )

    def command_compute_traverse(self):
        """Compute traverse"""
        if self.work_on_survey:
            self.obs_array = TopazeUtils.load_obs_array(self._obs_storage_mode)
            self.dlg_traverse.showDialog(
                self.obs_array, self._ptopo_array, self._ptopo_layer
            )
        else:
            PlgLogger.log(
                tr("Traverse can only be computed in a survey project"),
                log_level=2,
                push=True,
            )

    def command_compute_v0(self):
        """Compute the orientation V0 of a station occurrence."""
        if self.work_on_survey:
            self.obs_array = TopazeUtils.load_obs_array(self._obs_storage_mode)

            # Lazy creation keeps this snippet self-contained: no additional
            # constructor change is required in plugin_main.py.
            if not hasattr(self, "dlg_compute_v0") or self.dlg_compute_v0 is None:
                from topaze.gui.compute_v0_dialog import ComputeV0Dialog

                self.dlg_compute_v0 = ComputeV0Dialog()
                self.dlg_compute_v0.set_iface(self.iface)

            self.dlg_compute_v0.showDialog(
                self.obs_array,
                self._ptopo_array,
                self._ptopo_layer,
                get_observations_callback=self.get_observations,
                set_observations_callback=self.set_observations,
                storage_mode=self._obs_storage_mode,
            )
        else:
            PlgLogger.log(
                tr("Station V0 can only be computed in a survey project"),
                log_level=2,
                push=True,
            )

    def command_new_survey(self):
        """Open dialog New survey"""
        # (print("new_survey_dialog")
        self.dlg_new_survey.set_initial_dir(
            str(os.path.dirname(QgsProject.instance().homePath()))
        )
        result = self.dlg_new_survey.exec()
        if result == QDialog.Accepted:
            to_path = self.dlg_new_survey.get_survey_dir()
            self.new_survey(to_path)

    def new_survey(self, to_path: str):
        """Create new survey by copying template directory"""
        # (print("new_survey")
        try:
            if os.path.exists(to_path):
                m1 = tr("Creating new survey in :")
                m2 = tr("Do you want to override existing directory ?")
                msg = m1 + f"\n\n{to_path}\n\n" + m2
                if DlgUtil.show_alert(
                    tr("Create new survey"),
                    msg,
                    QMessageBox.StandardButton.Save,
                    QMessageBox.StandardButton.Cancel,
                ):
                    shutil.rmtree(to_path)
                else:
                    return
            from_path = DIR_PLUGIN_ROOT / "resources/templates/surveying"
            shutil.copytree(from_path, to_path)
            old_qgs = os.path.join(to_path, "surveying.qgs")
            new_qgs = os.path.join(
                to_path, os.path.basename(os.path.normpath(to_path)) + ".qgs"
            )
            os.rename(old_qgs, new_qgs)

        except Exception as e:
            self.iface.messageBar().pushMessage(
                tr("Unable to create new survey "),
                tr(" in {}").format(to_path),
                Qgis.MessageLevel.Critical,
            )
        else:
            self.iface.messageBar().pushMessage(
                tr("New survey created "),
                tr(" in {}").format(to_path),
                Qgis.Success,
            )
            self.iface.projectRead.connect(self.on_topaze_project_read)
            QgsProject.instance().read(new_qgs)
            self.on_topaze_project_read()

    def command_update_observations(self):
        """Open dialog Update observations"""
        # (print("command_update_observations")
        if self.work_on_survey:
            self.dlg_import_field_data.exec()
            if len(self.dlg_import_field_data.lineEdit.text()) > 0:
                self.update_observations(self.dlg_import_field_data.lineEdit.text())
        else:
            PlgLogger.log(
                tr("Observations can only be updated in a survey project"),
                log_level=2,
                push=True,
            )

    def update_observations(self, fullfilepath: str):
        """Update 'observations' table from text file"""
        # (print("update_observations")
        # open .obs file read fied data
        try:
            field_data_type = TopazeUtils.get_field_data_type(fullfilepath)
            if field_data_type not in ["AZIMUT", "TRIMBLE", "LEICA", "landxml"]:
                self.iface.messageBar().pushMessage(
                    tr("Unable to detect format from {}").format(fullfilepath),
                    "",
                    Qgis.MessageLevel.Critical,
                )
                return

            if field_data_type == "AZIMUT":
                new_survey = self.get_observations_from_obsfile(fullfilepath)
            else:
                try:
                    inputclass = BUILTIN_INPUT_FORMATS[field_data_type]
                except KeyError as e:
                    self.iface.messageBar().pushMessage(
                        tr("{} is not a valid input format").format(field_data_type),
                        "",
                        Qgis.MessageLevel.Critical,
                    )
                    return
                else:
                    if isinstance(inputclass, tuple):
                        try:
                            # builtin format parser
                            mod, cls, name = inputclass
                            format_parser = getattr(
                                importlib.import_module(
                                    f"topaze.totalopenstation.formats.{mod}"
                                ),
                                cls,
                            )
                        except ImportError as e:
                            self.iface.messageBar().pushMessage(
                                tr("Error importing {} from {}").format(cls, mod),
                                str(e),
                                Qgis.MessageLevel.Critical,
                            )
                            return

                if field_data_type == "landxml":
                    # Let the XML parser honour the encoding declared by the
                    # document instead of decoding it as a generic text file.
                    with open(fullfilepath, "rb") as field_file:
                        field_data = field_file.read()
                else:
                    with open(
                        fullfilepath,
                        "r",
                        encoding=TopazeUtils.find_file_encoding(fullfilepath),
                    ) as field_file:
                        field_data = field_file.read()

                fp = format_parser(field_data, point_starts_on="5", use_all=True)
                rl = fp.raw_line

                of = OutputFormat(rl)
                new_survey = of.process()

        except Exception as e:
            self.iface.messageBar().pushMessage(
                tr("Unable to open {}").format(fullfilepath),
                str(e),
                Qgis.Warning,
            )

        else:
            try:
                if self._observations_layer or self._obs_storage_mode == "file":
                    db_field_data = ""
                    db_field_data = self.get_observations(mode=self._obs_storage_mode)
                    new_survey += "\n"
                    if len(db_field_data):
                        new_field_data = db_field_data + new_survey
                    else:
                        new_field_data = new_survey
                    self.set_observations(new_field_data, mode=self._obs_storage_mode)

            except Exception as e:
                self.iface.messageBar().pushMessage(
                    tr("Unable to update observations with field data"),
                    str(e),
                    Qgis.Warning,
                )
            else:
                self.iface.messageBar().pushMessage(
                    tr("Observations updated"),
                    tr(" with field data from {}").format(fullfilepath),
                    Qgis.Success,
                )

    def command_import_points(self):
        """Open dialog Import points"""
        if self.work_on_survey:
            self.dlg_import_points.show()
        else:
            PlgLogger.log(
                tr("Points can only be imported in a survey project"),
                log_level=2,
                push=True,
            )

    def import_points(self, fullfilepath: str):
        """Import points  from text file"""
        try:
            if fullfilepath.lower().endswith(".jsi") or fullfilepath.lower().endswith(
                ".txt"
            ):
                from topaze.totalopenstation.formats.topstation_jsi import FormatParser
            with open(
                fullfilepath, "r", encoding=TopazeUtils.find_file_encoding(fullfilepath)
            ) as pointsfile:
                fp = FormatParser(pointsfile.read(), ptopo_only=True)

        except Exception as e:
            self.iface.messageBar().pushMessage(
                tr("Unable to open {}").format(fullfilepath),
                str(e),
                Qgis.Info,
            )

        else:
            # get points data
            try:
                rl = fp.raw_line
                from topaze.totalopenstation.output.tops_ptopo import OutputFormat

                of = OutputFormat(rl)
                self._ptopo_array.clear
                self._ptopo_array = of.process()
                nb = len(self._ptopo_array)
                n_cre, n_upd = self.create_points()
            except Exception as e:
                self.iface.messageBar().pushMessage(
                    tr("Unable to import points from {}").format(fullfilepath),
                    str(e),
                    Qgis.Warning,
                )
            else:
                self.iface.messageBar().pushMessage(
                    tr("{} point(s) imported from {}").format(nb, fullfilepath),
                    tr(" ({} created, {} updated)").format(n_cre, n_upd),
                    Qgis.Success,
                )

    def get_observations(self, mode: str = "db"):
        """Get observations from .obs file or from 'observations' table in DB

        :param mode: 'db' to get observations from 'observations' table, \
        'file' to get observations from .obs file, defaults to "db"
        :type mode: str, optional
        :param fullfilepath: full path to .obs file, defaults to ""
        :type fullfilepath: str, optional
        :return: observations data
        :rtype: str
        """
        field_data = ""
        if mode == "db":
            field_data = self.get_observations_from_db()
        elif mode == "file":
            fullfilepath = (
                QgsProject.instance().homePath()
                + "/db/"
                + QgsProject.instance().baseName()
                + ".obs"
            )
            field_data = self.get_observations_from_obsfile(fullfilepath)
        if not field_data:
            field_data = ""
        return field_data

    def get_observations_from_obsfile(self, fullfilepath: str) -> str:
        """Get observations from .obs file

        :param fullfilepath: _description_
        :type fullfilepath: str
        :return: _description_
        :rtype: _type_
        """
        try:
            with open(
                fullfilepath, "r", encoding=TopazeUtils.find_file_encoding(fullfilepath)
            ) as field_file:
                field_data = field_file.read()

        except Exception as e:
            s = str(e)
            return None

        else:
            return field_data

    def get_observations_from_db(self):
        """_summary_

        :param returns_rows: _description_, defaults to True
        :type returns_rows: bool, optional
        :return: _description_
        :rtype: _type_
        """
        try:
            field_data = ""
            if self._observations_layer:
                feature_dict = {}
                idx_obs_column = self._observations_layer.fields().indexFromName("obs")
                obs_iter = self._observations_layer.getFeatures()
                if obs_iter.isValid():
                    feature = QgsFeature()
                    if obs_iter.nextFeature(feature):
                        if idx_obs_column >= 0:
                            field_data = feature.attribute("obs")
                            if field_data == "\n":
                                field_data = ""

        except Exception as e:
            self.iface.messageBar().pushMessage(
                tr("Unable to get field data"),
                str(e),
                Qgis.Info,
            )
            return None

        else:
            return field_data

    def set_observations(self, new_data, mode: str = "db"):
        """Set observations in .obs file or in 'observations' table in DB

        :param new_data: observations data to set
        :type new_data: str
        :param mode: 'db' to set observations in 'observations' table, \
        'file' to set observations in .obs file, defaults to "db"
        :type mode: str, optional
        :param fullfilepath: full path to .obs file, defaults to ""
        :type fullfilepath: str, optional
        """
        if mode == "db":
            old_data = self.get_observations(mode=self._obs_storage_mode)
            self.set_observations_in_db(new_data, old_data)
        elif mode == "file":
            fullfilepath = (
                QgsProject.instance().homePath()
                + "/db/"
                + QgsProject.instance().baseName()
                + ".obs"
            )
            self.set_observations_in_obsfile(new_data, fullfilepath)

    def set_observations_in_obsfile(self, new_data, fullfilepath):
        try:
            with open(fullfilepath, "w", encoding="utf-8") as field_file:
                field_file.write(new_data)
        except Exception as e:
            self.iface.messageBar().pushMessage(
                tr("Unable to set field observations in {}").format(fullfilepath),
                str(e),
                Qgis.MessageLevel.Critical,
            )

    def set_observations_in_db(self, new_data, old_data):
        try:
            idx_obs_column = self._observations_layer.fields().indexFromName("obs")
            obs_iter = self._observations_layer.getFeatures()
            if obs_iter.isValid():
                feature = QgsFeature()
                if obs_iter.nextFeature(feature):
                    if idx_obs_column >= 0:
                        self._observations_layer.startEditing()
                        self._observations_layer.changeAttributeValue(
                            feature.id(), idx_obs_column, new_data, old_data
                        )
                        self._observations_layer.commitChanges()
        except Exception as e:
            self.iface.messageBar().pushMessage(
                tr("Unable to set field observations in table"),
                "",
                Qgis.MessageLevel.Critical,
            )

    def command_edit_observations(self):
        """Load observations edition dialog"""
        # (print("command_edit_observations")
        if not self.work_on_survey:
            PlgLogger.log(
                tr("Observations can only be edited in a survey project"),
                log_level=2,
                push=True,
            )
            return

        self.dlg_edit_obs.reset_widgets()
        self.dlg_edit_obs._field_rows = []
        self.dlg_edit_obs._filter = "all"
        field_data = self.get_observations(mode=self._obs_storage_mode)
        if field_data is None:
            field_data = ""
        self.dlg_edit_obs.load_observations(field_data.splitlines())

        self.dlg_edit_obs._obs_edited = False
        self.dlg_edit_obs.pushButton_save.setEnabled(False)
        self.dlg_edit_obs.show()

        # self.dlg_edit_obs.comboBox_type_obs.setCurrentIndex(0)

        self.dlg_edit_obs.listWidget_observations.currentItemChanged.connect(
            self.on_list_obs_current_item_changed
        )

        self.dlg_edit_obs.comboBox_type_obs.currentTextChanged.connect(
            self.on_combo_type_obs_current_text_changed
        )

    def command_process_field_codification(self):
        print("TODO : command_process_field_codification")
        ...

    def command_purge_observations(self):
        """Purge observations from .obs file or from 'observations' table in DB"""
        # (print("command_purge_observations")
        if self._obs_storage_mode == "file":
            fullfilepath = (
                QgsProject.instance().homePath()
                + "/db/"
                + QgsProject.instance().baseName()
                + ".obs"
            )
            if not os.path.exists(fullfilepath):
                DlgUtil.show_message_box(tr("No .obs file to remove"))
                return
        msg = tr("Do you actually want to purge observations")
        if DlgUtil.show_alert(tr("Purge observations"), msg):
            if self._obs_storage_mode == "db":
                self.purge_observations_in_db()
            elif self._obs_storage_mode == "file":
                self.purge_observations_in_obsfile(fullfilepath)

    def purge_observations_in_obsfile(self, fullfilepath):
        """Remove all observations from .obs file"""
        # (print("command_purge_observations in obsfile")
        try:
            fullfilepath = (
                QgsProject.instance().homePath()
                + "/db/"
                + QgsProject.instance().baseName()
                + ".obs"
            )
            if os.path.exists(fullfilepath):
                os.remove(fullfilepath)

        except Exception as e:
            self.iface.messageBar().pushMessage(
                tr("Unable to remove {}").format(fullfilepath),
                str(e),
                Qgis.Warning,
            )

        else:
            self.dlg_edit_obs._obs_edited = False
            self.dlg_edit_obs.pushButton_save.setEnabled(False)
            self.iface.messageBar().pushMessage(
                tr("Observations erased"), "", Qgis.Info
            )

    def purge_observations_in_db(self):
        """Remove all observations"""
        # (print("command_purge_observations in db")
        try:
            db_field_data = ""
            if self._observations_layer:
                idx_obs_column = self._observations_layer.fields().indexFromName("obs")
                obs_iter = self._observations_layer.getFeatures()
                if obs_iter.isValid():
                    feature = QgsFeature()
                    if obs_iter.nextFeature(feature):
                        if idx_obs_column >= 0:
                            db_field_data = feature.attribute("obs")
                            new_field_data = ""
                            self._observations_layer.startEditing()
                            self._observations_layer.changeAttributeValue(
                                feature.id(),
                                idx_obs_column,
                                new_field_data,
                                db_field_data,
                            )
                            self._observations_layer.commitChanges()

        except Exception as e:
            self.iface.messageBar().pushMessage(
                tr("Unable to erase observations"),
                str(e),
                Qgis.Warning,
            )

        else:
            self.dlg_edit_obs._obs_edited = False
            self.dlg_edit_obs.pushButton_save.setEnabled(False)
            self.iface.messageBar().pushMessage(
                tr("Observations erased"), "", Qgis.Info
            )

    def button_box_ok_new_survey_clicked(self):
        self.new_survey(self.dlg_new_survey.lineEdit.text())

    def button_box_save_field_data_clicked(self):
        if len(self.dlg_import_field_data.lineEdit.text()) > 0:
            self.update_observations(self.dlg_import_field_data.lineEdit.text())

    def button_box_import_points_clicked(self):
        if len(
            self.dlg_import_points.lineEdit.text()
        ) > 0 and self.dlg_import_points.lineEdit.text()[-4:].lower() in [
            ".csv",
            ".jsi",
            ".txt",
        ]:
            self.import_points(self.dlg_import_points.lineEdit.text())

    def on_push_button_add_obs_clicked(self):
        """add new observation"""
        try:
            self.dlg_edit_obs.pushButton_add.clicked.disconnect()
        except TypeError:
            pass
        dico = self.dlg_edit_obs.observation_fields_to_dict(
            self.dlg_edit_obs.comboBox_type_obs.currentText(),
            self.dlg_edit_obs.comboBox_type_point.currentText(),
        )
        rows = TopazeUtils.dict_to_obs_str(dico)
        if len(rows) == 1:
            try:
                idx = self.dlg_edit_obs.listWidget_observations.currentIndex().row()
                if idx == self.dlg_edit_obs.listWidget_observations.count() - 1:
                    self.dlg_edit_obs.listWidget_observations.addItem(rows[0])
                else:
                    self.dlg_edit_obs.listWidget_observations.insertItem(
                        idx + 1, rows[0]
                    )
            except Exception as e:
                self.iface.messageBar().pushMessage(
                    tr("Unable to add observation"),
                    str(e),
                    Qgis.Warning,
                )
            else:
                self.dlg_edit_obs.listWidget_observations.setCurrentRow(idx + 1)
        elif len(rows) > 1:
            ...
        else:
            ...
        ...
        self.dlg_edit_obs._obs_edited = True
        self.dlg_edit_obs.pushButton_save.setEnabled(True)
        self.dlg_edit_obs.pushButton_add.clicked.connect(
            self.on_push_button_add_obs_clicked
        )

    def on_push_button_change_obs_clicked(self):
        """change observation"""
        try:
            self.dlg_edit_obs.pushButton_change.clicked.disconnect()
        except TypeError:
            pass
        dico = self.dlg_edit_obs.observation_fields_to_dict(
            self.dlg_edit_obs.comboBox_type_obs.currentText(),
            self.dlg_edit_obs.comboBox_type_point.currentText(),
        )
        rows = TopazeUtils.dict_to_obs_str(dico)
        if len(rows) == 1:
            if self._current_obs is not None:
                self._current_obs.setText(rows[0])
        elif len(rows) > 1:
            ...
        else:
            ...
        self.dlg_edit_obs._obs_edited = True
        self.dlg_edit_obs.pushButton_save.setEnabled(True)
        self.dlg_edit_obs.pushButton_change.clicked.connect(
            self.on_push_button_change_obs_clicked
        )

    def on_push_button_delete_obs_clicked(self):
        """delete observation"""
        try:
            self.dlg_edit_obs.pushButton_delete.clicked.disconnect()
        except TypeError:
            pass
        current_row = self.dlg_edit_obs.listWidget_observations.currentRow()
        item = self.dlg_edit_obs.listWidget_observations.takeItem(current_row)
        self.dlg_edit_obs._obs_edited = True
        self.dlg_edit_obs.pushButton_save.setEnabled(True)
        self.dlg_edit_obs.pushButton_delete.clicked.connect(
            self.on_push_button_delete_obs_clicked
        )

    def on_push_button_export_lsci_clicked(self):
        """export codification to csv for LSCI"""
        # (print("export codification for lsci")
        # TODO: valider analyse et écrire csv
        try:
            points = []
            for row in self.dlg_edit_obs._field_rows:
                type = TopazeUtils.type_obs(row)
                if type == "pnt" and len(points) < 1:
                    code = []
                    nb = 0
                    csv_line = row[4:]
                elif type == "code" and len(points):
                    codes = []  # TODO: parse(row)
                    for code in codes:
                        nb = 1
                elif type == "pnt" and len(points) > 0:
                    pass
        except Exception as e:
            print(str(e))

    def on_push_button_save_obs_data_clicked(self):
        """save observations data"""
        if not self.dlg_edit_obs.pushButton_save.isEnabled():
            return
        try:
            new_data = ""
            rows = [
                self.dlg_edit_obs.listWidget_observations.item(x).text()
                for x in range(0, self.dlg_edit_obs.listWidget_observations.count())
            ]
            if len(rows) > 0:
                new_data = "\n".join(rows)
                new_data += "\n"
            self.set_observations(new_data, mode=self._obs_storage_mode)
        except Exception as e:
            self.iface.messageBar().pushMessage(
                tr("Unable to save field observations"),
                str(e),
                Qgis.Warning,
            )
        else:
            self.iface.messageBar().pushMessage(
                tr("Field observations data saved"),
                "",
                Qgis.Success,
            )
            self.dlg_edit_obs._obs_edited = False
            self.dlg_edit_obs._field_rows = None
            field_data = self.get_observations(mode=self._obs_storage_mode)
            self.dlg_edit_obs.load_observations(
                field_data.splitlines(), self.dlg_edit_obs._filter
            )
        self.dlg_edit_obs.pushButton_save.setEnabled(False)

    def on_push_button_print_selected_obs_clicked(self):
        """TODO : print selected observations"""
        ...

    def on_edit_observations_close(self, result):
        self.on_push_button_cancel_edition_clicked()

    def on_push_button_cancel_edition_clicked(self):
        """cancel changes and close dialog"""
        if self.dlg_edit_obs._obs_edited:
            if DlgUtil.show_alert(
                tr("Edit observations"),
                tr(
                    "Do you want to save or cancel and drop your changes ?",
                ),
                QMessageBox.StandardButton.Save,
                QMessageBox.StandardButton.Cancel,
            ):
                self.on_push_button_save_obs_data_clicked()
        self.dlg_edit_obs._obs_edited = False
        self.dlg_edit_obs.hide()

    def on_list_obs_current_item_changed(self, current, previous):
        """process current row changed"""
        self._current_obs = current
        self._previous_obs = previous
        if current is not None:
            type_obs, type_pt = TopazeUtils.types_obs(current.text())
            # if type_pt is not None:
            #     print(f"type_obs {type_obs}, type_pt {type_pt}")
            # else:
            #     print(f"type_obs {type_obs}")
            self.dlg_edit_obs.update_combobox_types(type_obs, type_pt)
            self.dlg_edit_obs.load_observation_fields(
                type_obs, TopazeUtils.obs_str_to_dict(current.text())
            )

    def on_combo_type_obs_current_text_changed(self, type_obs):
        """process curent type obs changed"""
        self.dlg_edit_obs.update_state_of_input_fields(type_obs)
