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

##--------------------------------------##
#              WxGeometrie               #
#                tabvar                  #
##--------------------------------------##
#    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

import re

from sympy import oo, nan, Symbol, Float, Rational

from .tablatexlib import convertir_en_latex, traduire_latex, test_parentheses,\
                         maths, extraire_facteurs, nice_str
from ...mathlib.sympy_functions import solve
from ...mathlib.intervalles import R, conversion_chaine_ensemble
from ...mathlib.solvers import ensemble_definition
from ...mathlib.interprete import Interprete
from ...mathlib.parsers import VAR
from ...mathlib.custom_functions import round_afz
from ... import param

def _auto_tabsign(chaine, cellspace=False, decimales=3, approche=False):
    """Génère le code du tableau de signe à partir d'une expression à variable réelle.

    On suppose que l'expression est continue sur tout intervalle de son
    ensemble de définition.
    Par ailleurs, ses zéros doivent être calculables pour la librairie sympy.

    Pour les valeurs approchées, on conserve par défaut 3 chiffres après la virgule.
    En mettant `decimales=2`, on peut par exemple afficher seulement 2 chiffres
    après la virgule, etc.
    """
    def nice_str2(x):
        if (isinstance(x, (float, Float)) and not isinstance(x, Rational)
                or approche and x not in (-oo, oo)):
            x = round_afz(float(x), decimales)
        return nice_str(x)

    chaine_initiale = chaine

    # Ensemble de définition
    if ' sur ' in chaine:
        chaine, ens_def = chaine.split(' sur ')
        ens_def = conversion_chaine_ensemble(ens_def, utiliser_sympy = True)
    else:
        ens_def = R

    # Légende de la dernière ligne
    if '=' in chaine:
        legende, chaine = chaine.split('=', 1)
    else:
        legende = chaine
    legende = legende.strip()

    # Décomposition en produit
    facteurs = extraire_facteurs(chaine)#.replace('/', '*'))
    # Conversion en expression sympy
    interprete = Interprete()
    interprete.evaluer(chaine)
    expr = interprete.ans()
    # Récupération de la variable
    variables = expr.atoms(Symbol)
    # On tente de récupérer le nom de variable dans la légende.
    # Par exemple, si la légende est 'f(x)', la variable est 'x'.
    m = re.match(r'%s\((%s)\)' % (VAR, VAR), legende)
    if m is not None:
        variables.add(Symbol(str(m.group(1))))
    if len(variables) > 1:
        # Il est impossible de dresser le tableau de variations avec des
        # variables non définies (sauf cas très particuliers, comme f(x)=a).
        raise ValueError("Il y a plusieurs variables dans l'expression !")
    elif not variables:
        # Variable par défaut.
        variables = [Symbol('x')]
    var = variables.pop()
    # Récupération de l'ensemble de définition
    ens_def *= ensemble_definition(expr, var)
    if param.debug and param.verbose:
        print('-> Ensemble de definition:', ens_def)

    code = str(var) # chaîne retournée, respectant la syntaxe de tabsign()
    valeurs_interdites = []
    xmin = ens_def.intervalles[0].inf
    if not ens_def.intervalles[0].inf_inclus:
        valeurs_interdites.append(xmin)
    sup = xmin
    for intervalle in ens_def.intervalles:
        inf = intervalle.inf
        if sup != inf:
            # Il y a un 'trou' dans l'ensemble de définition (ex: ]-oo;0[U]2;+oo[)
            raise NotImplementedError
        sup = intervalle.sup
        if not intervalle.sup_inclus:
            valeurs_interdites.append(sup)
        code += ': ' + ('' if (intervalle.inf_inclus or inf == -oo) else '!') + nice_str2(inf) + ';' \
                     + ('' if (intervalle.sup_inclus or sup == oo) else '!') + nice_str2(sup)
    xmax = sup


    # Étude du signe de chaque facteur
    for facteur in facteurs:
        interprete.evaluer(facteur)
        f_expr = interprete.ans()
        f_ens_def = ensemble_definition(f_expr, var)
        valeurs = {xmin: None, xmax: None}
        solutions = [sol for sol in solve(f_expr, var)
                        if xmin.evalf(200) <= sol.evalf(200) <= xmax.evalf(200)]
        for sol in solutions:
            valeurs[sol] = 0
        for val in valeurs_interdites:
            if val not in f_ens_def and val not in (-oo, oo):
                valeurs[val] = nan
        liste_valeurs = sorted(valeurs)
        # On génère le code de la ligne
        code += '// '
        #print solutions, valeurs_interdites
        if solutions and all(sol in valeurs_interdites for sol in solutions):
            code += '!'
        code += facteur + ':'
        for i, valeur in enumerate(liste_valeurs):
            if valeurs[valeur] == 0:
                code += nice_str2(valeur)
            elif valeurs[valeur] == nan:
                code += '!' + nice_str2(valeur)

            if i != len(liste_valeurs) - 1:
                valeur_suivante = liste_valeurs[i + 1]
                if valeur == -oo:
                    if valeur_suivante == +oo:
                        val_intermediaire = 0
                    else:
                        val_intermediaire = valeur_suivante - 1
                elif valeur_suivante == +oo:
                    val_intermediaire = valeur + 1
                else:
                    val_intermediaire = (valeur + valeur_suivante)/2
                # On suppose la fonction continue sur tout intervalle de son ensemble de définition.
                if f_expr.subs(var, val_intermediaire) > 0:
                    code += ' ++ '
                else:
                    code += ' -- '

    code += '// ' + legende
    if param.debug and param.verbose:
        print('Code TABSign:', code)
    return tabsign(code, cellspace = cellspace) + '% ' + chaine_initiale + '\n'



def _augmenter(val):
    """Retourne une valeur numérique *légèrement* supérieure à la valeur rentrée.

    La valeur retournée est aussi proche que possible de la valeur rentrée, tout
    en s'assurant que _augmenter(val)>val.
    """
    if float(val) == float('-inf'):
        return -100000000000000000000000000000000
    eps = 1e-20
    while val + eps <= val:
        eps *= 10
        if eps > 1e200:
            raise OverflowError("Can't find value bigger than %s." % val)
    return val + eps


def tabsign(chaine = '', cellspace=False, decimales=3, approche=False):
    """Indiquer ligne par ligne le signe de chaque facteur.
La dernière ligne (produit ou quotient) est générée automatiquement.

Exemples:
x:-pi;pi //  sin(x):-pi -- 0 ++ pi //  !cos(x):-- -pi/2 ++ pi/2 -- // tan(x)
x:-2;2 // x+1:-- -1 ++ // !x-1:-- 1 ++

Le point d'exclamation avant une expression signifie qu'elle correspond à un dénominateur."""

    chaine_originale = chaine = chaine.strip()
    chaine = chaine.replace('//', '\n').replace(r'\\', '\n').replace("-oo", r"-\infty").replace("+oo", r"+\infty")

    lignes = [ligne.strip() for ligne in chaine.split('\n') if ligne.strip()]

    if len(lignes) == 1:
        if ':' in lignes[0]:
            lignes = [''] + lignes
        else:
            return _auto_tabsign(lignes[0], cellspace=cellspace, approche=approche, decimales=decimales)

    # 'resultat' est la dernière ligne, sauf si elle contient ':'
    # (Dans ce cas, 'resultat' sera généré automatiquement plus tard, à partir des autres lignes).
    resultat = (lignes.pop() if ':' not in lignes[-1] else '')

    ligne_variable = lignes.pop(0)
    #print lignes
    if ":" in ligne_variable:
        variable, donnees_variable = ligne_variable.split(":", 1)
    elif ";" in ligne_variable:
        variable = ""
        donnees_variable = ligne_variable
    else:
        variable = ligne_variable
        donnees_variable = ""

    variable = variable.strip()
    if ';' not in donnees_variable:
        donnees_variable += ';'

    def _inter2tuple(s):
        inf, sup = s.split(';')
        inf = inf.strip()
        sup = sup.strip()
        if not inf:
            inf = '-oo'
        if not sup:
            sup = '+oo'
        return (inf, sup)

    intervalles = [_inter2tuple(inter) for inter in donnees_variable.split(':')]

    # Séparation de la légende et des autres données pour chaque ligne
    expressions = []
    donnees_expressions = []
    for ligne in lignes:
        expression, signe_expression = ligne.split(":")
        expressions.append(expression.strip())
        donnees_expressions.append(signe_expression.strip())

    # Au besoin, on génère la légende de la dernière ligne (c.à-d. le résultat - produit ou quotient)
    if resultat == "":
        numerateur = []
        denominateur = []
        for expression in expressions:
            if expression[0] == "!":
                liste = denominateur
                expression = expression[1:]
            else:
                liste = numerateur
            if "+" in expression or "-" in expression[1:]:
                # il s'agit d'une somme ou d'une difference, il faut donc des parentheses
                liste.append("(" + expression + ")")
            else:
                liste.append(expression)
        if denominateur:
            if len(denominateur) == 1 and denominateur[0][0] == "(" and denominateur[0][-1] == ")":
                denominateur[0] = denominateur[0][1:-1]
            if len(numerateur) == 1 and numerateur[0][0] == "(" and numerateur[0][-1] == ")":
                numerateur[0] = numerateur[0][1:-1]
            if not numerateur:
                numerateur = ["1"]
            resultat = "\\frac{%s}{%s}" %("".join(numerateur), "".join(denominateur))
        else:
            resultat = "".join(numerateur)

        resultat_genere_automatiquement = True
    else:
        resultat_genere_automatiquement = False

    # Cas particulier : 'produit' de 1 seul élément -> inutile d'afficher une ligne résultat
    if len(expressions) == 1 and not expressions[0].startswith("!"):
        afficher_resultat = False
    else:
        afficher_resultat = True

    # Au besoin, on génère la légende de la première ligne (c.à-d. la variable),
    # à partir du résultat.
    if not variable:
        # On regarde si le résultat est de la forme `fonction(variable)`,
        # par exemple, `f(x)`.
        m = re.match(r'%s\((%s)\)' % (VAR, VAR), resultat.strip())
        if m is not None:
            variable = m.group(1)
        else:
            # On cherche les lettres isolées (sauf 'e', qui représente exp(1))
            m = re.search('(?<![A-Za-z])[A-DF-Za-df-z](?![A-Za-z])', resultat)
            # Si on n'en trouve pas, la variable sera 'x'
            variable = m.group() if m else 'x'

    # On récupère la liste de toutes les valeurs de x
    valeurs = set() # va contenir toutes les valeurs numeriques
    correspondances = {} # servira à retrouver le code (LaTeX notamment) à partir de la valeur numérique.

    valeurs_interdites = set()

    # `defaut_val` permet de construire des tableaux de signes contenant
    # des valeurs symboliques (par exemple "f(x): -- x_1 ++ x_2 --").
    # Pour attribuer une valeur numérique à cette valeur symbolique,
    # on prend une valeur légèrement supérieure à la dernière valeur numérique rencontrée.
    # NOTE: cet algorithme n'est pas parfait, mais permet de gérer les cas simples.
    # Pour les cas plus compliqués, l'utilisateur garde la possibilité de rentrer
    # manuellement une valeur numérique associée à la valeur symbolique.
    # Exemple: f(x): -- x_1=0.27 ++ x_2=3.58 --

    defaut_val = None

    # On convertit les bornes du domaine en valeurs numériques
    for intervalle in intervalles:
        for borne in intervalle:
            exclue = (borne[0] == '!')
            if exclue:
                borne = borne[1:]
            num = eval(traduire_latex(borne), maths.__dict__)
            if defaut_val is None:
                # Un peu plus que la borne inférieure.
                defaut_val = _augmenter(num)
            if exclue:
                valeurs_interdites.add(num)
            valeurs.add(num)
            correspondances[num] = borne

    # Idem pour toutes les autres valeurs
    for i, donnees in enumerate(donnees_expressions):
        donnees = re.split(r'(\+\+|--|00)', donnees)
        for j, valeur in enumerate(donnees):
            valeur = valeur.strip()
            if valeur not in ('++', '--', '00', ''):
                # On préformate les donnees en vue d'un traitement ulterieur.
                # Ex: '-5 ++ (1;|) -- 4' devient [(-5, "0"), "+", (1, "|"), "-", (4, "0")].
                if valeur[0] == '(' and valeur[-1] == ')' and test_parentheses(valeur[1:-1]):
                    valeur = valeur[1:-1]
                vals = [val.strip() for val in valeur.split(';')]
                if len(vals) == 1:
                    if vals[0].startswith('!'):
                        vals[0] = vals[0][1:]
                        vals.append('|')
                    else:
                        vals.append('0')
                else:
                    vals[1] = '$' + vals[1] + '$'
                if '=' in vals[0]:
                    vals[0], valeur_num = vals[0].split('=', 1)
                    valeur_num = eval(traduire_latex(valeur_num), maths.__dict__)
                else:
                    try:
                        valeur_num = eval(traduire_latex(vals[0]), maths.__dict__)
                        defaut_val = valeur_num
                    except (NameError, SyntaxError):
                        defaut_val = _augmenter(defaut_val)
                        valeur_num = defaut_val

                valeurs.add(valeur_num)
                correspondances[valeur_num] = vals[0]
                vals[0] = valeur_num
                donnees[j] = vals
        #print donnees
        donnees_expressions[i] = donnees

    # 'donnees_expressions' est désormais une liste de liste.
    # Pour chaque ligne du tableau, 'donnees_expressions' contient une liste
    # du genre [(-5, "0"), "+", (1, "|"), "-", (4, "0")].

    valeurs = sorted(valeurs)

    indices_denominateurs = []
    # indique que les lignes correspondantes se trouvent au denominateur

    # On initialise le code LaTeX de chaque ligne avec sa légende.
    # NB: la liste 'lignes' contiendra le code LaTeX pour chaque ligne.
    n = max(len(chaine) for chaine in expressions + [variable, resultat])
    lignes = [convertir_en_latex(variable)]
    i = 1
    for expression in expressions:
        if expression[0] == "!":
            expression = expression[1:]
            indices_denominateurs.append(i)
        lignes.append(convertir_en_latex(expression))
        i += 1
    lignes.append(convertir_en_latex(resultat))

    titres = lignes[:]

    # On justifie le texte de la première colonne pour que le code LaTeX généré soit plus lisible.
    n = max(len(texte) for texte in lignes) + 1
    for i, ligne in enumerate(lignes):
        lignes[i] = ligne.ljust(n)

    def latex_signe(val, co, li):
        "Retourne le signe à afficher dans le tableau, selon la valeur (et la colonne)."
        if val == nan:
            return '\\geopharDB{%s}' % titres[li]
        elif val > 0:
            return '+' if not co%2 else ''
        elif val < 0:
            return '$-$' if not co%2 else ''
        else:
            return '0'

    dict_signes = {'++': 1, '--': -1, '00': 0}


    # On génère maintenant le code LaTeX correspondant au tableau proprement dit.
    # On procède colonne par colonne.
    nbr_colonnes = 2*len(valeurs)
    for co in range(1, nbr_colonnes):
        colonne = ['' for i in range(len(donnees_expressions) + 2)]

        signe = 1
        # (1 pour positif, -1 pour négatif, nan pour valeur interdite -> cf. latex_signe())

        # Première ligne (valeurs de la variable)
        if co%2:
            # Il s'agit d'une valeur (et non d'un signe + ou -)
            valeur_num = valeurs[(co - 1)//2]
            valeur = correspondances[valeur_num]

            # On applique un formatage LaTeX à certaines expressions :
            colonne[0] = convertir_en_latex(valeur)

            if valeur_num in valeurs_interdites:
                signe = nan


        # Autres lignes
        for li, donnees in enumerate(donnees_expressions):
            # À quel endroit de la ligne sommes-nous ? (3 cas)
            valeurs_precedentes = [k for k, val in enumerate(donnees) if not isinstance(val, str) and val[0]<=valeur_num]
            if valeurs_precedentes:
                # position de la dernière valeur de la ligne
                position = valeurs_precedentes[-1]
                if co%2: # 1er cas: on est au niveau d'une valeur
                    if donnees[position][0] == valeur_num:
                        if donnees[position][1] == '|':
                            colonne[li + 1] = '\\geopharDB{%s}' % titres[li + 1]
                            signe = nan
                        else:
                            colonne[li + 1] = donnees[position][1]
                            signe *= 0
                        if li + 1 in indices_denominateurs:
                            signe = nan
                else: # 2e cas: on est entre deux valeurs
                    assert position + 1 < len(donnees), "Verifiez que les valeurs de la ligne sont bien rangees dans l'ordre croissant."
                    signe_ = dict_signes[donnees[position + 1]]
                    colonne[li + 1] += latex_signe(signe_, co, li + 1) # le signe qui est juste apres la derniere valeur
                    signe *= signe_
            else: # 3e cas: on est en début de ligne
                if co%2 == 0: # on est entre deux valeurs
                    signe_ = dict_signes[donnees[1]]
                    colonne[li + 1] += latex_signe(signe_, co, li + 1)
                    signe *= signe_

        # Dernière ligne : signe du produit ou du quotient
        colonne[-1] += latex_signe(signe, co, -1)

        # On centre le texte dans la colonne pour que le code LaTeX généré soit plus lisible.
        n = max(len(texte) for texte in colonne) + 2
        for i, ligne in enumerate(colonne):
            colonne[i] = ligne.center(n)

        # Une fois la colonne entièrement générée, le texte de la colonne est rajouté à chaque ligne
        for num, text in enumerate(colonne):
            lignes[num] += '&' + text


    # Cas particulier : 'produit' de 1 seul élément
    if not afficher_resultat:
        lignes.pop(-1 if resultat_genere_automatiquement else -2)
        # NB: si le resultat n'a pas été généré automatiquement, on garde la dernière ligne,
        # et on supprime l'avant dernière, pour garder le nom éventuel de la fonction
        # par exemple, si f(x)=x-1
        # cela permet d'avoir   f(x) | -  1  +   dans le tableau,
        # au lieu de            x-1  | -  1  +


    code = []
    # Et on rassemble les lignes
    if cellspace:
        code.append("\\begin{center}\n\\begin{tabular}{|Sc|" + (nbr_colonnes - 1)*"Sc" + "|}\n\\hline")
    else:
        code.append("\\begin{center}\n\\begin{tabular}{|c|" + (nbr_colonnes - 1)*"c" + "|}\n\\hline")
    for ligne in lignes:
        code.append(ligne + "\\\\\n\\hline")
    code.append("\\end{tabular}\n\\end{center}\n% " + chaine_originale)
    if any(('\\geopharDB{' in line) for line in code):
        code.insert(0, '\\providecommand{\\geopharDB}[1]{$\\left|\\vphantom{\\text{#1}}\\right|$}')
    return '\n'.join(code) + '\n'
