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

##--------------------------------------#######
#           Mathlib 2 (sympy powered)         #
##--------------------------------------#######
#    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

## Cette librairie contient des fonctions mathématiques à usage interne

from functools import reduce
from numpy import roots as nroots

from sympy import Mul, Float, Basic, roots, sympify, div, Symbol
from ..pylib import warning
from .. import param


def is_float(val):
    return isinstance(val, (float, Float))

def is_pos(expr):
    return getattr(expr, "is_positive", float(expr) > 0)

def is_neg(expr):
    return getattr(expr, "is_negative", float(expr) < 0)

def is_var(expression, variable):
    return hasattr(expression, "has") and expression.has(variable)


def poly_factor(polynome, variable, corps = None, approchee = None):
    """Factorise un polynome à une variable.

    Le corps peut être R ou C.
    Par défaut, le corps de factorisation est celui des coefficients."""
    from .sympy_functions import simplifier_racines
    if approchee is None:
        # Paramètre utilisé en interne par 'l'interpreteur' de commandes
        # (cf. méth. evaluer() de la classe Interprete(), dans custom_objects.py)
        approchee = getattr(param, 'calcul_approche', False)

    if polynome.is_Mul:
        return reduce(lambda x,y:x*y, [poly_factor(fact, variable, corps = corps) for fact in polynome.args], 1)
    sym_poly = polynome.as_poly(variable)
    coeffs = sym_poly.all_coeffs()

    if any(is_float(coeff) for coeff in coeffs):
        approchee = True
        racines_brutes = {}.fromkeys(nroots(coeffs),  1)
    else:
        if corps == "R":
            if not all(coeff.is_real for coeff in coeffs):
                raise ValueError("factorisation dans 'R' impossible.")
        elif corps is None:
            if all(coeff.is_real for coeff in coeffs):
                corps = "R"
            else:
                corps = "C"
        racines_brutes = roots(polynome, variable, cubics=True, quartics=True)
    racines = list((simplifier_racines(racine), mult) for racine, mult in racines_brutes.items())

    if approchee:
        nbr_racines = sum(multiplicite for racine, multiplicite in racines)
        if nbr_racines < sym_poly.degree():
            # On cherche une approximation des racines manquantes
            sol_approchees = list(nroots(coeffs))
            # On associe à chaque racine l'approximation qui lui correspond
            for racine, multiplicite in racines:
                distances = [(sol, abs(complex(racine) - sol)) for sol in sol_approchees]
                distances.sort(key = lambda x:x[1])
                for i in range(multiplicite):
                    distances.pop(0)
                # Les racines approchées qui restent ne correspondent à aucune racine exacte
                sol_approchees = [sol for sol, distance in distances]
            racines.extend((sympify(sol), sol_approchees.count(sol)) for sol in set(sol_approchees))

    coefficient = coeffs[0]
    produit = 1
    if corps == "R":
        racines_en_stock = []
        multiplicites_en_stock = []
        for racine, multiplicite in racines:
            if not isinstance(racine, Basic):
                racine = sympify(racine)
            reel = racine.is_real
            if not reel:
                # is_real n'est pas fiable (26/11/2009)
                # cf. ((54*6**(1/3)*93**(1/2) - 162*I*6**(1/3)*31**(1/2) - 522*6**(1/3) + 6*6**(2/3)*(-522 + 54*93**(1/2))**(1/3) + 522*I*3**(1/2)*6**(1/3) + 6*I*3**(1/2)*6**(2/3)*(-522 + 54*93**(1/2))**(1/3) - 24*(-522 + 54*93**(1/2))**(2/3))/(36*(-522 + 54*93**(1/2))**(2/3))).is_real
                re, im = racine.expand(complex=True).as_real_imag()
                reel = im.is_zero or im.evalf(80).epsilon_eq(0,'10e-80')
                if reel:
                    racine = re
                # Approximation utile (?) pour la factorisation de certains polynômes de degrés 3 et 4
                # De toute manière, une vérification de la factorisation par division euclidienne
                # a lieu à la fin de l'algorithme.
            if reel:
                produit *= (variable - racine)**multiplicite
            else:
                conjuguee = racine.conjugate()
                if conjuguee in racines_en_stock:
                    produit *= (variable**2 - 2*re*variable + re**2 + im**2)**multiplicite
                    i = racines_en_stock.index(conjuguee)
                    racines_en_stock.pop(i)
                    multiplicites_en_stock.pop(i)
                else:
                    racines_en_stock.append(racine)
                    multiplicites_en_stock.append(multiplicite)
        if racines_en_stock:
            # Il reste des racines qu'on n'a pas réussi à appareiller.
            P = 1
            for racine, multiplicite in zip(racines_en_stock, multiplicites_en_stock):
                P *= (variable - racine)**multiplicite
            produit *= P.expand()
    else:
        for racine, multiplicite in racines:
            produit *= (variable - racine)**multiplicite
#    print produit
    quotient, reste = div(polynome, coefficient*produit, variable)
    if reste != 0 and not approchee:
        ##print(polynome, coefficient*produit, variable)
        raise NotImplementedError
    poly_factorise = coefficient*produit*quotient
    if isinstance(poly_factorise, Mul) and poly_factorise.args[0] == 1.:
        poly_factorise = Mul(*poly_factorise.args[1:])
        # sinon, poly_factor(x**2+2.5*x+1,x) donne 1.0*(x + 0.5)*(x + 2.0)
    return poly_factorise

def syms(expression):
    """Retourne la liste des symboles utilisés par l'expression."""
    return tuple(expression.atoms(Symbol))

def extract_var(expression):
    """Retourne la variable de l'expression (renvoie un avertissement s'il y en a plusieurs, et 'x' par défaut s'il n'y en a pas)."""
    if hasattr(expression, "atoms"):
        symboles = expression.atoms(Symbol)
        if len(symboles) == 0:
            return Symbol("x")
        else:
            if len(symboles) > 1 and param.debug:
                warning("l'expression possede plusieurs variables.")
            return expression.atoms(Symbol).pop()
    else:
        return Symbol("x")

def count_syms(expression, symbole):
    """Compte le nombre d'occurence de la variable dans l'expression."""
    if expression.has(symbole):
        if expression.is_Atom:
            return 1
        else:
            return sum(count_syms(arg, symbole) for arg in expression.args)
    else:
        return 0
