# -*- coding: utf-8 -*-
"""
Created on Fri Aug  2 13:55:33 2019

@author: psakicki
"""

import copy
import datetime as dt

#### Import the logger
import logging
import os

########## BEGIN IMPORT ##########
#### External modules
from collections import Counter

import matplotlib
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import scipy
from matplotlib.widgets import MultiCursor

#### geodeZYX modules
from geodezyx import conv
from geodezyx import files_rw
from geodezyx import reffram
from geodezyx import stats
from geodezyx import time_series
from geodezyx import utils

log = logging.getLogger("geodezyx")
log.setLevel(logging.INFO)

##########  END IMPORT  ##########


class Point:

    def __init__(
        self,
        A=0.0,
        B=0.0,
        C=0.0,
        T=0.0,
        initype="XYZ",
        sA=0.0,
        sB=0.0,
        sC=0.0,
        name="noname",
        anex=None,
    ):
        """
        Initialize a Point Object by importing the coordinate component

        Parameters
        ----------
        A : float
            X, F (latitude), E (depends on initype).
        B : float
            Y, L (longitude), N (depends on initype).
        C : float
            Z, H (hight), U (depends on initype).
        T : float, optional
            Epoch of the measure. The default is 0..
        initype : str, optional
            The inital coordinates type. The default is 'XYZ'.
            The others are 'FLH', 'ENU' and 'NED'
        sA : TYPE, optional
            sigma of A component. The default is 0.
        sB : TYPE, optional
            sigma of B component. The default is 0.
        sC : TYPE, optional
            sigma of C component. The default is 0.
        name : str, optional
            Flexible name for the Point identification. The default is 'noname'.
        anex : dict, optional
            Additional data. The default is None. See Note

        Note
        ----

        A dictionary called anex is also initialized to allow a
        versatile storage of a variety of data

        Exemple of dictionary keys
        RMS: average RMS (for gipsy)
        sdAB , sdBC , sdAC : the variances between A,B,C (for rtklib)
        sdXY , sdXZ , sdYZ : the variances between XYZ (pbo.pos)
        Vx , Vy , Vz , sVx , sVy , sVz : velocity of the point (EPOS coordinates)
        """

        self.Tset(T)
        self.ENUset()
        self.name = name
        if not anex:
            self.anex = dict()
        else:
            self.anex = anex

        # le dico "anex" permet de stocker de manière polyvalente des données diverses
        # On trouvera (LISTE SE DEVANT ETRE LA PLUS EXHAUSTIVE POSSIBLE )
        #
        # RMS : moyenne RMS (pour les gipsy bosser)
        # sdAB , sdBC , sdAC : les variances entre A,B,C (pour les rtklib)
        # sdXY , sdXZ , sdYZ : les variances entre XYZ (pbo.pos)
        # Vx , Vy , Vz , sVx , sVy , sVz : velocity of the point (EPOS coordinates)

        if initype == "XYZ":
            self.XYZset(A, B, C, sA, sB, sC)
        elif initype == "FLH":  # On travaille en degres decimaux
            self.FLHset(A, B, C, sA, sB, sC)

        elif initype == "ENU":
            self.ENUset(A, B, C, sA, sB, sC)

        elif initype == "NED":
            self.NEDset(A, B, C, sA, sB, sC)

        elif initype == "UTM":
            self.UTMset(A, B, C, sA, sB, sC)
        else:
            log.error("wrong initype")

    def __call__(self):

        if self.initype == "XYZ":
            return self.X, self.Y, self.Z, self.Tdt, self.T
        elif self.initype == "FLH":
            return self.F, self.L, self.H, self.Tdt, self.T
        elif self.initype == "ENU":
            return self.E, self.N, self.U, self.Tdt, self.T
        elif self.initype == "NED":
            return self.N, self.E, self.D, self.Tdt, self.T
        elif self.initype == "UTM":
            return self.Eutm, self.Nutm, self.Uutm, self.Tdt, self.T
        else:
            log.error("wrong initype")

    def __repr__(self):
        if not hasattr(self, "X"):
            return "{},{},{},{},{}".format(self.E, self.N, self.U, self.Tdt, self.T)
        else:
            return "{},{},{},{},{}".format(self.X, self.Y, self.Z, self.Tdt, self.T)

    def XYZset(self, X=0, Y=0, Z=0, sX=0, sY=0, sZ=0):
        self.X = X
        self.Y = Y
        self.Z = Z
        self.sX = sX
        self.sY = sY
        self.sZ = sZ

        self.initype = "XYZ"
        self.F, self.L, self.H = conv.xyz2geo(self.X, self.Y, self.Z)

    def FLHset(self, F=0, L=0, H=0, sF=0, sL=0, sH=0):
        self.F = F
        self.L = L
        self.H = H
        self.sF = sF
        self.sL = sL
        self.sH = sH

        self.initype = "FLH"
        self.X, self.Y, self.Z = conv.geo2xyz(self.F, self.L, self.H)
        self.sX, self.sY, self.sZ = conv.sigma_geo2xyz(F, L, H, sF, sL, sH)

    def ENUset(self, E=np.nan, N=np.nan, U=np.nan, sE=np.nan, sN=np.nan, sU=np.nan):
        self.E = E
        self.N = N
        self.U = U
        self.sE = sE
        self.sN = sN
        self.sU = sU

        self.initype = "ENU"
        self._flh_computed = False

    def NEDset(self, N=np.nan, E=np.nan, D=np.nan, sN=np.nan, sE=np.nan, sD=np.nan):
        self.N = N
        self.E = E
        self.D = D
        self.sN = sN
        self.sE = sE
        self.sD = sD

        self.initype = "NED"
        self._flh_computed = False

    def UTMset(
        self,
        Eutm=np.nan,
        Nutm=np.nan,
        Uutm=np.nan,
        sEutm=np.nan,
        sNutm=np.nan,
        sUutm=np.nan,
    ):
        self.Eutm = Eutm
        self.Nutm = Nutm
        self.Uutm = Uutm
        self.sEutm = sEutm
        self.sNutm = sNutm
        self.sUutm = sUutm

        self.initype = "UTM"
        self._flh_computed = False

    def add_offset(self, dA, dB, dC, coortype="ENU"):
        temp = time_series.add_offset_point(self, dA, dB, dC, coortype=coortype)
        self.__dict__ = temp.__dict__

    def Tset(self, T=0):
        if T == 0:
            T = dt.datetime.now()

        if type(T) == dt.datetime:
            self.Tdt = T
            self.T = conv.dt2posix(T)

        else:
            self.T = float(T)
            self.Tdt = conv.posix2dt(float(T))

    def ENUcalc_pt(self, refENU):

        self.refENU = refENU

        dX = self.X - refENU.X
        dY = self.Y - refENU.Y
        dZ = self.Z - refENU.Z

        Etmp, Ntmp, Utmp = conv.xyz2enu(
            self.X, self.Y, self.Z, refENU.X, refENU.Y, refENU.Z
        )

        self.E, self.N, self.U = Etmp[0], Ntmp[0], Utmp[0]

        if self.initype == "FLH" and hasattr(self, "s_f"):
            if not np.isnan(self.sF):
                self.sE, self.sN, self.sU = conv.sigma_geo2enu(
                    self.F, self.L, self.H, self.sF, self.sL, self.sH
                )
        elif self.initype == "XYZ" and hasattr(self, "sX"):
            if np.isnan(self.sX):

                return

            if "sdXY" in self.anex.keys():
                sXY = self.anex["sdXY"]
                sXZ = self.anex["sdXZ"]
                sYZ = self.anex["sdYZ"]
            else:
                sXY, sXZ, sYZ = 0, 0, 0

            self.sE, self.sN, self.sU = conv.sigma_xyz2enu(
                self.X,
                self.Y,
                self.Z,
                self.sX,
                self.sY,
                self.sZ,
                s_xy=sXY,
                s_yz=sYZ,
                s_xz=sXZ,
            )

    def UTMcalc_pt(self, ellips="wgs84"):
        self.Eutm, self.Nutm, _ = conv.utm_geo2xy(self.F, self.L)
        self.Uutm = self.H

    def keysanex(self):
        return list(self.anex.keys())

    def helmert_trans(self, params="itrf2008_2_etrf2000", invert=False):
        Xb = reffram.helmert_trans(np.array([self.X, self.Y, self.Z]), params, invert)
        self.XYZset(*Xb)
        return None

    def velocity_trans(self, vx, vy, vz, epoc_init="auto", epoc_end="auto"):
        """
        auto == epoc of the measures
        """

        if epoc_init == "auto" and epoc_end == "auto":
            log.error("epoc_init == auto and epoc_end == auto")
            return None

        tdt = conv.posix2dt(self.T)
        yeardec = conv.dt2year_decimal(tdt)

        if epoc_init == "auto":
            epoc_init = yeardec

        if epoc_end == "auto":
            epoc_end = yeardec

        Xb = reffram.itrf_speed_calc(
            self.X, self.Y, self.Z, epoc_init, vx, vy, vz, epoc_end
        )
        self.XYZset(*Xb)
        return None


class TimeSeriePoint:

    def __init__(self, stat="STAT"):
        """
        Initialize a TimeSeriePoint object

        Parameters
        ----------
        stat : str, optional
            station 4-char. code. The default is 'STAT'.

        """

        self.pts = []
        self.i_nomi = 0

        self.meta_set(stat=stat)

        self.boolENU = False
        self.boolUTM = False

        self.bool_interp_uptodate = False
        self.bool_discont = False
        self.bool_discont_manu = False

        self.anex = dict()

    def __repr__(self):
        """
        Representation of a TimeSeriePoint object
        """
        if self.pts == []:
            raise Exception("ERR: TimeSeriePoint is empty ...")

        start = self.startdate()
        end = self.enddate()
        nbday = int((end - start).days + 1.0)

        ratio = self.nbpts * 100.0 / nbday

        return "{} {} {} {} {} {} {} {:5.2f}{}".format(
            self.stat, self.nbpts, "points", start, end, nbday, "nb days", ratio, "%"
        )

    def __getitem__(self, i):
        return self.pts[i]

    @property
    def nbpts(self):
        """
        Method to have the length of the TimeSerie

        Returns
        -------
        int
            Length of the TimeSerie.

        """
        return len(self.pts)

    def meta_set(self, path="", stat="STAT", name=""):
        """
        Set meta data about the TimeSerie

        Parameters
        ----------
        path : str, optional
            file path. The default is ''.
        stat : str, optional
            station 4-char. code. The default is 'STAT'.
        name : str, optional
            free name of for the TS,
            like the experience, the periode , the software ...
            The default is ''.

        Returns
        -------
        None.
        """

        self.path = path
        self.stat = stat

        bn = os.path.basename(path)
        dn = os.path.dirname(path)

        if name == "":
            if bn == "tdp_final":
                self.name = os.path.basename(dn)
            else:
                self.name = bn
        else:
            self.name = name
        self.interval_nominal()

    def del_data(self):
        """
        Method to purge the data in the TimeSeriePoint

        Returns
        -------
        None.

        """
        self.pts = []

        self.bool_interp_uptodate = False
        self.bool_discont = False

    def readfile(self, filein):
        """
        Method to read the data form a file
        Should be used with care

        Parameters
        ----------
        filein : str
            path of the file.

        Returns
        -------
        None.

        """
        self.__dict__ = files_rw.read_all_points(filein).__dict__

        self.interp_set()

    def add_point(self, point_inp):
        """
        Method to add a Point in the TimeSerie Object

        Parameters
        ----------
        point_inp : Point Object

        Returns
        -------
        None.

        """
        self.pts.append(point_inp)
        # this line is discontiued, because now nbpts is a property
        # self.nbpts = len(self.pts)

        self.bool_interp_uptodate = False

    def aleapt(self):
        """
        Method to get a random Point in the TimeSeries

        Returns
        -------
        Point Object

        """
        ipt = np.random.randint(self.nbpts)
        log.info("random point selected #%s", str(ipt))

        return self.pts[ipt]

    def startdate(self):
        """
        Method to get the first epoch of the data in the TimeSerie

        Returns
        -------
        DateTime

        """
        self.sort()
        return conv.posix2dt(self.pts[0].T)

    def enddate(self):
        """
        Method to get the last epoch of the data in the TimeSerie

        Returns
        -------
        DateTime

        """
        self.sort()
        return conv.posix2dt(self.pts[-1].T)

    def len_period(self, output_seconds=False):
        """
        Method to get the period length

        Returns
        -------
        timedelta or
        """
        delta = self.enddate() - self.startdate()

        if output_seconds:
            return delta.days * 86400 + delta.seconds
        else:
            return delta

    def interval_nominal(self):
        """
        Method to get the nominal internal between two epochs.

        Returns
        -------
        float
            interval nominal.

        """

        if len(self.pts) < 2:
            self.i_nomi = 0
        else:
            Ttemp = np.sort([pt.T for pt in self.pts])
            self.i_nomi = np.round(np.min(np.diff(Ttemp)), 1)

        return self.i_nomi

    def from_list(self, T, A, B, C, coortype="XYZ", sA=[], sB=[], sC=[]):
        """
        Method to load data from lists to the TimeSerie

        Parameters
        ----------
        T : float
            Time.
        A : list of float
            X, F (latitude), E..
        B : list of float
            Y, L (longitude), N.
        C : list of float
            Z, H (hight), U.
        coortype : str, optional
            The coordinates type. The default is 'XYZ'.
        sA : list of float, optional
            sigma of A component. The default is [].
        sB : list of float, optional
            sigma of B component. The default is [].
        sC : list of float, optional
            sigma of C component. The default is [].

        Returns
        -------
        None.

        """

        if not sA:
            sA = np.zeros(len(A))
        if not sB:
            sB = np.zeros(len(B))
        if not sC:
            sC = np.zeros(len(C))

        for t, a, b, c, sa, sb, sc in zip(T, A, B, C, sA, sB, sC):
            point = Point(a, b, c, t, coortype, sa, sb, sc)

            self.add_point(point)

        if coortype == "ENU":
            self.boolENU = True

        if coortype == "UTM":
            self.boolUTM = True

        self.sort()

        return None

    def to_list(self, coortype="XYZ", specific_output=None, time_as_datetime=False):
        """
        Export the TimeSerie Object as Lists (Numpy Arrays)

        Parameters
        ----------
        coortype : str, optional
        The coordinates type exported to the list.
        The default is 'XYZ'.
        specific_output : int, optional
            ask for a specific list, ranges between 0 and 6.
            The default is None.
        time_as_datetime : bool, optional
            if True the Time list is exported in datetime
            if False the Time list is exported in Posix time

        Returns
        -------
        A,B,C,T,sA,sB,sC : lists
            A = X, F (latitude), E.
            B = Y, L (longitude), N.
            C = Z, H (hight), U.
            T = Time
            sA = sigma of A component
            sB = sigma of B component
            sC = sigma of C component
        """

        if coortype == "XYZ":
            A, B, C = "X", "Y", "Z"
            sA, sB, sC = "sX", "sY", "sZ"

        elif coortype == "FLH":
            A, B, C = "F", "L", "H"
            sA, sB, sC = "sF", "sL", "sH"

        elif coortype == "ENU":
            if not self.boolENU:
                log.warning("no ENU coord. for " + self.name)
                return None

            A, B, C = "E", "N", "U"
            sA, sB, sC = "sE", "sN", "sU"

        elif coortype == "UTM":
            if not self.boolUTM:
                log.warning("no UTM coord. for " + self.name)
                return None

            A, B, C = "Eutm", "Nutm", "Uutm"
            sA, sB, sC = "sEutm", "sNutm", "sUutm"

        else:
            log.error("coortype does not exist")

        if self.nbpts == 0:
            log.error(self.name + " the timeserie is empty")

        A = np.asarray([getattr(pt, A) for pt in self.pts])
        B = np.asarray([getattr(pt, B) for pt in self.pts])
        C = np.asarray([getattr(pt, C) for pt in self.pts])
        T = np.asarray([pt.T for pt in self.pts])

        if hasattr(self.pts[0], sA):
            sA = np.asarray([getattr(pt, sA) for pt in self.pts])
            sB = np.asarray([getattr(pt, sB) for pt in self.pts])
            sC = np.asarray([getattr(pt, sC) for pt in self.pts])
        else:
            sA = np.asarray([np.nan] * len(self.pts))
            sB = np.asarray([np.nan] * len(self.pts))
            sC = np.asarray([np.nan] * len(self.pts))
        # il faut squeezer les vecteurs parce que des fois on se retrouve
        # avec des matrices

        if time_as_datetime:
            Tout = conv.posix2dt(T)
        else:
            Tout = T

        sq = np.squeeze
        outtup = (sq(A), sq(B), sq(C), sq(Tout), sq(sA), sq(sB), sq(sC))
        if specific_output == None:
            return outtup
        elif type(specific_output) is int:
            return outtup[specific_output]
        else:
            log.info("this mode must be implemented ;)")
            log.info("use an int as index instead")
            return outtup

    def to_dataframe(self, coortype="XYZ", anex_key_list=None):
        """
        Export the TimeSerie Object as DataFrame

        Parameters
        ----------
        coortype : str or iterable of str.
            The coordinates type exported to the DataFrame.
            'XYZ', 'FLH', 'ENU', 'NED'
            can be also an iterable like ('XYZ','FLH')
            The default is 'XYZ'.
        anex_key_list : list of str, optional
            list of point's 'anex' keys to be added as columns in the DataFrame.
            The default is None.
        Returns
        -------
        df : DataFrame
            output DataFrame.
        """

        if not utils.is_iterable(coortype):
            coortype = (coortype,)

        col_stk = tuple()
        col_name_stk = []

        for icoty, coty in enumerate(coortype):
            A, B, C, T, sA, sB, sC = self.to_list(coty)

            if coty == "UTM":
                cotycolnam = ["Eutm", "Nutm", "Uutm"]
            else:
                cotycolnam = coty

            if icoty == 0:
                Tdt = conv.posix2dt(T)
                col_stk = col_stk + (Tdt, T, A, B, C, sA, sB, sC)
                col_name_stk = (
                    ["Tdt", "T"]
                    + [e for e in cotycolnam]
                    + ["s" + e for e in cotycolnam]
                )
            else:
                col_stk = col_stk + (A, B, C, sA, sB, sC)
                col_name_stk = [e for e in coty] + ["s" + e for e in coty]

            if anex_key_list:
                for key in anex_key_list:
                    val_list = [
                        pt.anex[key] if key in pt.anex.keys() else np.nan
                        for pt in self.pts
                    ]
                    col_stk = col_stk + (val_list,)
                    col_name_stk = col_name_stk + [key]

        big = np.column_stack(col_stk)
        df = pd.DataFrame(big)
        df.columns = col_name_stk
        df = df.infer_objects()

        return df

    def sort(self):
        """
        Internal method to sort the point in the TimeSerie Object

        Returns
        -------
        None.

        """
        self.pts.sort(key=lambda x: x.T)

    def plot(
        self,
        coortype="ENU",
        diapt=2,
        alpha=0.8,
        fig=1,
        errbar=True,
        symbol=".",
        errbar_width=1,
        ylim=None,
    ):
        """
        Plot data in a TimeSerie Object

        Parameters
        ----------
        coortype : str, optional
            The coordinates type. The default is 'ENU'.
        diapt : float, optional
            Point diameter. The default is 2.
        alpha : float, optional
            Alpha (transparency) of points. The default is 0.8.
        fig : int or Figure object, optional
            Figure ID where the data will be plotted
            can accept a int (id of a Figure)
            OR the figure Object itself.
            The default is 1.
        errbar : bool, optional
            Plot the error bars. The default is True.
        symbol : str, optional
            symbol. The default is '.'.
        errbar_width : float, optional
            coefficient for the error bar size. The default is 1.
        ylim : tuple, optional
            Y-axis limits. The default is None.

        Returns
        -------
        The matplotlib Figure object.

        """

        log.setLevel(logging.INFO)

        try:
            A, B, C, T, sA, sB, sC = self.to_list(coortype=coortype)
        except TypeError as tyer:
            log.error("unable to get coordinates")
            log.info("TRICK : check if the given coortype is in the timeserie")
            raise tyer

        # Define titles and labels based on coordinate type
        coord_config = {
            "ENU": ("East", "North", "Up", "displacement (m)"),
            "XYZ": ("X", "Y", "Z", "displacement (m)"),
            "FLH": ("Phi", "Lambda", "Haut", "displacement (m)"),
            "UTM": ("East (UTM)", "North (UTM)", "Up", "displacement (m)"),
        }

        Atitle, Btitle, Ctitle, yylabel = coord_config.get(
            coortype, ("A", "B", "C", "displacement (??)")
        )

        log.info("plot : %s, pts : %s", self.nbpts, self.stat)

        Tdt = conv.posix2dt(T)

        # Get figure number
        if isinstance(fig, int):
            fig_num = fig
        elif isinstance(fig, plt.Figure):
            fig_num = fig.number
        else:
            fig_num = 1

        # Label for the plot
        name4plot = self.name[:10] if self.name else self.stat

        # Check if figure already exists, reuse axes if yes, create it if no
        if plt.fignum_exists(fig_num):
            figobj = plt.figure(fig_num)
            # Reuse existing axes if the figure has 3 axes
            if len(figobj.axes) == 3:
                axes = figobj.axes
            else:
                # Clear and create new axes if wrong number of axes
                figobj.clear()
                axes = figobj.subplots(3, 1, sharex=True)
        else:
            figobj, axes = plt.subplots(3, 1, num=fig_num, sharex=True)
        figobj.suptitle(self.stat)

        # Component data and titles
        components = [
            (A, sA, Atitle),
            (B, sB, Btitle),
            (C, sC, Ctitle),
        ]

        # Common plot parameters
        plot_kwargs = {
            'label': name4plot,
            'markersize': diapt,
            'alpha': alpha,
        }

        errbar_kwargs = {
            **plot_kwargs,
            'fmt': symbol,
            'ecolor': 'xkcd:light grey',
            'elinewidth': errbar_width,
        }

        # Loop through components and plot
        for ax, (data, sigma, title) in zip(axes, components):
            if errbar:
                ax.errorbar(Tdt, data, sigma, **errbar_kwargs)
            else:
                ax.plot(Tdt, data, symbol, **plot_kwargs)
            ax.set_ylabel(yylabel)
            ax.set_title(title)
            ax.legend()
            if ylim:
                ax.set_ylim(ylim)

        # Set x-label only on bottom subplot
        axes[-1].set_xlabel("Date")

        figobj.autofmt_xdate()
        figobj.set_size_inches(8.27, 11.69)
        figobj.tight_layout()
        plt.subplots_adjust(top=0.93)

        return figobj

    def plot_discont(self, fig=1):
        """
        Plot discontinuties of a TimeSerie Object contained in discont list

        Parameters
        ----------
        fig : int or Figure object, optional
            Figure ID where the data will be plotted
            can accept a int (id of a Figure)
            OR the figure Object itself.
            The default is 1.

        Returns
        -------
        None.

        """

        if not self.bool_discont:
            log.warning("no discontinuities in TimeSerie")
            return None

        if type(fig) is int:
            figobj = plt.figure(fig)
        elif type(fig) is plt.Figure:
            figobj = fig

        for ax in figobj.axes:
            stats.plot_vertical_bar_ax(self.discont, ax, "r")

        if self.bool_discont_manu:
            for ax in figobj.axes:
                stats.plot_vertical_bar_ax(self.discont_manu, ax, "g")

    #        figobj.axes[1]
    #        stats.plot_vertical_bar(self.discont)
    #
    #        figobj.axes[2]
    #        stats.plot_vertical_bar(self.discont)

    def discont_manu_click(self, fig=1):
        """
        manual discontinuities are both recorded in the "main" discont list
        and in a new discont_manu list,
        thus the manual discontinuites can be identified

        IMPORTANT : cursor objects (multi , cid)
                    must be stored as global variables like this :
                    multi , cid = tsout.discont_manu_click()


        NOTE : This method was created before point_n_click_plot():
            this other one is more complete
            both has to be merged ASAP !!!!!

        """

        if type(fig) is int:
            figobj = plt.figure(fig)
        elif type(fig) is plt.Figure:
            figobj = fig

        if not self.bool_discont:
            self.discont = []

        if not self.bool_discont_manu:
            self.discont_manu = []

        log.info("press SPACE to record a manual discontinuity")

        def onclick_discont(event):
            ix, iy = (
                matplotlib.dates.num2date(event.xdata).replace(tzinfo=None),
                event.ydata,
            )

            log.info("discontinuity recorded : %s", ix)
            for ax in figobj.axes:
                stats.plot_vertical_bar_ax([ix], ax, "g")
            #

            self.bool_discont = True
            self.bool_discont_manu = True

            self.discont.append(ix)
            self.discont = sorted(self.discont)

            self.discont_manu.append(ix)
            self.discont_manu = sorted(self.discont_manu)

            # figobj.show()
            plt.draw()

            return None

        multi = MultiCursor(figobj.canvas, figobj.axes, color="k", lw=1)
        cid = figobj.canvas.mpl_connect("key_press_event", onclick_discont)

        return multi, cid

    def initype(self):
        L = [pt.initype for pt in self.pts]
        return Counter(L).most_common(1)[0][0]

    def ENUcalc(self, refENU):
        """
        Method to determine the ENU components based on a reference point

        Parameters
        ----------
        refENU : Point Object or TimeSeriePoint Object
            Reference point.

        Returns
        -------
        None.

        """
        if refENU.__class__.__name__ == "Point":
            self.refENU = refENU
            [pt.ENUcalc_pt(refENU) for pt in self.pts]
            self.boolENU = True
            self.bool_interp_uptodate = False
            self.interp_set()
        elif refENU.__class__.__name__ == "TimeSeriePoint":
            self.refENU = refENU.mean_posi()
            refENU.interp_set()
            [
                pt.ENUcalc_pt(
                    Point(
                        A=refENU.XfT(pt.T),
                        B=refENU.YfT(pt.T),
                        C=refENU.ZfT(pt.T),
                        initype="XYZ",
                        T=pt.T,
                    )
                )
                for pt in self.pts
            ]

            self.boolENU = True
            self.bool_interp_uptodate = False
            self.interp_set()

    def ENUcalc_from_mean_posi(self, mean_type="median"):
        """
        Method to determine the ENU components based directly
        on the mean/median position

        Returns
        -------
        None.

        """
        self.ENUcalc(self.mean_posi(mean_type="median"))
        return None

    def ENUcalc_from_first_posi(self):
        """
        Method to determine the ENU components based directly
        on the mean/median position

        Returns
        -------
        None.

        """
        self.ENUcalc(self.pts[0])
        return None

    def from_uniq_point(self, Point, startdate, enddate, pas=1):
        self.del_data()

        if type(startdate) == dt.datetime:
            startdate = conv.dt2posix(startdate)
        if type(enddate) == dt.datetime:
            enddate = conv.dt2posix(enddate)

        N = int(np.round(enddate - startdate / pas))
        # datelist = np.arange(startdate,enddate,pas)

        Point.Tset(startdate)

        for i in range(N):
            Point.Tset(Point.T + pas)
            self.add_point(copy.copy(Point))

        self.i_nomi = pas

    def UTMcalc(self):
        """
        Method to determine the UTM E and N projected coordinates

        Parameters
        ----------

        Returns
        -------
        None.

        """
        self.boolUTM = True
        [pt.UTMcalc_pt() for pt in self.pts]

    def time_win(self, windows, mode="keep"):
        """IL EST TRES DANGEREUX DE L'APPLIQUER UN FENETRAGE A SOI MEME"""
        self.__dict__ = time_series.time_win(self, windows, mode).__dict__

    def interp_set(self, interptype="slinear"):
        """
        Method to set the coordinate interpolators

        Parameters
        ----------
        interptype : TYPE, optional
            Interpolation type. The default is 'slinear'.

        Returns
        -------
        None.

        """
        if (not hasattr(self.pts[0], "E")) or np.isnan(self.pts[0].E) == True:
            log.warning("no ENU for " + self.name)
        else:
            E, N, U, T, _, _, _ = self.to_list("ENU")
            self.EfT = scipy.interpolate.interp1d(
                T, E, bounds_error=False, kind=interptype
            )
            self.NfT = scipy.interpolate.interp1d(
                T, N, bounds_error=False, kind=interptype
            )
            self.UfT = scipy.interpolate.interp1d(
                T, U, bounds_error=False, kind=interptype
            )

        if (not hasattr(self.pts[0], "X")) or np.isnan(self.pts[0].X) == True:
            log.warning("no XYZ for " + self.name)
        else:

            X, Y, Z, T, _, _, _ = self.to_list("XYZ")

            self.XfT = scipy.interpolate.interp1d(
                T, X, bounds_error=False, kind=interptype
            )
            self.YfT = scipy.interpolate.interp1d(
                T, Y, bounds_error=False, kind=interptype
            )
            self.ZfT = scipy.interpolate.interp1d(
                T, Z, bounds_error=False, kind=interptype
            )

        if (not hasattr(self.pts[0], "F")) or np.isnan(self.pts[0].L) == True:
            log.warning("no FLH for " + self.name)
        else:
            F, L, H, T, _, _, _ = self.to_list("FLH")

            self.FfT = scipy.interpolate.interp1d(
                T, F, bounds_error=False, kind=interptype
            )
            self.LfT = scipy.interpolate.interp1d(
                T, L, bounds_error=False, kind=interptype
            )
            self.HfT = scipy.interpolate.interp1d(
                T, H, bounds_error=False, kind=interptype
            )

        if (not hasattr(self.pts[0], "Eutm")) or np.isnan(self.pts[0].Eutm) == True:
            # log.warning("no UTM for " + self.name)
            pass
        else:
            Eutm, Nutm, Uutm, T, _, _, _ = self.to_list("UTM")

            self.EutmfT = scipy.interpolate.interp1d(
                T, Eutm, bounds_error=False, kind=interptype
            )
            self.NutmfT = scipy.interpolate.interp1d(
                T, Nutm, bounds_error=False, kind=interptype
            )
            self.UutmfT = scipy.interpolate.interp1d(
                T, Uutm, bounds_error=False, kind=interptype
            )

        self.bool_interp_uptodate = True

    def interp_get(self, T, coortype="ENU"):
        """
        Method to get the coordinate interpolators

        Parameters
        ----------
        T : float or list of float
            Time (IN POSIX Time) where the interpolation is wished.
        coortype : str, optional
            The coordinates type. The default is 'ENU'.

        Returns
        -------
        tsout :
            DESCRIPTION.

        """

        if self.bool_interp_uptodate == False:
            log.warning("interp obsolete, recalcul auto")
            self.interp_set()

        tsout = copy.copy(self)
        tsout.del_data()

        if not utils.is_iterable(T):
            T = np.array([T])

        if coortype == "ENU":
            A = self.EfT(T)
            B = self.NfT(T)
            C = self.UfT(T)

        if coortype == "XYZ":
            A = self.XfT(T)
            B = self.YfT(T)
            C = self.ZfT(T)

        if coortype == "FLH":
            A = self.FfT(T)
            B = self.LfT(T)
            C = self.HfT(T)

        if coortype == "UTM":
            A = self.EutmfT(T)
            B = self.NutmfT(T)
            C = self.UutmfT(T)

        for i in range(len(T)):
            tsout.add_point(Point(A[i], B[i], C[i], T=T[i], initype=coortype))

        return tsout

    def set_discont(self, indiscont):
        """
        Method to set the discontinuties list

        Parameters
        ----------
        indiscont : list of time
            Discontinuities in the TimeSerie.

        Returns
        -------
        None.

        """
        self.discont = indiscont
        self.bool_discont = True

    def mean_posi(self, coortype="XYZ", outtype="point", mean_type="median"):
        """
        Method to determine the mean position of the TimeSerie

        Parameters
        ----------
        coortype : TYPE, optional
            The coordinates type. The default is 'XYZ'.
        outtype : TYPE, optional
            'point' or 'tuple'. The default is 'point'.
        mean_type : TYPE, optional
            'mean' or 'median'. The default is 'median'.

        Returns
        -------
        Point or coordinates tuple

        """

        # special case where only one point
        if self.nbpts == 1:
            return copy.copy(self)

        A, B, C, T, sA, sB, sC = self.to_list(coortype=coortype)

        if mean_type == "mean":
            Aout = np.nanmean(A)
            Bout = np.nanmean(B)
            Cout = np.nanmean(C)
        elif mean_type == "median":
            Aout = np.nanmedian(A)
            Bout = np.nanmedian(B)
            Cout = np.nanmedian(C)

        Tout = (np.max(T) - np.min(T)) / 2

        if outtype == "point":
            out = Point(Aout, Bout, Cout, Tout)
        elif outtype == "tuple":
            out = (Aout, Bout, Cout, Tout)
        else:
            out = (Aout, Bout, Cout, Tout)

        return out

    def add_offset(self, dA, dB, dC, coortype="ENU"):
        """
        Method to add an offset to all points in the TimeSerie
        """
        for pt in self.pts:
            pt.add_offset(dA, dB, dC, coortype="ENU")

    def decimate(self, dec):
        """
        Method to decimate a TimeSerie

        Parameters
        ----------
        dec : int
            keep 1/dec point in the TimeSerie.

        Returns
        -------
        None.

        """
        self.__dict__ = time_series.decimate_cleaner(self, dec).__dict__
        # decimate_cleaner(self,dec,True)

    def find_point(self, tin, tol=0.001, stop_when_found=True):
        """
        Method to find a specific point according to its timestamp

        Parameters
        ----------
        tin : float or datetime
            timestamp of the researched point.
        tol : float, optional
            tolerence of the research. The default is 0.001.
        stop_when_found : bool, optional
            Stop the research when a point is found. The default is True.

        Returns
        -------
        Point Object
            Point Found.
        int or list of int
            index of the point.

        """

        find = False

        if type(tin) is dt.datetime:
            tin = conv.dt2posix(tin)

        tmin = self.pts[0].T
        tmax = self.pts[-1].T

        if not (tmin <= tin <= tmax):
            log.warning("t !€ [tmin , tmax]")

        if stop_when_found:
            for i, p in enumerate(self.pts):
                if np.abs(p.T - tin) < tol:
                    find = True
                    break
            if find:
                return p, i
            else:
                return Point(), np.nan

        else:
            pts_stk = []
            i_stk = []
            for i, p in enumerate(self.pts):
                if np.abs(p.T - tin) < tol:
                    pts_stk.append(p)
                    i_stk.append(i)
            return pts_stk, i_stk

    def rm_duplicat_pts(self, coortype="XYZ"):
        """
        Remove duplicate points from the time series.

        Parameters
        ----------
        coortype : str, optional
            The coordinate type to consider for duplication check. The default is "XYZ".

        Returns
        -------
        None
        """
        T = self.to_dataframe(coortype)["T"]

        dup_bool = T.duplicated()

        if dup_bool.sum() > 0:
            log.warning(
                "%s duplicated point(s) removed for %s", dup_bool.sum(), self.name
            )

        self.pts = list(pd.Series(self.pts)[np.logical_not(dup_bool)])


#  ______                      _                      _        _    _____ _
# |  ____|                    (_)                    | |      | |  / ____| |
# | |__  __  ___ __   ___ _ __ _ _ __ ___   ___ _ __ | |_ __ _| | | |    | | __ _ ___ ___  ___  ___
# |  __| \ \/ / '_ \ / _ \ '__| | '_ ` _ \ / _ \ '_ \| __/ _` | | | |    | |/ _` / __/ __|/ _ \/ __|
# | |____ >  <| |_) |  __/ |  | | | | | | |  __/ | | | || (_| | | | |____| | (_| \__ \__ \  __/\__ \
# |______/_/\_\ .__/ \___|_|  |_|_| |_| |_|\___|_| |_|\__\__,_|_|  \_____|_|\__,_|___/___/\___||___/
#             | |
#             |_|


class Attitude:
    def __init__(
        self, R=0, P=0, Y=0, T=0, sR=0, sP=0, sY=0, devID="NULL", angtype="deg"
    ):

        self.Tset(T)
        self.devID = devID

        if angtype == "deg":
            self.R = R
            self.P = P
            self.Y = Y

        elif angtype == "rad":
            self.R = np.rad2deg(R)
            self.P = np.rad2deg(P)
            self.Y = np.rad2deg(Y)
        else:
            raise Exception("Mauvais angtype")

        self.Qcalc()

    def __call__(self):
        return self.R, self.P, self.Y, self.Tdt

    def __repr__(self):
        return "{},{},{},{}".format(self.R, self.P, self.Y, self.Tdt)

    def Tset(self, T=0):
        if T == 0:
            T = dt.datetime.now()

        if type(T) == dt.datetime:
            self.Tdt = T
            self.T = conv.dt2posix(T)

        else:
            self.T = float(T)
            self.Tdt = conv.posix2dt(float(T))

    def RPYget(self):
        return self.R, self.P, self.Y

    def RPYset(self, R=0, P=0, Y=0, sR=0, sP=0, sY=0):
        self.R = R
        self.P = P
        self.Y = Y
        self.sR = sR
        self.sP = sP
        self.sY = sY

    def Qcalc(self):
        self.Q = conv.quaternion(self.R, self.P, self.Y, "deg")
        return None


class TimeSerieObs(object):
    """LES DIFFERENCES AVEC TSPOINT
    * Les objets ne contiennent qu'un type de données sous une seul forme
    (a la difference d'un point qui peut exister sous plusieurs formes)
    * Dans un fichier en input, il peut y avoir plusieurs "devices"
      => les fonctions de lectures produisent donc obligatoirement des listes
      de TS (le cas échéant une liste à 1 élt)
      => la methode readfile() nécessite donc l'indice de la device"""

    def __init__(self, typeobs="NULL", filepath=""):

        self.obs = []
        self.nbobs = 0
        self.i_nomi = 0
        self.typeobs = typeobs
        # ASM Attitude
        self.bool_interp_uptodate = False

        self.meta_set(filepath)

    def meta_set(self, path="", devID="NULL", name=""):
        self.path = path
        self.devID = devID

        bn = os.path.basename(path)
        dn = os.path.dirname(path)

        if name == "":
            if bn == "tdp_final":
                self.name = os.path.basename(dn)
            else:
                self.name = bn

        self.interval_nominal()

    def del_data(self):
        self.obs = []
        self.nbobs = 0

    def readfile(self, filein, indtab=0):
        log.info("selection device %s", indtab)
        temp = files_rw.read_all_obs(filein)[indtab]
        self.__dict__ = temp.__dict__

        self.interp_set()

    def add_obs(self, inObs):
        self.obs.append(inObs)
        self.nbobs = len(self.obs)

        self.bool_interp_uptodate = False

    def aleaobs(self):
        iobs = np.random.randint(0, self.nbobs)
        log.info("observation no " + str(iobs))

        log.info(self.obs[iobs])

        return self.obs[iobs]

    def interval_nominal(self):
        if len(self.obs) < 2:
            self.i_nomi = 0
        else:
            Ttemp = np.sort([o.T for o in self.obs])
            self.i_nomi = np.round(np.min(np.diff(Ttemp)), 1)

        return self.i_nomi

    def timewin(self, windows, mode="keep"):
        """IL EST TRES DANGEREUX DE L'APPLIQUER UN FENETRAGE A SOI MEME"""
        self.__dict__ = time_series.time_win(self, windows, mode).__dict__

    def startdate(self):
        return self.obs[0].T

    def enddate(self):
        return self.obs[-1].T

    def to_list(self):
        if self.typeobs == "NULL":
            log.error("pas de typeobs defini (NULL)")
            return 0

        if self.typeobs == "RPY":
            A, B, C = "R", "p", "Y"
            sA, sB, sC = "sR", "sP", "sY"

        A = np.asarray([getattr(o, A) for o in self.obs])
        B = np.asarray([getattr(o, B) for o in self.obs])
        C = np.asarray([getattr(o, C) for o in self.obs])
        T = np.asarray([o.T for o in self.obs])

        if hasattr(self, sA):
            sA = np.asarray([getattr(o, sA) for o in self.obs])
            sB = np.asarray([getattr(o, sB) for o in self.obs])
            sC = np.asarray([getattr(o, sC) for o in self.obs])

        else:
            sA = np.asarray([np.nan] * len(self.obs))
            sB = np.asarray([np.nan] * len(self.obs))
            sC = np.asarray([np.nan] * len(self.obs))

        return A, B, C, T, sA, sB, sC

    def interp_set(self, interptype="slinear"):

        if self.typeobs == "NULL":
            log.error("no typeobs defined (NULL)")
            return 0

        if self.typeobs == "RPY":

            R, P, Y, T, _, _, _ = self.to_list()

            self.RfT = scipy.interpolate.interp1d(
                T, R, bounds_error=False, kind=interptype
            )
            self.PfT = scipy.interpolate.interp1d(
                T, P, bounds_error=False, kind=interptype
            )
            self.YfT = scipy.interpolate.interp1d_ang(
                T, Y, bounds_error=False, kind=interptype
            )

        self.bool_interp_uptodate = True

    def interp_get(self, T):

        if self.bool_interp_uptodate == False:
            log.warning("interp obsolete, recalcul auto")
            self.interp_set()

        tsout = copy.copy(self)
        tsout.del_data()

        if not utils.is_iterable(T):
            T = np.array([T])

        if self.typeobs == "NULL":
            log.error("no typeobs defined (NULL)")
            return 0

        if self.typeobs == "RPY":
            A = self.RfT(T)
            B = self.PfT(T)
            C = self.YfT(T)

            for i in range(len(T)):
                tsout.add_obs(Attitude(A[i], B[i], C[i], T=T[i]))

        return tsout

    def plot(self, diapt=10, alpha=0.8, fig=1, new_style=True):

        A, B, C, T, sA, sB, sC = self.to_list()

        log.info("plot : %s, pts : %s", self.nbpts, self.stat)

        if self.typeobs == "RPY":
            listtitle = ["", "Roll", "Pitch", "Yaw"]
        else:
            listtitle = ["", "", "", ""]

        namest = 0
        namend = 10
        Tdt = conv.posix2dt(T)

        if new_style:
            styleint = 410
        else:
            styleint = 220

        plt.figure(fig)
        plt.subplot(styleint + 1)
        plt.axis("equal")
        plt.plot(
            A,
            B,
            ".",
            label=str(self.devID)[namest:namend],
            markersize=diapt,
            alpha=alpha,
        )
        plt.legend()
        plt.title(listtitle[0])

        plt.subplot(styleint + 2)
        plt.plot(
            Tdt,
            A,
            ".",
            label=str(self.devID)[namest:namend],
            markersize=diapt,
            alpha=alpha,
        )
        plt.legend()
        plt.title(listtitle[1])

        plt.subplot(styleint + 3)
        plt.plot(
            Tdt,
            B,
            ".",
            label=str(self.devID)[namest:namend],
            markersize=diapt,
            alpha=alpha,
        )
        plt.legend()
        plt.title(listtitle[2])

        plt.subplot(styleint + 4)
        plt.plot(
            Tdt,
            C,
            ".",
            label=str(self.devID)[namest:namend],
            markersize=diapt,
            alpha=alpha,
        )
        plt.legend()
        plt.title(listtitle[3])

    #  _____       _                      _   _             _____  _       _
    # |_   _|     | |                    | | (_)           |  __ \| |     | |
    #   | |  _ __ | |_ ___ _ __ __ _  ___| |_ ___   _____  | |__) | | ___ | |_
    #   | | | '_ \| __/ _ \ '__/ _` |/ __| __| \ \ / / _ \ |  ___/| |/ _ \| __|
    #  _| |_| | | | ||  __/ | | (_| | (__| |_| |\ v /  __/ | |    | | (_) | |_
    # |_____|_| |_|\__\___|_|  \__,_|\___|\__|_|_\_/ \___| |_|    |_|\___/ \__|
    # |  __ \    (_)     | |     ___     / ____| (_)    | |
    # | |__) |__  _ _ __ | |_   ( _ )   | |    | |_  ___| | __
    # |  ___/ _ \| | '_ \| __|  / _ \/\ | |    | | |/ __| |/ /
    # | |  | (_) | | | | | |_  | (_>  < | |____| | | (__|   <
    # |_|   \___/|_|_| |_|\__|  \___/\/  \_____|_|_|\___|_|\_\

    """
    USAGE :

        Then :

            PnC = point_n_click_plot()

            multi , cid = PnC(fig=6)

            PnC.selectedX

        i.e. :

            Create an object point_n_click_plot (here it is PnC in the exemple below)

            Call the object like a function with the id of the plot figure

            Make your selection using SPACE key

            Get your results in a list called PnC.selectedX




    """


class point_n_click_plot:
    """
    This method allow to do "point and click" on a plot, to localize offsets
    for instance

    Usage
    -----
    Data have to be ploted already in a figure

    .. code-block:: python

        PnC = point_n_click_plot()
        multi , cid = PnC(fig=1,Xdata_are_time=True)
        PnC.selectedX

    i.e.

    Create an object point_n_click_plot (here it is PnC in the exemple below) \n
    Call the object like a function with as 1st argument the id of the plot figure or the plot figure itself \n
    Make your selection using the SPACE key \n
    Get your results in a list called PnC.selectedX

    Important
    ---------

    cursor objects (i.e. multi & cid)
    must be stored as global variables when you call the method
    like this :

    .. code-block:: python

        multi , cid = PnC(fig=1)

    """

    def __init__(self):
        self.selectedX = []
        self.ver_bar_stk = []

    def __call__(self, fig=1, Xdata_are_time=True):
        """
        IMPORTANT : cursor objects (i.e. multi & cid)
                    must be stored as global variables when you call the method
                    like this :
                    multi , cid = PnC(fig=1)
        """

        if type(fig) is int:
            figobj = plt.figure(fig)
        elif type(fig) is matplotlib.figure.Figure:
            figobj = fig

        log.info(
            "press SPACE to record a X-value, \n       press R to Remove the previously recorded one"
        )

        def onclick_discont(event):

            if event.key == " ":
                if Xdata_are_time:
                    ix, iy = (
                        matplotlib.dates.num2date(event.xdata).replace(tzinfo=None),
                        event.ydata,
                    )
                else:
                    ix, iy = event.xdata, event.ydata

                log.info("X value recorded : ", ix)

                for ax in figobj.axes:
                    out_bar_list = stats.plot_vertical_bar_ax(
                        [ix], ax, "b", linewidth=1
                    )

                    self.ver_bar_stk.append(out_bar_list[0])

                self.selectedX.append(ix)

                plt.draw()

            elif event.key in ("r", "R") and len(self.selectedX) > 0:
                last = self.selectedX[-1]

                self.selectedX.remove(last)

                last_bars = self.ver_bar_stk[-len(figobj.axes) :]

                for bar in last_bars:
                    self.ver_bar_stk.remove(bar)
                    bar.remove()

                log.info("value removed : ", last)

                plt.draw()

            return None

        multi = MultiCursor(figobj.canvas, figobj.axes, color="k", lw=1)
        cid = figobj.canvas.mpl_connect("key_press_event", onclick_discont)

        return multi, cid
