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

##--------------------------------------#######
#                Calculatrice                 #
##--------------------------------------#######
#    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 functools import partial

from PyQt5.QtGui import QIcon, QCursor, QTextCursor
from PyQt5.QtWidgets import QCheckBox, QPushButton, QTextEdit, QMenu, QLabel, \
    QSpinBox, QToolButton, QWidget, QTabWidget, QGroupBox, QComboBox, \
    QLineEdit, QHBoxLayout, QVBoxLayout
from PyQt5.QtCore import Qt

from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure

from ...GUI.ligne_commande import LigneCommande
from ...GUI.qtlib import png
from ...GUI.inspecteur import FenCode
from ...GUI.menu import MenuBar
from ...GUI.panel import Panel_simple
from ...mathlib.interprete import Interprete
from ...mathlib.parsers import latex2mathtext
from ...mathlib.end_user_functions import __classement__

from ...pylib import print_error, debug, no_argument, eval_safe
from ... import param


class CalculatriceMenuBar(MenuBar):
    def __init__(self, panel):
        MenuBar.__init__(self, panel)
        self.ajouter("Fichier", ["Réinitialiser",
                                    "Réinitialiser la calculatrice.", "Ctrl+N",
                                    self.panel.initialiser],
                                ["ouvrir"], ["enregistrer"],
                                ["enregistrer_sous"], ['session'], None, ["quitter"])
        self.ajouter("Affichage", ["onglet"], ["plein_ecran"])
        for rubrique in __classement__:
            self.ajouter(rubrique, *(self.formater(contenu, rubrique != "Symboles") for contenu in __classement__[rubrique]))
            # pas de parenthese apres un symbole
        self.ajouter("Outils",
                        ["Mémoriser le résultat", "Copie le resultat du calcul dans le presse-papier, afin de pouvoir l'utiliser ailleurs.", "Ctrl+M", self.panel.vers_presse_papier],
                        ["options"],
                        )
        self.ajouter("Avancé",
                        ["État interne de l'interprète", "État de l'interprète de commandes.", "Ctrl+H", self.panel.EtatInterne],
                        ["ligne_commande"],
                        ["debug"],
                        )
        self.ajouter("?")


    def formater(self, contenu, parentheses = True):
        if contenu is None:
            return
        titre, nom, doc = contenu
        return [titre, doc, "", partial(self.panel.insere, nom=nom, parentheses=parentheses)]



class BoutonValider(QToolButton):

    modes = [('exact', 'résultats exacts'), ('approche', 'résultats approchés'),
             ('scientifique', 'résultats en écriture scientifique')]

    def __init__(self, parent):
        QToolButton.__init__(self)
        self.parent = parent
        self.setAutoRaise(True)
        self.mode_normal()

        self.menu = menu = QMenu(self)
        self.setMenu(menu)
        for mode, titre in self.modes:
            nom = 'mode_' + mode
            action = menu.addAction(QIcon(png(nom)), titre)
            action.setIconVisibleInMenu(True)
            action.triggered.connect(getattr(self, nom))
        self.clicked.connect(self.mode_occupe)


    def _set_icon(self, nom):
        pix = png(nom)
        self.setIcon(QIcon(pix))
        self.setIconSize(pix.size())

    def mode_exact(self, *args):
        self._set_icon('mode_exact_')
        self.parent.param("calcul_exact", True)
        self.parent.param("ecriture_scientifique", False)

    def mode_approche(self, *args):
        self._set_icon('mode_approche_')
        self.parent.param("calcul_exact", False)
        self.parent.param("ecriture_scientifique", False)

    def mode_scientifique(self, *args):
        self._set_icon('mode_scientifique_')
        self.parent.param("calcul_exact", False)
        self.parent.param("ecriture_scientifique", True)

    def mode_occupe(self):
        self._set_icon('thinking2_')

    def mode_normal(self):
        if self.parent.param("calcul_exact"):
            self.mode_exact()
        elif self.parent.param("ecriture_scientifique"):
            self.mode_scientifique()
        else:
            self.mode_approche()



class PaveNumerique(QWidget):
    def __init__(self, parent):
        QWidget.__init__(self, parent)
        self.parent = parent
        ### Pave numerique de la calculatrice ###
        # On construit le pavé de la calculatrice.
        # Chaque bouton du pavé doit provoquer l'insertion de la commande correspondante.

        self.pave = pave = QVBoxLayout()
#        pave.setSpacing(1)
        boutons = ["2nde", "ans", "ouv", "ferm", "egal", "7", "8", "9", "div", "x", "4", "5", "6", "mul", "y", "1", "2", "3", "minus", "z", "0", "pt", "pow", "plus", "t", "rac", "sin", "cos", "tan", "exp", "i", "pi", "e", "abs", "mod"]
        inserer = ["", "ans()", "(", ")", "=", "7", "8", "9",  "/", "x", "4", "5", "6", "*", "y", "1", "2", "3", "-", "z", "0", ".", "^", "+", "t", "sqrt(", ("sin(", "asin(", "sinus / arcsinus"), ("cos(", "acos(", "cosinus / arccosinus"), ("tan(", "atan(", "tangente / arctangente"), ("exp(", "ln(", "exponentielle / logarithme neperien"), ("i", "cbrt(", "i / racine cubique"), ("pi", "sinh(", "pi / sinus hyperbolique"), ("e", "cosh", "e / cosinus hyperbolique"), ("abs(", "tanh", "valeur absolue / tangente hyperbolique"), (" mod ", "log10(", "modulo / logarithme decimal")]

        self.seconde = False # indique si la touche 2nde est activee.

        self.actions = [self.touche_2nde]

        for i, nom_bouton in enumerate(boutons):
            # On aligne les boutons de la calculatrice par rangées de 5.
            if i%5 == 0:
                self.rangee = rangee = QHBoxLayout()
                rangee.addStretch(1)
                pave.addLayout(rangee)

            # Ensuite, on construit une liste de fonctions, parallèlement à la liste des boutons.
            if i > 0:
                self.actions.append(partial(self.action, commande=inserer[i]))

            bouton = QPushButton()
            pix = png('btn_' + nom_bouton)
            bouton.setIcon(QIcon(pix))
            bouton.setIconSize(pix.size())
            bouton.setFlat(True)
#            bouton.SetBackgroundColour(self.GetBackgroundColour())
            rangee.addWidget(bouton)
            if i%5 == 4:
                rangee.addStretch(2)
            # A chaque bouton, on associe une fonction de la liste.
            bouton.clicked.connect(self.actions[i])
            if type(inserer[i]) == tuple:
                bouton.setToolTip(inserer[i][2])

            self.setLayout(self.pave)

    def touche_2nde(self, event=None):
        self.seconde = not self.seconde
        if self.seconde:
            self.message("Touche [2nde] activée.")
        else:
            self.message("")

    def action(self, event=None, commande=''):
        entree = self.parent.entree
        if type(commande) == tuple:
            entree.insert(commande[self.seconde])
        else:
            entree.insert(commande)
        n = entree.cursorPosition()
        entree.setFocus()
        entree.setCursorPosition(n)
        self.seconde = False
        self.parent.message("")



class Options(QWidget):
    def __init__(self, parent):
        QWidget.__init__(self, parent)
        self.parent = parent
        prm = parent.param

        self.ensembles = ('R', 'C')

        ### Liste des options de la calculatrice ###
        self.pave = QVBoxLayout()

        # Chiffres significatifs
        box = QGroupBox("Mode calcul approché")
        box_layout = QVBoxLayout()
        box.setLayout(box_layout)

        ligne = QHBoxLayout()
        box_layout.addLayout(ligne)
        ligne.addWidget(QLabel("Afficher "))
        self.sc_precision_affichage = sc = QSpinBox(self)
        # param.precision_calcul = 60 par défaut
        sc.setRange(1, 50)
        sc.setValue(prm("precision_affichage"))
        sc.valueChanged.connect(self.EvtPrecisionAffichage)
        ligne.addWidget(sc)
        ligne.addWidget(QLabel(" chiffre(s) significatif(s)."))
        ligne.addStretch()

        self.pave.addWidget(box)

        # Nombre de décimales
        box = QGroupBox("Mode écriture scientifique")
        box_layout = QVBoxLayout()
        box.setLayout(box_layout)

        ligne = QHBoxLayout()
        box_layout.addLayout(ligne)
        ligne.addWidget(QLabel("Arrondir les résultats à "))
        self.sc_decimales = sc = QSpinBox(self)
        sc.setRange(0, 11)
        sc.setValue(prm("ecriture_scientifique_decimales"))
        sc.valueChanged.connect(self.EvtDecimales)
        ligne.addWidget(sc)
        ligne.addWidget(QLabel(" décimale(s)."))
        ligne.addStretch()

        self.pave.addWidget(box)

        box = QGroupBox("Copie Automatique")
        box_layout = QVBoxLayout()
        box.setLayout(box_layout)
        # Copie du résultat dans le presse-papier
        ligne = QHBoxLayout()
        box_layout.addLayout(ligne)
        self.cb_copie_automatique = cb = QCheckBox(self)
        cb.setChecked(prm("copie_automatique"))
        cb.stateChanged.connect(self.EvtCopieAutomatique)
        cb.setToolTip("Copier automatiquement le résultat du calcul dans le presse-papier.")
        ligne.addWidget(cb)
        ligne.addWidget(QLabel("Copie du résultat dans le presse-papier."))
        ligne.addStretch()

        # En mode LaTeX
        ligne = QHBoxLayout()
        box_layout.addLayout(ligne)
        self.cb_copie_automatique_LaTeX = cb = QCheckBox(self)
        cb.setChecked(prm("copie_automatique_LaTeX"))
        cb.setToolTip("Copier le résultat du calcul au format LaTeX dans le presse-papier.")
        ligne.addWidget(cb)
        cb.stateChanged.connect(self.EvtCopieAutomatiqueLatex)
        self.st_copie_automatique_LaTeX = st = QLabel("Copie au format LaTeX (si possible).")
        ligne.addWidget(st)
        ligne.addStretch()

        self.pave.addWidget(box)
        #~ self.pave.addStretch()

        box = QGroupBox("Ensemble de résolution")
        box_layout = QVBoxLayout()
        box.setLayout(box_layout)
        ligne = QHBoxLayout()
        box_layout.addLayout(ligne)
        ligne.addWidget(QLabel('Résoudre et factoriser dans '))
        self.cb_ensemble = cb = QComboBox()
        ligne.addWidget(cb)
        cb.addItems(('R (réels)', 'C (complexes)'))
        cb.setCurrentIndex(self.ensembles.index(prm('ensemble')))
        cb.currentIndexChanged.connect(self.EvtEnsemble)
        self.pave.addWidget(box)
        self.pave.addStretch()

        self.setLayout(self.pave)
        # Pour (dés)activer la ligne "Copie au format LaTeX" au besoin.
        self.EvtCopieAutomatique()

    def EvtPrecisionAffichage(self, event=None):
        val = self.sc_precision_affichage.value()
        self.parent.param("precision_affichage", val)


    def EvtDecimales(self, event=None):
        val = self.sc_decimales.value()
        self.parent.param("ecriture_scientifique_decimales", val)


    def EvtCopieAutomatique(self, event=None):
        valeur = self.cb_copie_automatique.isChecked()
        self.parent.param("copie_automatique", valeur)
        if valeur:
            self.cb_copie_automatique_LaTeX.setEnabled(True)
            self.st_copie_automatique_LaTeX.setEnabled(True)
        else:
            self.cb_copie_automatique_LaTeX.setEnabled(False)
            self.st_copie_automatique_LaTeX.setEnabled(False)


    def EvtCopieAutomatiqueLatex(self, event=None):
        val = self.cb_copie_automatique_LaTeX.isChecked()
        self.parent.param("copie_automatique_LaTeX", val)


    def EvtEnsemble(self, index):
        self.parent.param("ensemble", self.ensembles[index])



class Avance(QWidget):
    def __init__(self, parent):
        QWidget.__init__(self, parent)
        self.parent = parent
        prm = parent.param


        ### Liste des options avancées de la calculatrice ###
        self.pave = QVBoxLayout()

        box = QGroupBox("Post-traitement")
        box_layout = QVBoxLayout()
        box.setLayout(box_layout)
        box_layout.addWidget(QLabel("Traitement automatiquement du résultat :"))
        self.traitement = QLineEdit()
        traitement = self.parent.param("appliquer_au_resultat") or '_'
        self.traitement.setText(traitement)
        self.traitement.setMinimumWidth(100)
        self.traitement.setToolTip("Fonction ou opérations à appliquer automatiquement au résultat (représenté par _). Ex: 'factoriser(_)'.")
        self.traitement.editingFinished.connect(self.EvtAppliquerResultat)
        box_layout.addWidget(self.traitement)
        self.pave.addWidget(box)
        self.setLayout(self.pave)
        self.pave.addStretch()


    def EvtAppliquerResultat(self, event=None):
        val = self.traitement.text().strip()
        if not val:
            self.traitement.setText('_')
            val = None
        elif val == '_':
            val = None
        self.parent.param("appliquer_au_resultat", val)




class OngletsCalc(QTabWidget):
    def __init__(self, parent):
        QTabWidget.__init__(self, parent)
        self.addTab(PaveNumerique(parent), ' Pavé numérique ')
        self.addTab(Options(parent), 'Options')
        self.addTab(Avance(parent), 'Avancé')
        self.setTabPosition(QTabWidget.South)
        self.setStyleSheet("""
        QTabBar::tab:selected {
        background: white;
        border: 1px solid #C4C4C3;
        border-top-color: white; /* same as the pane color */
        border-bottom-left-radius: 4px;
        border-bottom-right-radius: 4px;
        border-top-left-radius: 0px;
        border-top-right-radius: 0px;
        min-width: 8ex;
        padding: 7px;
        }
        QStackedWidget {background:white}
        QTabBar QToolButton {
        background:white;
        border: 1px solid #C4C4C3;
        border-top-color: white; /* same as the pane color */
        border-bottom-left-radius: 4px;
        border-bottom-right-radius: 4px;
        border-top-left-radius: 0px;
        border-top-right-radius: 0px;
        }
        """)





class Calculatrice(Panel_simple):
    titre = "Calculatrice" # Donner un titre a chaque module

    def __init__(self, *args, **kw):
        Panel_simple.__init__(self, *args, **kw)
        self.interprete = Interprete(calcul_exact = self.param("calcul_exact"),
                                ecriture_scientifique = self.param("ecriture_scientifique"),
                                formatage_OOo = self.param("formatage_OOo"),
                                formatage_LaTeX = self.param("formatage_LaTeX"),
                                ecriture_scientifique_decimales = self.param("ecriture_scientifique_decimales"),
                                precision_calcul = self.param("precision_calcul"),
                                precision_affichage = self.param("precision_affichage"),
                                appliquer_au_resultat = self.param('appliquer_au_resultat'),
                                simpify = True,
                                ensemble=self.param('ensemble'),
                                )

        bouton = BoutonValider(self)
        bouton.setToolTip("Laissez appuyé pour changer de mode.")
        self.entree = entree = LigneCommande(self, longueur=550,
                                action=self.affichage_resultat, bouton=bouton)
        entree.setToolTip("[Maj]+[Entrée] pour une valeur approchée.")
        self.entree.texte.setContextMenuPolicy(Qt.CustomContextMenu)
        self.entree.texte.customContextMenuRequested.connect(self.EvtMenu)


        self.sizer = sizer = QVBoxLayout()
        sizer.addWidget(entree)
        self.corps = corps = QHBoxLayout()
        sizer.addLayout(corps)
        self.resultats = resultats = QTextEdit(self)
        resultats.setMinimumSize(450, 310)
        resultats.setReadOnly(True)
        corps.addWidget(resultats, 1)
        onglets = OngletsCalc(self)
        corps.addWidget(onglets)
        onglets.setCurrentIndex(self.param('onglet'))
        onglets.currentChanged.connect(self.EvtCurrentChanged)

        self.figure = Figure(figsize=(5,1.3), frameon=True, facecolor="w")
        self.visualisation = FigureCanvas(self.figure)
        self.axes = axes = self.figure.add_axes([0, 0, 1, 1], frameon=False)
        axes.axison = False
        self.pp_texte = axes.text(0.5, 0.5, "", horizontalalignment='center',
                verticalalignment='center', transform = axes.transAxes, size=18)
        self.visualisation.setContextMenuPolicy(Qt.CustomContextMenu)
        self.visualisation.customContextMenuRequested.connect(self.EvtMenuVisualisation)
        sizer.addWidget(self.visualisation)

        self.setLayout(self.sizer)
        self.initialiser()


    def activer(self):
        Panel_simple.activer(self)
        # Actions à effectuer lorsque l'onglet devient actif
        self.entree.setFocus()


    def _sauvegarder(self, fgeo):
        fgeo.contenu["Calculatrice"] = [{}]
        fgeo.contenu["Calculatrice"][0]["Historique"] = [repr(self.entree.historique)]
        fgeo.contenu["Calculatrice"][0]["Affichage"] = [self.resultats.toPlainText()]
        fgeo.contenu["Calculatrice"][0]["Etat_interne"] = [self.interprete.save_state()]



    def _ouvrir(self, fgeo):
        if "Calculatrice" in fgeo.contenu:
            calc = fgeo.contenu["Calculatrice"][0]
            self.initialiser()

            self.entree.historique = eval_safe(calc["Historique"][0])
            resultats = calc["Affichage"][0]
            if resultats:
                resultats += '\n\n'
            self.resultats.setPlainText(resultats)
            self.resultats.moveCursor(QTextCursor.End)
            self.interprete.load_state(calc["Etat_interne"][0])


    def modifier_pp_texte(self, chaine):
        """Modifier le résultat affiché en LaTeX (pretty print)."""
        if self.param("latex"):
            # On utilise directement LaTeX pour le rendu
            chaine = "$" + chaine + "$"
        else:
            # On utilise le parser matplotlib.mathtext, moins complet mais bien
            # plus rapide. Certaines adaptations doivent être faites.
            chaine = latex2mathtext(chaine)
        self.pp_texte.set_text(chaine)
        self.visualisation.draw()

    def vers_presse_papier(self, event = None, texte = None):
        if texte is None:
            texte = self.dernier_resultat
        Panel_simple.vers_presse_papier(texte)

    def copier_latex(self, event = None):
        self.vers_presse_papier(texte = self.interprete.latex_dernier_resultat.strip("$"))

    def initialiser(self, event = None):
        self.dernier_resultat = "" # dernier resultat, sous forme de chaine formatee pour l'affichage
        self.entree.initialiser()
        self.interprete.initialiser()
        self.resultats.clear()

    def affichage_resultat(self, commande, **kw):
        # Commandes spéciales:
        if commande in ('clear', 'clear()', 'efface', 'efface()'):
            self.initialiser()
            self.modifier_pp_texte("Calculatrice réinitialisée.")
            return

        self.modifie = True
        try:
            try:
                ##self.parent.parent.application.processEvents()
                if kw.get("shift"):
                    self.interprete.calcul_exact = False
                resultat, latex = self.interprete.evaluer(commande)
                if latex == "$?$": # provoque une erreur (matplotlib 0.99.1.1)
                    latex = "Désolé, je ne sais pas faire..."
            finally:
                self.interprete.calcul_exact = self.param('calcul_exact')
                self.entree.bouton.mode_normal()
            aide = resultat.startswith("\n== Aide sur ")
            if aide:
                latex = ''
            elif not latex:
                latex = resultat
            #LaTeX
            debug("Expression LaTeX: " + latex)
            try:
                try:
                    # Affichage en LaTeX si possible.
                    self.modifier_pp_texte(latex)
                except Exception:
                    print_error()
                    # Sinon, affichage en texte simple.
                    #  `matplotlib.mathtext` est encore loin d'être
                    # pleinement compatible avec LaTeX !
                    self.modifier_pp_texte(resultat)
            except Exception:
                print_error()
                # Si tout a raté... mais ça ne devrait jamais arrivé.
                self.modifier_pp_texte("<Affichage impossible>")
            #Presse-papier
            self.dernier_resultat = resultat
            if self.param("copie_automatique"):
                if self.param("copie_automatique_LaTeX"):
                    self.copier_latex()
                else:
                    self.vers_presse_papier()
            # TextCtrl
            numero = str(len(self.interprete.derniers_resultats))
            # Évite le décalage entre la première ligne et les suivantes (matrices)
            if "\n" in resultat and not aide:
                resultat = "\n" + "\n".join(20*" " + ligne for ligne in resultat.split("\n"))
            self.resultats.moveCursor(QTextCursor.End)
            self.resultats.insertPlainText(" Calcul n\xb0" + numero + " :   "
                                                        + commande + "\n Résultat :"
                                                        + " "*(4+len(numero))
                                                        + resultat + "\n__________________\n\n")
            self.resultats.moveCursor(QTextCursor.End)
            self.message("Calcul effectué." + self.interprete.warning)
            self.entree.clear()
#            self.resultats.setCursorPosition(len(self.resultats.plainText()))
#            self.resultats.setFocus()
#            self.resultats.ScrollLines(1)
            self.entree.setFocus()
        except Exception:
            self.message("Calcul impossible.")
            self.entree.setFocus()
            if param.debug:
                raise


    def insere(self, event=None, nom='', parentheses=True):
        entree = self.entree
        deb, fin = entree.getSelection()
        if parentheses:
            entree.setCursorPosition(fin)
            entree.insert(")")
            entree.setCursorPosition(deb)
            entree.insert(nom + "(")
            entree.setFocus()
            if deb == fin:
                final = fin + len(nom) + 1
            else:
                final = fin + len(nom) + 2
        else:
            entree.insert(nom)
            final = fin + len(nom)
        entree.setFocus()
        entree.setCursorPosition(final)


    def EvtMenu(self, event):
        menu = QMenu()
        menu.setWindowTitle("Fonctions mathématiques")
        debut = True
        for rubrique in __classement__:
            if not debut:
                menu.addSeparator()
            debut = False
            for titre, nom, doc in (_f for _f in __classement__[rubrique] if _f):
                action = menu.addAction(titre, partial(self.insere, nom=nom, parentheses=(rubrique != "Symboles")))
                # Pas de parenthèses après un symbole.
                action.setToolTip(doc)
        menu.exec_(QCursor.pos())


    def EvtMenuVisualisation(self, event):
        menu = QMenu()
        action = menu.addAction("Copier LaTeX", self.copier_latex)
        action.setToolTip("Copier le code LaTeX dans le presse-papier.")
        menu.exec_(QCursor.pos())

    def EvtCurrentChanged(self, index):
        self.param('onglet', index)

    def param(self, parametre, valeur = no_argument, defaut = False):
        if valeur is not no_argument:
            setattr(self.interprete, parametre, valeur)
        return Panel_simple.param(self, parametre = parametre, valeur = valeur, defaut = defaut)

    def EtatInterne(self, event):
        contenu = self.interprete.save_state()
        h = FenCode(self, "État interne de l'interprète", contenu, self.interprete.load_state)
        h.show()
