"""
/***************************************************************************
 FreeStationDialog
                                 A QGIS plugin
 Topaze
                             -------------------
        begin                : 2025-09-29
        git sha              : $Format:%H$
        copyright            : (C) 2025 by Jean-Marie ARSAC
        email                : jmarsac@arsac.wf
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 ***************************************************************************/
"""

import json
import os

from PyQt5 import QtCore, QtWidgets, uic
from qgis.core import Qgis, QgsProject
from qgis.PyQt.QtCore import QDir, QUrl

from topaze.calc.free_station import FreeStation
from topaze.dlg_util import DlgUtil
from topaze.file_utils import FileUtils
from topaze.ptopo import Ptopo, PtopoConst
from topaze.toolbelt import PlgLogger
from topaze.toolbelt.i18n import tr
from topaze.topaze_utils import TopazeUtils
from topaze.topo_sight import TopoSight

FORM_CLASS, _ = uic.loadUiType(
    os.path.join(os.path.dirname(__file__), "free_station_dialog.ui")
)


def _qualified_ref(ref: TopoSight) -> str:
    qual = ref.matricule
    qual = (
        qual + " ("
        if ref.ah is not None or ref.av is not None or ref.di is not None
        else qual
    )
    qual = qual + "h" if ref.ah is not None else qual
    if ref.ah is None:
        qual = qual + "v" if ref.av is not None else qual
    else:
        qual = qual + ",v" if ref.av is not None else qual
    if ref.ah is None and ref.av is None:
        qual = qual + "d" if ref.di is not None else qual
    else:
        qual = qual + ",d" if ref.di is not None else qual
    qual = (
        qual + ")"
        if ref.ah is not None or ref.av is not None or ref.di is not None
        else qual
    )
    return qual


def _unqualified_ref(qual: str) -> str:
    if "(" in qual:
        return qual.split(" (")[0]
    return qual


class FreeStationDialog(QtWidgets.QDialog, FORM_CLASS):
    def __init__(self, parent=None):
        """Constructor."""
        super(FreeStationDialog, self).__init__(parent)
        # Set up the user interface from Designer.
        # After setupUI you can access any designer object by doing
        # self.<objectname>, and you can use autoconnect slots - see
        # http://qt-project.org/doc/qt-4.8/designer-using-a-ui-file.html
        # #widgets-and-dialogs-with-auto-connect
        self.setupUi(self)
        self.setWindowTitle(tr("Compute free station"))
        self.comboBox_compute_station.currentTextChanged.connect(self.load_available)
        self.listWidget_available.currentTextChanged.connect(
            self.display_available_coordinates
        )
        self.listWidget_useful.currentTextChanged.connect(
            self.display_useful_coordinates
        )
        self.pushButton_use.clicked.connect(self.use_it)
        self.pushButton_unuse.clicked.connect(self.unuse_it)
        self.pushButton_use_all.clicked.connect(self.use_all)
        self.pushButton_unuse_all.clicked.connect(self.unuse_all)
        self.pushButton_compute.clicked.connect(self.on_pushbutton_compute_clicked)
        self.pushButton_quit.clicked.connect(self.on_pushbutton_quit_clicked)
        self.pushButton_save.clicked.connect(self.on_pushbutton_save_clicked)
        self.pushButton_solution_1.clicked.connect(
            self.on_pushbutton_solution_1_clicked
        )
        self.pushButton_solution_2.clicked.connect(
            self.on_pushbutton_solution_2_clicked
        )
        self.label_emq.setVisible(False)
        self.lineEdit_emq_x.setVisible(False)
        self.lineEdit_emq_y.setVisible(False)
        self.lineEdit_emq_z.setVisible(False)
        self._obs_array = None
        self.iface = None
        self._ptopo_array = None
        self._ptopo_layer = None
        self.payload = None

    def set_iface(self, iface):
        self.iface = iface

    def showDialog(self, obs_array, ptopo_array=None, ptopo_layer=None):
        self._obs_array = obs_array
        self._ptopo_array = ptopo_array
        self._ptopo_layer = ptopo_layer
        station_matricules = TopazeUtils.find_all_station_matricules_in_obs_array(
            obs_array
        )
        DlgUtil.load_combobox_list_array(
            self.comboBox_compute_station, station_matricules, False
        )
        self.show()

    def on_pushbutton_compute_clicked(self):
        self._pt_sol_1 = None
        self._pt_sol_2 = None
        try:
            to_compute = [self.comboBox_compute_station.currentText()]
            references = []
            if self.listWidget_useful.count() < 2:
                raise RuntimeError(
                    tr(
                        "At least 2 reference points with azimuth and one distance are required."
                    )
                )
            for index in range(self.listWidget_useful.count()):
                item = self.listWidget_useful.item(index)
                references.append(_unqualified_ref(item.text()))
            # print(to_compute)
            # print(references)
            fullfilepath = self.save_data_to_compute_free_station(
                "free_station.json", self._obs_array, to_compute, references
            )
            if not fullfilepath:
                raise RuntimeError(
                    tr("Failed to save data for computing free station.")
                )
            self.clear_xyz_fields_and_disable_solution_buttons()
            self._payload = FreeStation.compute_free_station()
            if self._payload is None:
                raise RuntimeError(tr("Failed to compute free station."))
            if self._payload["result"].get("chosen", None) is not None:
                self._pt_sol_1 = self._payload["result"]["chosen"]
                self._pt_sol_2 = None
            else:
                self._pt_sol_1 = self._payload["result"]["candidates"][0]
                self._pt_sol_2 = self._payload["result"]["candidates"][1]
            if self._pt_sol_1[0] is None and self._pt_sol_1[1] is None:
                raise RuntimeError(tr("Failed to compute free station."))

            if self._pt_sol_1[0] is not None and self._pt_sol_1[1] is not None:
                self.lineEdit_x_sol_1.setText(f"{self._pt_sol_1[0]:>16.4f}")
                self.lineEdit_y_sol_1.setText(f"{self._pt_sol_1[1]:>16.4f}")
                if self._payload["z_block"] is not None:
                    if self._payload["z_block"].get("z_cmp", None) is not None:
                        self.lineEdit_z_sol_1.setText(
                            f"{self._payload['z_block']['z_cmp']:>16.4f}"
                        )
                self.pushButton_solution_1.setEnabled(True)
            if (
                self._pt_sol_2 is not None
                and self._pt_sol_2[0] is not None
                and self._pt_sol_2[1] is not None
            ):
                self.lineEdit_x_sol_2.setText(f"{self._pt_sol_2[0]:>16.4f}")
                self.lineEdit_y_sol_2.setText(f"{self._pt_sol_2[1]:>16.4f}")
                if self._payload["z_block"] is not None:
                    if self._payload["z_block"].get("z_cmp", None) is not None:
                        self.lineEdit_z_sol_2.setText(
                            f"{self._payload['z_block']['z_cmp']:>16.4f}"
                        )
                self.pushButton_solution_2.setEnabled(True)
            else:
                self.lineEdit_x_sol_2.setText("")
                self.lineEdit_y_sol_2.setText("")
                self.lineEdit_z_sol_2.setText("")

            if not self.pushButton_solution_2.isEnabled():
                if len(references) > 2:
                    self.pushButton_solution_1.setText(tr("Refine"))
                    self.pushButton_solution_1.setFocus()
                else:
                    #    self.pushButton_solution_1.setText(tr("Compute Z"))
                    #    self.pushButton_solution_1.setFocus()
                    self.pushButton_solution_1.setEnabled(False)
            target_path = (
                QgsProject.instance().homePath() + "/rapport/free_station_report.pdf"
            )
            msg1 = tr("Free station calculation of ") + to_compute[0]
            msg2 = tr(' report in folder <a href="{}">{}</a>').format(
                QUrl.fromLocalFile(target_path).toString(),
                QDir.toNativeSeparators(target_path),
            )
            PlgLogger.log(msg1 + msg2, log_level=3, push=True)
        except Exception as e:
            PlgLogger.log(str(e), log_level=2, push=True)

    def on_pushbutton_quit_clicked(self):
        if FileUtils.exists_temp_file("free_station.json"):
            FileUtils.remove_temp_file("free_station.json")
        self.hide()

    def on_pushbutton_save_clicked(self):
        station_id = self.comboBox_compute_station.currentText()
        x = (
            float(self.lineEdit_x_sol_2.text())
            if self.lineEdit_x_sol_2.text()
            else float(self.lineEdit_x_sol_1.text())
        )
        y = (
            float(self.lineEdit_y_sol_2.text())
            if self.lineEdit_y_sol_2.text()
            else float(self.lineEdit_y_sol_1.text())
        )
        z = (
            float(self.lineEdit_z_sol_2.text())
            if self.lineEdit_z_sol_2.text()
            else (
                float(self.lineEdit_z_sol_1.text())
                if self.lineEdit_z_sol_1.text()
                else PtopoConst.UNKNOWN_Z
            )
        )
        pt = Ptopo(matricule=station_id, type="R", x=x, y=y, z=z)
        if TopazeUtils.ptopo_exists_in_array(pt.matricule, self._obs_array):
            TopazeUtils.update_xyz_in_array(
                self._obs_array, pt.matricule, pt.x, pt.y, pt.z
            )
        else:
            self._obs_array.append(pt)
        fid, created = TopazeUtils.upsert_ptopo_in_layer(pt, True, self.iface)
        if fid:
            msg1 = tr("Free station")
            if created:
                msg2 = tr("PTOPO #{} ({}) created.").format(str(fid), station_id)
            else:
                msg2 = tr("PTOPO #{} ({}) updated.").format(str(fid), station_id)
            self.iface.messageBar().pushMessage(msg1, msg2, Qgis.Success, 10)

    def on_pushbutton_solution_1_clicked(self):
        if self._payload is not None:
            side = self._payload["result"].get("side", "auto")
            FreeStation.choose_free_station_candidate(
                self._payload, side=side
            )  # ou index=0/1
            # Après choix utilisateur (ex: par bouton “keep LEFT”)
            data_json = FileUtils.load_temp_file("free_station.json")
            obs_data = json.loads(data_json) if data_json else {}

            # === Bloc 3 : LSQ XY + θ sur toutes les visées sélectionnées (angles/distances) ===
            self._payload = FreeStation.refine_free_station_with_lsq_and_report(
                self._payload,
                obs_data,
                use_dirs=True,
                use_dists=True,
                robust=True,
                sigma_dir_gr=0.0003,  # ~3 cc (à adapter à ton instrument)
                sigma_dist_m=0.005,  # ~5 mm
                out_basename="free_station_report",
            )

            # Calculer Z et régénérer le rapport
            self._payload = FreeStation.finalize_free_station_with_z_and_report(
                self._payload, obs_data
            )
            if not self._payload:
                return

            if (
                self._payload.get("z_block") is not None
                and self._payload["z_block"].get("z_cmp") is not None
            ):
                self.lineEdit_z_sol_1.setText(
                    f"{self._payload['z_block']['z_cmp']:>16.4f}"
                )
                self.lineEdit_z_sol_2.setText("")

            #    if payload["z_block"].get("emqx_z", None) is not None:
            #        self.lineEdit_emq_z.setText(
            #            f"{payload["z_block"]['emqx_z']:>16.4f}"
            #        )

    def on_pushbutton_solution_2_clicked(self):
        if self._payload is not None:
            side = self._payload["result"].get("side", "auto")
            FreeStation.choose_free_station_candidate(
                self._payload, side=side
            )  # ou index=0/1
            # Après choix utilisateur (ex: par bouton “keep LEFT”)
            data_json = FileUtils.load_temp_file("free_station.json")
            obs_data = json.loads(data_json) if data_json else {}

            # === Bloc 3 : LSQ XY + θ sur toutes les visées sélectionnées (angles/distances) ===
            self._payload = FreeStation.refine_free_station_with_lsq_and_report(
                self._payload,
                obs_data,
                use_dirs=True,
                use_dists=True,
                robust=True,
                sigma_dir_gr=0.0003,  # ~3 cc (à adapter à ton instrument)
                sigma_dist_m=0.005,  # ~5 mm
                out_basename="free_station_report",
            )

            # Calculer Z et régénérer le rapport
            self._payload = FreeStation.finalize_free_station_with_z_and_report(
                self._payload, obs_data
            )
            if not self._payload:
                return

            if (
                self._payload.get("z_block") is not None
                and self._payload["z_block"].get("z_cmp") is not None
            ):
                self.lineEdit_z_sol_2.setText(
                    f"{self._payload['z_block']['z_cmp']:>16.4f}"
                )
                self.lineEdit_z_sol_1.setText("")

            #    if payload["z_block"].get("emqx_z", None) is not None:
            #        self.lineEdit_emq_z.setText(
            #            f"{payload["z_block"]['emqx_z']:>16.4f}"
            #        )

    def load_available(self):
        station_matricule = self.comboBox_compute_station.currentText()
        if station_matricule and self._obs_array:
            reference_matricules = (
                TopazeUtils.find_all_reference_matricules_in_obs_array(
                    self._obs_array, station_matricule
                )
            )
            available = self.filter_available_for_free_station(
                self._obs_array, station_matricule
            )
            self.listWidget_available.clear()
            self.listWidget_useful.clear()
            self.clear_xyz_fields_and_disable_solution_buttons()
            if available:
                DlgUtil.load_combobox_list_array(
                    self.listWidget_available, available, False
                )
            else:
                PlgLogger.log(
                    tr(
                        "At least, one  reference point with azimuth and distance required for computin free station {}."
                    ).format(station_matricule),
                    log_level=2,
                    push=True,
                )

    def use_it(self):
        selectedItems = self.listWidget_available.selectedItems()
        if not selectedItems:
            return

        for item in selectedItems:
            self.listWidget_available.takeItem(self.listWidget_available.row(item))
            self.listWidget_useful.addItem(item)

        self.clear_xyz_fields_and_disable_solution_buttons()

    def unuse_it(self):
        selectedItems = self.listWidget_useful.selectedItems()
        if not selectedItems:
            return

        for item in selectedItems:
            self.listWidget_useful.takeItem(self.listWidget_useful.row(item))
            self.listWidget_available.addItem(item)

        self.clear_xyz_fields_and_disable_solution_buttons()

    def use_all(self):
        items = self.listWidget_available.findItems("", QtCore.Qt.MatchContains)
        if not items:
            return

        for item in items:
            self.listWidget_available.takeItem(self.listWidget_available.row(item))
            self.listWidget_useful.addItem(item)

        self.clear_xyz_fields_and_disable_solution_buttons()

    def unuse_all(self):
        items = self.listWidget_useful.findItems("", QtCore.Qt.MatchContains)
        if not items:
            return

        for item in items:
            self.listWidget_useful.takeItem(self.listWidget_useful.row(item))
            self.listWidget_available.addItem(item)
        self.clear_xyz_fields_and_disable_solution_buttons()

    def display_coordinates(self, matricule):
        try:
            # print(matricule)
            self.clear_xyz_fields_and_disable_solution_buttons()
            pt = TopazeUtils.get_ptopo_by_matricule_in_obs_array(
                matricule, self._obs_array
            )
            if pt is None:
                pt = TopazeUtils.get_ptopo_by_matricule(
                    matricule, self._ptopo_array, self._ptopo_layer
                )
            if pt:
                self.lineEdit_x_sol_1.setText(str(pt.x))
                self.lineEdit_y_sol_1.setText(str(pt.y))
                self.lineEdit_z_sol_1.setText(str(pt.z))
        except Exception as e:
            print(str(e))

    def display_available_coordinates(self):
        try:
            matricule = self.listWidget_available.currentItem().text()
            self.display_coordinates(matricule)
        except Exception as e:
            print(str(e))

    def display_useful_coordinates(self):
        try:
            matricule = self.listWidget_useful.currentItem().text()
            self.display_coordinates(matricule)
        except Exception as e:
            print(str(e))

    def filter_available_for_free_station(self, obs_array, station_matricule):
        available = []
        matricules = []
        references = TopazeUtils.find_all_reference_matricules_in_obs_array(
            obs_array, station_matricule
        )
        for matricule in references:
            if matricule in matricules:
                continue
            refs = TopazeUtils.find_all_references_in_obs_array(
                obs_array, station_matricule
            )
            for ref in refs:
                if ref.matricule == matricule:
                    if ref.ah is not None and ref.di is not None:
                        available.append(_qualified_ref(ref))
                        matricules.append(matricule)
                        break
        if not len(available):
            return None
        for matricule in references:
            if matricule in matricules:
                continue
            refs = TopazeUtils.find_all_references_in_obs_array(
                obs_array, station_matricule
            )
            for ref in refs:
                if ref.matricule == matricule:
                    if matricule not in available and ref.ah is not None:
                        available.append(_qualified_ref(ref))
                        matricules.append(matricule)
                        is_used = True
                        break
        return available

    def save_data_to_compute_free_station(
        self, tmp_filename, obs_array, to_compute_array, reference_array
    ):
        data_dict = {"calcul": to_compute_array, "stations": [], "obs": []}
        a_is_known = b_is_known = False

        stations = TopazeUtils.find_all_stations_in_obs_array(obs_array)
        for station in stations:
            if station.matricule in to_compute_array:
                dico = {"matricule": station.matricule}
                pt = TopazeUtils.get_ptopo_by_matricule_in_obs_array(
                    station.matricule, obs_array
                )
                if pt is None:
                    pt = TopazeUtils.get_ptopo_by_matricule(
                        station.matricule, self._ptopo_array, self._ptopo_layer
                    )
                if pt:
                    if not pt.z or pt.z <= PtopoConst.UNKNOWN_Z:
                        dico.update({"x": pt.x, "y": pt.y, "z": PtopoConst.NO_Z})
                    else:
                        dico.update({"x": pt.x, "y": pt.y, "z": pt.z})
                else:
                    dico.update(
                        {
                            "x": PtopoConst.NO_X,
                            "y": PtopoConst.NO_Y,
                            "z": PtopoConst.NO_Z,
                        }
                    )
                data_dict["stations"].append(dico)

                reference_matricules = (
                    TopazeUtils.find_all_reference_matricules_in_obs_array(
                        obs_array, station.matricule
                    )
                )
                for matricule in reference_matricules:
                    dico = {"matricule": matricule}
                    pt = TopazeUtils.get_ptopo_by_matricule_in_obs_array(
                        matricule, obs_array
                    )
                    if pt is None:
                        pt = TopazeUtils.get_ptopo_by_matricule(
                            matricule, self._ptopo_array, self._ptopo_layer
                        )
                    if pt:
                        if not pt.z or pt.z <= PtopoConst.UNKNOWN_Z:
                            dico.update({"x": pt.x, "y": pt.y, "z": PtopoConst.NO_Z})
                        else:
                            dico.update({"x": pt.x, "y": pt.y, "z": pt.z})
                    else:
                        dico.update(
                            {
                                "x": PtopoConst.NO_X,
                                "y": PtopoConst.NO_Y,
                                "z": PtopoConst.NO_Z,
                            }
                        )
                    if dico not in data_dict["stations"]:
                        data_dict["stations"].append(dico)
                references = TopazeUtils.find_all_references_in_obs_array(
                    obs_array, station.matricule
                )
                for ref in references:
                    if (
                        ref.matricule in to_compute_array
                        or ref.matricule in reference_array
                    ):
                        dico = {"origine": station.matricule, "hi": station.hi}
                        dico.update(
                            {
                                "origine": station.matricule,
                                "hi": station.hi,
                                "cible": ref.matricule,
                                "ah": ref.ah,
                                "av": ref.av,
                                "di": ref.di,
                                "hp": ref.hp,
                            }
                        )
                        data_dict["obs"].append(dico)
                data_dict["free_station"] = {}
                for obs in data_dict["obs"]:
                    if obs["origine"] == station.matricule:
                        if obs["ah"] is not None and obs["di"] is not None:
                            a = {"A": obs["cible"]}
                            data_dict["free_station"].update(a)
                            a_is_known = True
                            break

                for obs in data_dict["obs"]:
                    if obs["origine"] == station.matricule:
                        if (
                            obs["ah"] is not None
                            and (
                                data_dict["free_station"].get("A", None) is not None
                                and obs["cible"] != data_dict["free_station"]["A"]
                            )
                            or data_dict["free_station"].get("A", None) is None
                        ):
                            b = {"B": obs["cible"]}
                            data_dict["free_station"].update(b)
                            b_is_known = True
                            break

        lsq = {
            "enabled": True,
            "use_dirs": True,
            "use_dists": True,
            "robust": True,
            "sigma_dir_gr": 0.0003,
            "sigma_dist_m": 0.005,
        }
        if len(obs) < 3:
            lsq["enabled"] = False
        data_dict.update({"lsq": lsq})

        if a_is_known and b_is_known:
            data_dict["free_station"].update({"choose": "auto"})
            data_s = json.dumps(data_dict, indent=4)
            fullfilepath = FileUtils.create_temp_file(tmp_filename, data_s)
        else:
            PlgLogger.log(
                tr(
                    "At least two reference points with azimuth and one distance are required."
                ),
                log_level=2,
            )
            fullfilepath = None
        return fullfilepath

    def clear_xyz_fields_and_disable_solution_buttons(self):
        self.lineEdit_x_sol_1.setText("")
        self.lineEdit_y_sol_1.setText("")
        self.lineEdit_z_sol_1.setText("")
        self.lineEdit_x_sol_2.setText("")
        self.lineEdit_y_sol_2.setText("")
        self.lineEdit_z_sol_2.setText("")
        self.lineEdit_emq_x.setText("")
        self.lineEdit_emq_y.setText("")
        self.lineEdit_emq_z.setText("")
        self.pushButton_solution_1.setEnabled(False)
        self.pushButton_solution_2.setEnabled(False)
