# mpl_canvas_onpick_event.py -- Pamhyr
# Copyright (C) 2023  INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.

import logging

from time import time

import numpy as np
from PyQt5 import QtWidgets
from PyQt5.QtCore import QItemSelectionModel, Qt
from PyQt5.QtWidgets import QApplication
from shapely.geometry.polygon import Polygon as ShapelyPolygon

logger = logging.getLogger()


class OnpickEvent(object):
    def __init__(self, ax, x, y, x_carto, y_carto, tableview=None):
        """
        Args:
            ax: objet Axes.
            x: abscisse x1 du graphe (f(x1) = y1) à tracer.
            y: ordonnée y1 du graphe (f(x1) = y1) à tracer.
            x_carto: (vecteur) abscisse des points (X,Y,Z) du profil.
            y_carto: (vecteur) abscisse des points (X,Y,Z) du profil
            tableview: tableau (de type QtableView) 'associé' au grahique.
        """
        self.ax = ax
        self.x = x
        self.y = y
        self.x_carto = x_carto
        self.y_carto = y_carto
        self.tableView = tableview
        self.counter_onclick = 0
        # self.select_point_plot()
        self.count = 0
        self.annotation_onclick = self.ax.annotate(
            "",
            xytext=(np.mean(self.x), np.mean(self.y)),
            xy=(np.mean(self.x), np.mean(self.y)),
            horizontalalignment='center',
            fontsize=8,
            # fontstyle='italic',
            fontweight='bold',
            alpha=0.7
        )
        self.annotation_onclick.set_visible(False)
        self.pos_x = 0
        self.zomm_xmin_xmax = self.ax.get_xlim()
        self.plot_selec()
        # self.select_point_plot()
        self._largeur_miroir, = self.ax.plot(
            self.x[1], self.y[1],
            color='blue', lw=1.2, ls=":"
        )

        self.pt = []
        self.tableView.selectionModel()\
                      .selectionChanged\
                      .connect(self.update_select_point_point)

    def select_row_pt_clicked(self, ind: int = 0):
        """
        Args:
             ind: Indice de la ligne où se trouve le point le plus proche
                  'visé'.

        Returns: Sélectionne la ligne (du tableau) correspondant au point le
                 plus proche 'visé' à la suite de l'événement onpick.
        """
        if self.tableView is not None:
            selectionModel = self.tableView.selectionModel()
            index = self.tableView.model().index(ind, 0)
            selectionModel.select(
                index,
                QItemSelectionModel.Rows |
                QItemSelectionModel.ClearAndSelect |
                QItemSelectionModel.Select
            )
            self.tableView.scrollTo(index)

    def select_qtableview_row(self, event):
        if self.tableView is not None:
            self.tableView.setFocus()
            ind = self.indice_points_onpick(event)
            dataidx_ecran = self.index_pt_plus_proche_ecran(event)
            self.select_row_pt_clicked(ind[dataidx_ecran])

    def select_point_plot(self):
        """
        Returns: sélectionne le(s) point(s) du graphique correspondant à
                 la/aux ligne(s) sélectionnée(s) dans le tableau.
        """
        if self.tableView is not None:
            rows = list(set(
                [index.row() for index in self.tableView.selectedIndexes()]
            ))
            for row in rows:
                pass

    def update_select_point_point(self):
        if self.tableView is not None:
            rows = list(set(
                [index.row() for index in self.tableView.selectedIndexes()]
            ))

            if len(rows) > 1:
                for row in rows:
                    self.pt1 = self.ax.plot(self.x[row], self.y[row],
                                            '+', c='Blue', markersize=7)
                    self.pt.append(self.pt1)
                    self.update_select_point_point_bis(
                        self.x[row], self.y[row])
            elif len(rows) == 1:
                for row in rows:
                    try:
                        [pl[0].set_data([], [])
                         for pl in self.pt if len(self.pt) > 1]
                    except Exception:
                        logger.info("update_select_point_point: Update issue")

                    try:
                        self.update_select_point_point_bis(self.x[row],
                                                           self.y[row])
                    except Exception:
                        logger.info(
                            "update_select_point_point_bis: Update issue, " +
                            "possible index missing"
                        )

            self.ax.figure.canvas.draw_idle()

    def plot_selec(self):
        self.point_selec, = self.ax.plot(self.x[0], self.y[0],
                                         '+', c='Blue', markersize=7)
        self.point_selec.set_visible(False)

    def update_select_point_point_bis(self, x_ind, y_ind):
        self.point_selec.set_data(x_ind, y_ind)
        self.point_selec.set_visible(True)
        self.ax.figure.canvas.draw_idle()

    def plot_selection_point(self, x, y):
        """
        Args:
            x: abscissa
            y: ordinate

        Returns: sélectionne le point du graphique correspond à la ligne
                 sélectionnée dans le tableau.
        """
        if self.tableView is not None:
            self.select_point, = self.ax.plot(
                x, y,
                '+', c='Blue',
                markersize=7
            )
        else:
            self.select_point, = self.ax.plot([], [])

    def geometrie_sans_rebord(self):
        rebord = True
        z_sans_rebord = [i for i in self.y]
        x_sans_rebord = [i for i in self.x]

        while rebord:
            if z_sans_rebord[1] >= z_sans_rebord[0]:
                z_sans_rebord.pop(0)
                x_sans_rebord.pop(0)
            else:
                rebord = False

        rebord = True
        while rebord:
            if z_sans_rebord[-1] <= z_sans_rebord[-2]:
                z_sans_rebord.pop()
                x_sans_rebord.pop()
            else:
                rebord = False

        z_berge_basse = min(z_sans_rebord[0], z_sans_rebord[-1])

        return z_berge_basse, z_sans_rebord, x_sans_rebord

    @property
    def z_berge_basse(self):
        return self.geometrie_sans_rebord()[0]

    @property
    def z_sans_rebord(self):
        return self.geometrie_sans_rebord()[1]

    @property
    def x_sans_rebord(self):
        return self.geometrie_sans_rebord()[2]

    @property
    def z_fond(self):
        return np.array(self.z_sans_rebord)

    @property
    def z_point_bas(self):
        """
        Returns: la cote (Zmin) du point le plus bas.
        """
        return min(self.y)

    @property
    def delta_x(self):
        """"
        Returns: la longueur entre les limites de la vue sur l'axe des x,
                 c'est-à-dire |x_max_visible - x_min_visible|.
        """
        xgauche, xdroite = self.ax.get_xlim()
        delta_x = abs(xdroite - xgauche)
        return delta_x

    @property
    def delta_y(self):
        """
        Returns: la longueur entre les limites de la vue sur l'axe des y,
                 c'est à dire |y_max_visible - y_min_visible|.
        """
        ybas, yhaut = self.ax.get_ylim()
        delta_y = abs(yhaut - ybas)
        return delta_y

    @staticmethod
    def indice_points_onpick(event):
        """
        Args: event
        Returns: le(s) indexe(s) du/des point(s) (plus précisement les
                 coordonnées de points) capturé(s) par l'événement onpick
                 (voir picker)
        """
        return event.ind

    def points_onpick(self, event):
        """
        Args:
            event:
        Returns: une array contenant les coordonées des points qui se
                 trouvent dans la zone définie par l'événement onpick
                 (voir picker)
        """
        thisline = event.artist
        xdata = thisline.get_xdata()
        ydata = thisline.get_ydata()
        points_onpick = np.array(
            [(xdata[i], ydata[i]) for i in self.indice_points_onpick(event)]
        )
        return points_onpick

    def distance_normee(self, event):
        """
        Args:
            event:
        Returns: la liste des distances normées (en m) entre les points
                 situés dans la région définie par l'événement onpick
                 (voir picker).
        """
        ind = event.ind
        thisline = event.artist
        xdata = thisline.get_xdata()
        ydata = thisline.get_ydata()
        points_onpick = np.array([(xdata[i], ydata[i]) for i in ind])
        distances_normees = [
            (((x - event.mouseevent.xdata) / self.delta_x) ** 2 +
             ((y - event.mouseevent.ydata) / self.delta_y) ** 2) ** (1 / 2)
            for (x, y) in points_onpick
        ]
        return distances_normees

    def position_souris(self, event):
        """
        Args:
            event:
        Returns: la position de la souris
        """
        self.pos_souris = [(event.mouseevent.xdata, event.mouseevent.ydata)]
        return self.pos_souris

    def distance_ecran(self, event):
        """
        Args:
            event:
        Returns: la liste des distances 'visuelles' entre les points situés
                 dans la région définie par l'événement onpick (voir picker).
        """
        bbox = self.ax.get_window_extent()\
                      .transformed(self.ax.figure.dpi_scale_trans.inverted())
        ratio_w_sur_h = bbox.width / bbox.height
        distances_ecran = [
            (
                (
                    (x - event.mouseevent.xdata) /
                    (self.delta_x * ratio_w_sur_h)
                ) ** 2 +
                (
                    (y - event.mouseevent.ydata) /
                    self.delta_y
                ) ** 2
            ) ** (1 / 2)
            for (x, y) in self.points_onpick(event)
        ]

        return distances_ecran

    def distances(self, event):
        """
        Args:
            event:
        Returns: la liste des distances entre la position de la souris et
                 tous les points se trouvant dans la zone définie par
                 l'événement onpick (voir picker)
        """
        distances = np.linalg.norm(
            self.points_onpick(event) - self.position_souris(event),
            axis=1
        )

        return distances

    def index_pt_plus_proche_ecran(self, event):
        """
        Args:
            event:
        Returns: indice du point le plus proche visuellement de la position
                 du click.
        """
        dataidx_ecran = np.argmin(self.distance_ecran(event))
        return dataidx_ecran

    def point_plus_proche_ecran(self, event):
        point_onpick = self.points_onpick(event)
        datapos_ecran = point_onpick[
            self.index_pt_plus_proche_ecran(event)
        ]

        return self.points_onpick(event)[
            self.index_pt_plus_proche_ecran(event)
        ]

    def index_pt_plus_proche(self, event):
        """
        Args:
            event:
        Returns: indice du point le plus proche de la position du click.
        """
        dataidx = np.argmin(self.distances(event))

        return dataidx

    def point_plus_proche(self, event):
        """
        Args:
            event:
        Returns: point le plus proche de la position du click
        """
        point_onpick = self.points_onpick(event)
        datapos = point_onpick[self.index_pt_plus_proche(event)]

        return datapos

    def annotate_onpick(self, x, y):
        """
        Args:
            x: abscisse du point à annoter.
            y: ordonnée du point à annoter.
        Returns: annote le point xy avec du texte text = xytext.
        """
        return self.ax.annotate(
            "X", xytext=(x, y),
            xy=(x, y), fontsize=9,
            bbox=dict(
                boxstyle='round,pad=0.8', fc='yellow', alpha=0.75
            ),
            arrowprops=dict(
                arrowstyle='->',
                connectionstyle='arc3,rad=0.',
                color='blue'
            )
        )

    def on_ylims_change(self, event_ax):
        return event_ax.get_ylim()

    def annotate_onclick(self, event):
        if self.z_point_bas <= event.ydata:
            self.count += 1

            if event.ydata <= self.z_berge_basse:
                A, p, L = self.calcul_ligne_eau(event.ydata)
            else:
                event.ydata = self.z_berge_basse
                A, p, L = self.calcul_ligne_eau(event.ydata)

            etiq = f"Z = {event.ydata:.3f} m, A = {A:.3f} "\
                f"m\u00B2, p = {p:.3f} m, L = {L:.3f} m"
            self.annotation_onclick.set_text(etiq)
            x_min, x_max = self.ax.get_xlim()
            self.pos_x_annotation = x_min + ((x_max - x_min) / 2)

            percent = 0
            y_ecran_lim = ((max(self.ax.set_ylim()) -
                           min(self.ax.set_ylim())) / 2)

            if abs(y_ecran_lim) > 4:
                percent = 0.05
            elif 4 < abs(y_ecran_lim) < 1.5:
                percent = 0.01
            elif 0.5 < abs(y_ecran_lim) < 1.5:
                percent = 0.05
            elif 0.25 < abs(y_ecran_lim) < 0.5:
                percent = 0.08
            elif 0 < abs(y_ecran_lim) < 0.25:
                percent = 0.25
            else:
                percent = 0.1

            cte = 0.
            if abs(event.ydata) < 100:
                cte = 0.05
            else:
                cte = event.y * 0.1 / 100

            self.y_pos_text_param_hydrau = event.ydata + cte
            self.annotation_onclick.set_position(
                (self.pos_x_annotation,
                 self.y_pos_text_param_hydrau)
            )

            self.ax.callbacks.connect('ylim_changed', self.on_ylims_change)

            self.annotation_onclick.set_color("DarkBlue")
            self.annotation_onclick.set_visible(True)
            self.annotation_onclick.set_horizontalalignment('center')

            self.ax.figure.canvas.draw_idle()

            return self.annotation_onclick

    def largeur_au_miroir(self, event):
        if event.ydata <= self.z_berge_basse:
            self._largeur_miroir.set_data(
                [min(self.x), max(self.x)],
                [event.ydata, event.ydata]
            )
        else:
            self._largeur_miroir.set_data(
                [min(self.x), max(self.x)],
                [self.z_berge_basse, self.z_berge_basse]
            )

        return self._largeur_miroir

    def onpick(self, event):
        modifiers = QApplication.keyboardModifiers()

        if modifiers == Qt.ControlModifier:
            if event.mouseevent.inaxes == self.ax:
                self.select_qtableview_row(event)
                x_proche, y_proche = self.point_plus_proche_ecran(event)
                self.update_select_point_point_bis(x_proche, y_proche)

            self.ax.figure.canvas.draw_idle()

    def onclick(self, event):
        modifiers = QtWidgets.QApplication.keyboardModifiers()
        if modifiers == Qt.ShiftModifier:
            if event.inaxes == self.ax:
                if self.z_point_bas < event.ydata:
                    try:
                        self.poly_col_bis.remove()
                        self.largeur_au_miroir(event)
                    except Exception:
                        self.largeur_au_miroir(event)

                self.annotate_onclick(event)
                self.ax.figure.canvas.draw_idle()

    def remplir_zone_mouillee(self, x, y1, y2):
        """
        Args:
            x: Les coordonnées x des nœuds définissant la courbe.
            y1: points définisant le polygone à déssiner.
            y2: points définisant le polygone à déssiner.
        Returns: dessine et colorie la région définie par le polygone.
        """
        return self.ax.fill_between(
            x, y1=y1, y2=y2, where=y1 > y2, interpolate=True,
            facecolor='skyblue', alpha=0.7
        )

    def calcul_ligne_eau(self, val: float) -> (float, float, float):
        """
        Args:
            val: Valeur de la cote Z à laquelle on veut caluler A , p et L.
        Returns: la valeur de la section mouillée A, du périmètre mouillé p
                 et de la largeur au miroir L.
        """
        largeur_miroir = 0.
        section_mouillee_totale = 0.
        perimetre_mouille_total = 0.

        if self.z_point_bas < val <= self.z_berge_basse:
            z_eau = np.array([val] * (len(self.z_sans_rebord)))
            self.poly_col_bis = self.remplir_zone_mouillee(self.x_sans_rebord,
                                                           z_eau,
                                                           self.z_sans_rebord)
            liste_chemins = self.poly_col_bis.get_paths()
            couleurs = ['crimson', 'pink'] * len(liste_chemins)
            aire_calculee_shapely = None
            perimetre_mouille_total_shapely = None
            perimetre_shapely = 0.
            perim_calc = 0.

            for polyg, coul in zip(
                    liste_chemins,
                    couleurs[0:len(liste_chemins)]
            ):
                points_polygone = polyg.vertices
                xs = points_polygone[:, 0]
                ys = points_polygone[:, 1]

                liste_points_miroir = [
                    x for (x, y) in zip(xs, ys) if np.isclose(y, val)
                ]
                largeur_miroir_polygone = liste_points_miroir[-2] - \
                    liste_points_miroir[0]
                largeur_miroir += largeur_miroir_polygone

                polygone_shapely = ShapelyPolygon(points_polygone)
                aire_calculee_shapely = polygone_shapely.area
                perimetre_shapely = polygone_shapely.length
                perimetre_mouille_total_shapely = (
                    polygone_shapely.length - largeur_miroir
                )
                liste_points_fond = [
                    (x, y) for (x, y) in zip(xs, ys) if not np.isclose(y, val)
                ]
                x_pt_prec, y_pt_prec = max(liste_points_miroir), val

                perimetre = 0
                aire = 0

                for un_point in liste_points_fond + [
                        (min(liste_points_miroir), val)
                ]:
                    x_pt_suivant, y_pt_suivant = un_point
                    perimetre += ((x_pt_prec - x_pt_suivant) ** 2 +
                                  (y_pt_prec - y_pt_suivant) ** 2) ** (1 / 2)
                    aire += (((val - y_pt_prec) + (val - y_pt_suivant)) *
                             abs(x_pt_suivant - x_pt_prec) / 2)
                    x_pt_prec, y_pt_prec = x_pt_suivant, y_pt_suivant
                    perim_calc = perimetre

            perimetre_mouille_total = perimetre_shapely - largeur_miroir
            section_mouillee_totale = aire_calculee_shapely

        return section_mouillee_totale, perimetre_mouille_total, largeur_miroir
