# -*- 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

## Surclasse les printers de sympy

from math import floor, log

from sympy import Symbol, Integer, Float, Basic, FunctionClass, I, Mul
# from sympy.core.core import BasicMeta
from sympy.printing.latex import LatexPrinter
from sympy.printing.str import StrPrinter
from sympy.core import S

from .custom_objects import Decim


class MyCustomPrinter(object):
    """Personnalisation du printer de sympy.

    * implémentation de méthodes génériques pour gérer les objets Decim.
    * changement de l'ordre de recherche des méthodes d'impression :
      on cherche d'abord l'existence d'une méthode _print_ClasseObjet,
      avant de regarder si l'objet a une méthode ._latex() ou ._str().

      Ceci facilite la personnalisation de l'impression : il est plus
      propre de rajouter une méthode au printer que de modifier les
      méthodes d'impressions de nombreux objets par surclassage ou
      par "Monkey Patching" !
    """

    def _print(self, expr, *args, **kwargs):
        """Internal dispatcher

        Tries the following concepts to print an expression:
            1. Take the best fitting method defined in the printer.
            2. Let the object print itself if it knows how.
            3. As fall-back use the emptyPrinter method for the printer.
        """
        self._print_level += 1
        try:
            # See if the class of expr is known, or if one of its super
            # classes is known, and use that print function
            for cls in type(expr).__mro__:
                printmethod = '_print_' + cls.__name__
                if hasattr(self, printmethod):
                    return getattr(self, printmethod)(expr, *args, **kwargs)

            # If the printer defines a name for a printing method
            # (Printer.printmethod) and the object knows for itself how it
            # should be printed, use that method.
            if (self.printmethod and hasattr(expr, self.printmethod)) :
                    # and not isinstance(expr, BasicMeta)):
                return getattr(expr, self.printmethod)(self, *args, **kwargs)

            # Unknown object, fall back to the emptyPrinter.
            return self.emptyPrinter(expr)
        finally:
            self._print_level -= 1

    def _convert_Decim(self, expr):
        conv = self._convert_Decim
        if isinstance(expr, FunctionClass):
            return expr
        elif hasattr(expr, 'atoms') and hasattr(expr, 'subs'):
            dico = {}
            for a in expr.atoms():
                if a.is_Rational and isinstance(a, Decim):
                    dico[a] = Float(a, dps=60)
            expr = expr.subs(dico).subs(Float(1), S.One)
        elif isinstance(expr, (list, tuple)):
            return expr.__class__(conv(item) for item in expr)
        elif isinstance(expr, dict):
            return dict((conv(key), conv(val)) for key, val in expr.items())
        return expr

    def _float_evalf(self, expr):
        "Évalue le flottant en respectant le réglage du printer (nombre de décimales)."
        if self._settings['mode_scientifique']:
            decimales = self._settings['decimales_sci'] + 1
        else:
            decimales = self._settings['decimales']
        # Le nombre de décimales ne doit pas dépasser la précision interne
        # du flottant, au risque d'ajouter des décimales fausses.
        # Cette précision interne est notée en bits, qu'il faut convertir en
        # nombre de décimales.
        # Nota: log(2)/log(10) = 0.3010299956639812
        precision_interne = round(expr._prec*0.3010299956639812) - 1
        decimales = max(min(decimales, precision_interne), 1)
        return expr.evalf(decimales)


class CustomStrPrinter(MyCustomPrinter, StrPrinter):
    def __init__(self, settings):
        defaults = {'decimales': 18,
                    'mode_scientifique': False,
                    'decimales_sci': 2
                    }
        self._default_settings.update(defaults)
        StrPrinter.__init__(self, settings)

    def _print_str(self, expr):
        # Ne pas utiliser repr(), car on veut tjs avoir des guillemets doubles
        # à l'extérieur de la chaîne (pour éviter les confusions avec le ' de
        # la dérivée).
        return '"%s"' % expr.replace('"', r'\"')

    def _print_Exp1(self, expr):
        return 'e'

    def _print_Abs(self, expr):
        return 'abs(%s)' % self._print(expr.args[0])

    def _print_ImaginaryUnit(self, expr):
        return 'i'

    def _print_Infinity(self, expr):
        return '+oo'

    def _print_log(self, expr):
        return "ln(%s)" % self.stringify(expr.args, ", ")

    def _print_Pow(self, *args, **kw):
        return StrPrinter._print_Pow(self, *args, **kw).replace('**', '^')

    def _print_Float(self, expr):
        exposant = None
        if self._settings['mode_scientifique']:
            # Conversion en écriture scientifique.
            puissance = int(floor(log(expr, 10)))
            flottant = self._float_evalf(expr*10**-puissance)
            mantisse = StrPrinter._print_Float(self, flottant)
            exposant = str(puissance)
        else:
            chaine = StrPrinter._print_Float(self, self._float_evalf(expr))
            if 'e' in chaine:
                # Déjà en mode scientifique (ex: 1.3e-15)
                mantisse, exposant = chaine.split('e')

        if exposant is not None:
            # Affichage en mode scientifique
            mantisse = mantisse.rstrip('0')
            # On laisse la mantisse sous forme de flottant.
            # Ainsi, '2,00000*10^-8' devient '2,0*10^-8', et non '2*10^-8'.
            # Lorsqu'on sauvegarde l'état de l'interprète de la calculatrice,
            # les décimaux du type 0,00000002 sont ainsi sauvegardés sous
            # forme décimale, et non sous forme fractionnaire.
            if mantisse.endswith('.'):
                mantisse += '0'
            exposant = exposant.lstrip('+')
            return '%s*10^%s' % (mantisse, exposant)
        else:
            # Par contre, lorsque les décimaux sont des entiers, inutile de les
            # sauvegarder sous forme décimale.
            return chaine.rstrip('0').rstrip('.')

    def _print_Union(self, expr):
        return expr.__str__()

    def _print_set(self, expr):
        return '{%s}' % ' ; '.join(self._print(val) for val in expr)

    def _print_Fonction(self, expr):
        return "%s -> %s" % (", ".join(expr._variables()),
                        self._print(self._convert_Decim(expr.expression)))

    def doprint(self, expr):
        # Mieux vaut faire la substitution une seule fois dès le départ.
        expr = self._convert_Decim(expr)
        return StrPrinter.doprint(self, expr)

def custom_str(expr, **settings):
    return CustomStrPrinter(settings).doprint(expr)

# Modifie Basic.__repr__ pour utiliser `custom_str` au lieu de `sstr` (printer de sympy)
# Can't use partial() for this (cf. http://bugs.python.org/issue4331)

Basic.__repr__ = (lambda self: custom_str(self, order=None))


class CustomLatexPrinter(MyCustomPrinter, LatexPrinter):

    def __init__(self, settings):
        defaults = {'decimales': 18,
                    'mode_scientifique': False,
                    'decimales_sci': 2,
                    "mat_str" : "pmatrix",
                    "mat_delim" : "",
                    "mode": "inline",
                    "fold_frac_powers": False,
                    "fold_short_frac": False,
                    }
        self._default_settings.update(defaults)
        LatexPrinter.__init__(self, settings)

    def _print_Temps(self, expr):
        return r"%s \mathrm{j}\, %s \mathrm{h}\, %s \mathrm{min}\, %s \mathrm{s}" % expr.jhms()

    def _print_exp(self, expr, exp=None):
        tex = r"\mathrm{e}^{%s}" % self._print(expr.args[0])
        return self._do_exponent(tex, exp)

    def _print_Exp1(self, expr):
        return r"\mathrm{e}"

    def _print_Abs(self, *args, **kw):
        res = LatexPrinter._print_Abs(self, *args, **kw)
        return res.replace(r'\lvert', r'|').replace(r'\rvert', r'|')

    def _print_Pi(self, *args, **kw):
        return r"\pi"

    def _print_ImaginaryUnit(self, expr):
        return r"\mathrm{i}"

    def _print_Fonction(self, expr):
        return ", ".join(expr._variables()) + "\\mapsto " + self._print(expr.expression)

    def _print_Function(self, expr, exp=None):
        func = expr.func.__name__

        if hasattr(self, '_print_' + func):
            return getattr(self, '_print_' + func)(expr, exp)

        else:
            if exp is not None:
                name = r"\mathrm{%s}^{%s}" % (func, exp)
            else:
                name = r"\mathrm{%s}" % func
            if len(expr.args) == 1 and isinstance(expr.args[0], (Symbol, Integer)):
                return name + "(" + str(self._print(expr.args[0])) +")"
            else:
                args = [ str(self._print(arg)) for arg in expr.args ]
                return name + r"\left(%s\right)" % ",".join(args)

    def _print_ProduitEntiers(self, expr):
        def formater(exposant):
            if exposant == 1:
                return ""
            return "^" + str(exposant)
        return "\\times ".join((str(entier) + formater(exposant)) for entier, exposant in expr.couples)

    def _print_Float(self, expr):
        if self._settings['mode_scientifique']:
            # Gestion de l'écriture scientifique.
            n = int(floor(log(expr, 10)))
            s = LatexPrinter._print_Float(self, self._float_evalf(expr*10**-n))
            return r"%s \times 10^{%s}" % (s, n)
        s = LatexPrinter._print_Float(self, self._float_evalf(expr))
        if s.startswith(r'1.0 \times '): # sympy 0.7.3
            return s[11:]
        elif s.startswith(r'1.0 \cdot '): # sympy 0.7.5
            return s[10:]
        elif r'\times' not in s:
            # Ne pas supprimer un zéro de la puissance !
            s = s.rstrip('0').rstrip('.')
        return s

    def _print_Infinity(self, expr):
        return r"+\infty"

    def _print_Order(self, expr):
        return r"\mathcal{O}\left(%s\right)" % \
            self._print(expr.args[0])

    def _print_Union(self, expr):
        if expr.vide:
            return r"\varnothing"
        tex = r"\cup".join(self._print(intervalle) for intervalle in expr.intervalles)
        tex = tex.replace(r"\right\}\cup\left\{", "\,;\, ")
        return tex

    def _print_Mul(self, expr):
        args = expr.args
        if args[-1] is I:
            if len(args) == 2 and args[0] == -1:
                return LatexPrinter._print_Mul(self, expr)
            return '%s %s' % (self._print(Mul(*args[:-1])), self._print(I))
        return LatexPrinter._print_Mul(self, expr)
        #return ' '.join(self._print(arg) for arg in expr.args)

    def _print_set(self, expr):
        if expr:
            return r'\left\{%s\right\}' % '\,;\,'.join(self._print(val) for val in expr)
        return r"\emptyset"

    def _print_Intervalle(self, expr):
        if expr.vide:
            return r"\varnothing"
        elif expr.inf_inclus and expr.sup_inclus and expr.inf == expr.sup:
            return r"\left\{%s\right\}" % self._print(expr.inf)
        if expr.inf_inclus:
            left = r"\left["
        else:
            left = r"\left]"
        if expr.sup_inclus:
            right = r"\right]"
        else:
            right = r"\right["
        return r"%s%s;%s%s" % (left, self._print(expr.inf), self._print(expr.sup), right)

    def _print_tuple(self, expr):
        return r"\left(" + ",\,".join(self._print(item) for item in expr) + r"\right)"

    def _print_log(self, expr, exp=None):
        if len(expr.args) == 1 and isinstance(expr.args[0], (Symbol, Integer)):
            tex = r"\ln(%s)" % self._print(expr.args[0])
        else:
            tex = r"\ln\left(%s\right)" % self._print(expr.args[0])
        return self._do_exponent(tex, exp)

    def _print_function(self, expr):
        return r"\mathrm{Fonction}\, " + expr.__name__

    def _print_Decim(self, expr):
        return self._print_Float(Float(expr, dps=expr.prec))

    def doprint(self, expr):
        expr = self._convert_Decim(expr)
        tex = LatexPrinter.doprint(self, expr)
        return tex.replace(r'\operatorname{', r'\mathrm{')


def custom_latex(expr, **settings):
    return CustomLatexPrinter(settings).doprint(expr)
