# -*- coding: utf-8 -*-

##--------------------------------------#######
#                   Interpolation                    #
##--------------------------------------#######
#    WxGeometrie
#    Dynamic geometry, graph plotter, and more for french mathematic teachers.
#    Copyright (C) 2005-2013  Nicolas Pourcelot
#
#    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.
#
#    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, write to the Free Software
#    Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA


from operator import attrgetter

from numpy import array, arange, append

try:
    # wxgeometrie can be imported even if scipy is not found.
    # Only Interpolation_polynomiale_par_morceaux will fail.
    from scipy.interpolate import BPoly
    scipy_found = True
except ImportError:
    scipy_found = False
    class BPoly:
        def from_derivatives(*args, **kw):
            raise NotImplementedError('Error: scipy library not found !')

from .objet import Ref, Argument, Arguments

from .courbes import Courbe_generique
##from .lignes import Tangente_courbe
from .contexte import contexte

from ..pylib import fullrange
from .. import param



class Interpolation_generique(Courbe_generique):
    """Classe mère de toutes les interpolations."""

    points = __points = Arguments("Point_generique")

    _style_defaut = param.interpolations

    def __init__(self, *points, **styles):
        self.__points = points = tuple(Ref(pt) for pt in points)
        Courbe_generique.__init__(self, **styles)


    def _affiche_extremites(self, vec_deb = None, vec_fin = None):
        couleur = self.style("couleur")
        niveau = self.style("niveau")
        epaisseur = self.style("epaisseur")
        debut = self.style("debut")
        fin = self.style("fin")

        # Début de la courbe
        plot = self._representation[-2]
        x, y = self.__points[0].coordonnees
        a, b = self.__points[1].coordonnees
        if vec_deb is None:
            vec_deb = (a - x, b - y)
        if debut is True:
            plot.set_data((x,), (y,))
            plot.set(visible=True, color=couleur, marker="o", markeredgecolor=couleur,
                     markersize=2*self.canvas.taille["o"], linewidth=epaisseur)
            plot.zorder = niveau
        elif debut is False:
            arc = self.rendu.arc(x, y, vec_deb)
            plot.set_data(*arc.get_data()) # À TESTER !!
            plot.set(visible=True, color=couleur, linewidth=epaisseur, marker="")
            plot.zorder = niveau
        else:
            plot.set_visible(False)

        # Fin de la courbe
        plot = self._representation[-1]
        x, y = self.__points[-1].coordonnees
        a, b = self.__points[-2].coordonnees
        if vec_fin is None:
            vec_fin = (a - x, b - y)
        if fin is True:
            plot.set_data((x,), (y,))
            plot.set(visible=True, color=couleur, marker="o", markeredgecolor=couleur,
                     markersize=2*self.canvas.taille["o"], linewidth=epaisseur)
            plot.zorder = niveau
        elif fin is False:
            arc = self.rendu.arc(x, y, vec_fin)
            plot.set_data(*arc.get_data()) # À TESTER !!
            plot.set(visible=True, color=couleur, linewidth=epaisseur, marker="")
            plot.zorder = niveau
        else:
            plot.set_visible(False)




class Interpolation_lineaire(Interpolation_generique):
    """Une interpolation linéaire.

    Interpolation entre les points donnés par des segments joints (courbe de classe C0).
    """

    points = __points = Arguments("Point_generique")

    def __init__(self, *points, **styles):
        if styles.get("points", None):
            points = styles.pop("points")
        elif len(points) == 1 and isinstance(points[0], (list, tuple)):
            points = points[0]
        self.__points = points = tuple(Ref(pt) for pt in points)
        Interpolation_generique.__init__(self, *points, **styles)

    def _creer_figure(self):
        n = len(self.__points)
        couleur = self.style("couleur")
        niveau = self.style("niveau")
        style = self.style("style")
        epaisseur = self.style("epaisseur")
        if not self._representation:
            self._representation = [self.rendu.ligne() for i in range(n + 1)]

        self._xarray = array([])
        self._yarray = array([])

        if n < 2:
            return

        pas = self.canvas.pas()

        for i in range(n - 1):
            plot = self._representation[i]
            x1, y1 = self.__points[i].coordonnees
            x2, y2 = self.__points[i+1].coordonnees
            plot.set_data(array((x1, x2)), array((y1, y2)))
            plot.set(color=couleur, linestyle=style, linewidth=epaisseur)
            plot.zorder = niveau
            # TODO: améliorer l'algo de détection (notamment si l'échelle
            # sur les 2 axes n'est pas la même).
            # Utiliser l'algorithme des segments.
            if abs(y2 - y1) < abs(x2 - x1):
                x_array = arange(x1, x2, pas if x1 < x2 else -pas)
                a = (y2 - y1)/(x2 - x1)
                b = y1 - a*x1
                y_array = a*x_array + b
            elif abs(y2 - y1) > contexte['tolerance']:
                y_array = arange(y1, y2, pas if y1 < y2 else -pas)
                a = (x2 - x1)/(y2 - y1)
                b = x1 - a*y1
                x_array = a*y_array + b
            else:
                continue
            self._xarray = append(self._xarray, x_array)
            self._yarray = append(self._yarray, y_array)

        self._affiche_extremites()


    def image_par(self, transformation):
        # FIXME: l'image n'est plus correcte si le nombre de points est modifié ensuite
        # Par exemple, interp = Interpolation_lineaire(A, B), puis interp.points = (A, B, C)
        return self.__class__(*(transformation(point) for point in self.__points))




class Interpolation_quadratique(Interpolation_generique):
    """Une interpolation quadratique.

    Interpolation des points donnés par une courbe polynomiale par morceaux, et de classe C1.
    Pour chaque morceau, x(t)=at²+bt+c et y(t)=dt²+et+f, où t appartient à [0;1].
    """
    points = __points = Arguments("Point_generique")

    def __init__(self, *points, **styles):
        if "points" in styles:
            points = styles.pop("points")
        elif len(points) == 1 and isinstance(points[0], (list, tuple)):
            points = points[0]
        self.__points = points = tuple(Ref(pt) for pt in points)
        Interpolation_generique.__init__(self, *points, **styles)


    def _creer_figure(self):
        n = len(self.__points)
        couleur = self.style("couleur")
        niveau = self.style("niveau")
        style = self.style("style")
        epaisseur = self.style("epaisseur")
        if not self._representation:
            self._representation = [self.rendu.ligne() for i in range(n + 1)]

        self._xarray = array([])
        self._yarray = array([])

        if n < 2:
            return

        pas = self.canvas.pas()
        t = fullrange(0, 1, pas)
        for i in range(n - 1):
            plot = self._representation[i]
            x0, y0 = self.__points[i].coordonnees
            x1, y1 = self.__points[i+1].coordonnees
            if i == 0:
                dx0, dy0 = x1 - x0, y1 - y0
            a, b, c = x1 - dx0 - x0, dx0, x0
            d, e, f = y1 - dy0 - y0, dy0, y0
            u = (a*t + b)*t + c
            v = (d*t + e)*t + f
            plot.set_data(u, v)
            plot.set(color=couleur, linestyle=style, linewidth=epaisseur)
            plot.zorder = niveau

            dx0 = 2*a + b
            dy0 = 2*d + e

            self._xarray = append(self._xarray, u)
            self._yarray = append(self._yarray, v)

        self._affiche_extremites(vec_fin = (dx0, dy0))




class Interpolation_cubique(Interpolation_generique):
    """Une interpolation cubique.

    Interpolation des points donnés par une courbe polynomiale par morceaux, de vecteur tangent horizontal aux sommets, et de classe C1 en général (ie. si x_n!=x_{n-1}).
    Pour chaque morceau, x(t)=at^3+bt^2+ct+d et y(t)=et^3+ft^2+gt+h, où t appartient à [0;1], et y'(0)=y'(1)=0.
    """

    points = __points = Arguments("Point_generique")

    def __init__(self, *points, **styles):
        if "points" in styles:
            points = styles.pop("points")
        elif len(points) == 1 and isinstance(points[0], (list, tuple)):
            points = points[0]
        self.__points = points = tuple(Ref(pt) for pt in points)
        Interpolation_generique.__init__(self, *points, **styles)


    def _creer_figure(self):
        n = len(self.__points)
        couleur = self.style("couleur")
        niveau = self.style("niveau")
        style = self.style("style")
        epaisseur = self.style("epaisseur")
        courbure = self.style("courbure")
        if courbure is None:
            courbure = 1
        if not self._representation:
            self._representation = [self.rendu.ligne() for i in range(n + 1)]

        self._xarray = array([])
        self._yarray = array([])

        if n < 2:
            return

        pas = self.canvas.pas()
        t = fullrange(0, 1, pas)
        for i in range(n - 1):
            plot = self._representation[i]
            x0, y0 = self.__points[i].coordonnees
            x1, y1 = self.__points[i+1].coordonnees
            if i == 0:
                dx0 = x1 - x0
                dy0 = y1 - y0
            else:
                dx0 = courbure*abs(x1 - x0)
                if dx1 != 0:
                    dy0 = dy1/dx1*dx0
                else:
                    dx0 = dx1
                    dy0 = dy1
            if i < n - 2:
                x2, y2 = self.__points[i+2].coordonnees
                if (y0 > y1) is (y1 > y2):
                    dy1 = y2 - y0
                    dx1 = x2 - x0
                else:
                    dy1 = 0
                    dx1 = courbure*abs(x1 - x0)
            else:
                dy1 = y1 - y0
                dx1 = x1 - x0
            a = 2*(x0 - x1) + dx0 + dx1
            b = 3*(x1 - x0) -2*dx0 - dx1
            c = dx0
            d = x0
            e = 2*(y0 - y1) + dy0 + dy1
            f = 3*(y1 - y0) -2*dy0 - dy1
            g = dy0
            h = y0
            u = ((a*t + b)*t + c)*t + d
            v = ((e*t + f)*t + g)*t + h
            plot.set_data(u, v)
            plot.set(color=couleur, linestyle=style, linewidth=epaisseur)
            plot.zorder = niveau

            self._xarray = append(self._xarray, u)
            self._yarray = append(self._yarray, v)

        self._affiche_extremites(vec_deb = (dx0, dy0), vec_fin = (dx1, dy1))


class Interpolation_polynomiale_par_morceaux(Interpolation_generique):
    """Une courbe d'interpolation polynomiale par morceaux.
    Elle utilise l'interpolation par morceau de scipy pour construire la fonction,
    via la méthode scipy.interpolate.BPoly.from_derivatives()

    Elle passe par les points (xl, yl) et avec le nombre derive dans derivl à
    l'abscisse xl.

    exemple::

    >>> from wxgeometrie import *
    >>> A = Point(-1,-2)
    >>> B = Point(2,1)
    >>> C = Point(8,-3)
    >>> d = Interpolation_polynomiale_par_morceaux(A,B,C, derivees=[-1,0.5,2])

    :type derivee: list
    :param derivee: value of the diff number at each point

    :rtype : numpy.lib.polynomial

    """
    _style_defaut = param.interpolations_polynomiales_par_morceaux

    points = __points = Arguments("Point_interpolation")
    fonction = None # fonction d'interpolation

    def __init__(self, *points, **styles):
        debut = styles.pop("debut", True)
        fin = styles.pop("fin", True)
        if styles.get("points", None):
            points = styles.pop("points")
        # Accepter Interpolation_polynomiale_par_morceaux([A, B, C])
        # comme alias de Interpolation_polynomiale_par_morceaux(A, B, C).
        elif len(points) == 1 and isinstance(points[0], (list, tuple)):
            points = points[0]
        self.__points = points = tuple(Ref(pt) for pt in points)
        # en test: creation des tangentes: dictionnaire
        # key: nom du point, value: objet Tangente_courbe
        ##self.__tangentes = []
        # boucle avec ruse enumerate
        # cf http://docs.python.org/tutorial/datastructures.html#dictionaries
        ##for i, P in enumerate(points):
            ##dico = {'point': P, 'cdir': self.__derivees[i]}
            ##self.__tangentes.append(Tangente_courbe(**dico))

        Interpolation_generique.__init__(self, *points, **styles)


    @property
    def fonction(self):
        """Fonction wrapper vers la fonction de scipy BPoly.from_derivatives

        """
        pts = self.points_tries
        xl = [P.x for P in pts]
        yl = [P.y for P in pts]
        yl_cum = list(zip(yl, self._derivees()))
        return BPoly.from_derivatives(xl, yl_cum)


    def _derivees(self):
        derivees = []
        points = self.points_tries
        n = len(points)
        for i, P in enumerate(points):
            if P.derivee is None:
                if n == 1:
                    derivees.append(0)
                elif i == 0:
                    # Premier point de la courbe
                    A, B = points[:2]
                    dy = B.y - A.y
                    dx = B.x - A.x
                    derivees.append(dy/dx if dx else 0)
                elif i == n - 1:
                    # Dernier point de la courbe
                    A, B = points[-2:]
                    dy = B.y - A.y
                    dx = B.x - A.x
                    derivees.append(dy/dx if dx else 0)
                else:
                    # Il y a (au moins) un point avant et un point après
                    A, B, C = points[i - 1:i + 2]
                    if B.y >= max(A.y, C.y) or B.y <= min(A.y, C.y):
                        derivees.append(0)
                    else:
                        der = self.strategies.get(self.style('strategie'),
                                            self._strategie_pente_moyenne)
                        derivees.append(der(self, A, B, C))
            else:
                derivees.append(P.derivee)
        return derivees


    def _strategie_pente_moyenne(self, A, B, C):
        """La pente en B est celle de la droite (AC).

        Stratégie simple et qui donne un rendu assez esthétique en pratique.
        """
        dy = C.y - A.y
        dx = C.x - A.x
        return dy/dx if dx else 0

    def _strategie_pente_minimale(self, A, B, C):
        """Pente la plus faible entre celle de (AB) et celle de (BC).

        On prend la pente la plus faible, de façon à être sûr
        qu'à gauche et à droite du point considéré, la courbe d'interpolation
        reste comprise en ordonnées entre le point considéré et
        le point suivant.
        Cela facilite la construction d'extrema :
        si A, B, C sont trois points d'interpolation,
        avec B.y < A.y et B.y < C.y, alors on est assuré que
        la courbe ne descendra pas en dessous de B.y sur
        l'intervalle [A.x, C.x].
        """
        dy1 = B.y - A.y
        dx1 = B.x - A.x
        der1 = (dy1/dx1 if dx1 else 0)
        dy2 = C.y - B.y
        dx2 = C.x - B.x
        der2 = (dy2/dx2 if dx2 else 0)
        return der1 if abs(der1) < abs(der2) else der2

    def _strategie_moyenne_gauche_droite(self, A, B, C):
        """
        Moyenne de la pente moyenne à gauche et de la pente moyenne à droite.

        Stratégie peu concluante en pratique.
        Il faudrait utiliser un barycentre plutôt qu'une moyenne simple.
        """
        dy1 = B.y - A.y
        dx1 = B.x - A.x
        der1 = (dy1/dx1 if dx1 else 0)
        dy2 = C.y - B.y
        dx2 = C.x - B.x
        der2 = (dy2/dx2 if dx2 else 0)
        return .5*(der1 + der2)


    strategies = {'pente_moyenne': _strategie_pente_moyenne,
                  'pente_minimale': _strategie_pente_minimale,
                  'moyenne_gauche_droite': _strategie_moyenne_gauche_droite
                  }


    @property
    def points_tries(self):
        # TODO: mise en cache
        return sorted(self.__points, key=attrgetter('x'))


    def _creer_figure(self):
        points = self.points_tries
        n = len(points)
        couleur = self.style("couleur")
        niveau = self.style("niveau")
        style = self.style("style")
        epaisseur = self.style("epaisseur")
        if not self._representation:
            self._representation = [self.rendu.ligne() for i in range(3)]

        if n < 2:
            return
        # de même les tangentes sont recalculées
        # il doit y avoir moyen de faire moins de calculs
        # self.__tangentes = []
        # for i in range(len(self.__points)):
        #     self.__tangentes.append(Tangente_courbe(point = self.__points[i],\
        #                                                 cdir = self.__derivees[i]))
        #     #self.__tangentes[i]._creer_figure()

        pas = self.canvas.pas()
        plot = self._representation[0]
        x1, y1 = points[0].coordonnees
        x2, y2 = points[-1].coordonnees
        xarray = fullrange(x1, x2, pas)
        yarray = self.fonction(xarray)
        plot.set_data(xarray, yarray)
        plot.set(color=couleur, linestyle=style, linewidth=epaisseur)
        plot.zorder = niveau
        self._xarray = xarray
        self._yarray = yarray

        self._affiche_extremites()

    @property
    def xmin(self):
        return self.points_tries[0].x if self.__points else None

    @property
    def xmax(self):
        return self.points_tries[-1].x if self.__points else None

