#!/usr/bin/python3

licence={}
licence['en']="""\
This file is part of the project PYACIDOBASIC

pyacidobasic version %s:

a program to simulate acido-basic equilibria

Copyright (C) 2010-2017 Georges Khaznadar <georgesk@debian.org>
          (C) 2013 Jd Bourlier <jd.bourlier@gmail.com>

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 3 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, see
<http://www.gnu.org/licenses/>.
"""

from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *

from .reactifs import setupUi as reactifs_setupUi
from .acidebase import abHTMLparser
from .version import versionString, version
from .phplot import phPlot
from .doubleplotwidget import DoublePlotWidget
from .curveControl import CurveControl

import pyqtgraph as pg
import numpy as np

import os.path, pickle, re, sys

class acidoBasicMainWindow(QMainWindow):
    indicators=['rien.svg', 'bbt.svg', 'phenolphtaleine.svg',
                'helianthine.svg', 'bleu-bromophenol.svg', 'rouge-methyle.svg']
    simulRequest=pyqtSignal()
    clearCourbes=pyqtSignal()
    
    def __init__(self, parent, argv, locale="en_US"):
        """
        Le constructeur
        @param parent une fenêtre parente
        @argv une liste d'arguments
        """       
        
        ######QT
        QMainWindow.__init__(self)
        self.windowTitle="pyAcidoBasic"
        QWidget.__init__(self, parent)
        
        from .Ui_main import Ui_MainWindow
        self.ui = Ui_MainWindow()
        
        self.filename=""

        self.courbesActives=[] # la liste des courbes qu'on peut montrer/cacher
        self.ui.setupUi(self)
        # état initial des boutons radio concentrations/quantités
        self.showConcentrations=True
        # substitution des traceurs de courbes
        ####### traceur orienté pH
        self.ui.verticalLayout_labCourbes.removeWidget(self.ui.plotpH)
        self.ui.plotpH.close()
        self.ui.plotpH=phPlot(
            self.ui.tabWidget,
            axisLabels=("pH", self.labelQte()),
            colors=("b","b"),
        )
        self.ui.verticalLayout_labCourbes.insertWidget (1,self.ui.plotpH)
        ####### traceur orienté concentrations
        self.ui.horizontalLayout_concentrationCourbes.removeWidget(self.ui.plotQM)
        self.ui.plotQM.close()
        self.ui.plotQM=phPlot(
            self.ui.tabWidget,
            axisLabels=(self.labelQte(),"pH"),
            colors=("b","b"),
        )
        self.ui.horizontalLayout_concentrationCourbes.insertWidget (1,self.ui.plotQM)
        ######
        self.plots=(self.ui.plotpH,self.ui.plotQM)
        # volume max versé de la burette
        self.maxBurette=20
        
        ###### INDICATEUR COLORE
        self.clear()
        self.colors()
        ###### INDICATEUR COLORE

        self.configurePlot()
        dbFile=os.path.join("/usr/share/pyacidobasic/lang","db-{loc}.html".format(loc=locale))
        if os.path.exists(dbFile):
            self.abListe=abHTMLparser(dbFile).parseTableToList()
        else:
            locale=locale[:-3]
            dbFile=os.path.join("/usr/share/pyacidobasic/lang","db-{loc}.html".format(loc=locale))
            if os.path.exists(dbFile):
                self.abListe=abHTMLparser(dbFile).parseTableToList()
            else:
                self.abListe=abHTMLparser("/usr/share/pyacidobasic/lang/db-en_US.html").parseTableToList()
        self.filterChanged("") # met en place la liste des acides et des bases
        self.ui_connections()
        self.simulRequest.connect(self.simule)
        self.clearCourbes.connect(self.clear)
        if len(argv)>1:
            if os.path.exists(argv[1]):
                self.load(argv[1])
        

    def labelQte(self):
        """
        @return le label de concentration ou de quantité de matière
        selon le contexte
        """
        if self.showConcentrations:
            return "concentration (mol·L<sup>-1</sup>)"
        else:
            return "quantité de matière (mol)"
        
    def titreQte(self):
        """
        @return le titre de concentration ou de quantité de matière
        selon le contexte
        """
        if self.showConcentrations:
            return "Évolution des concentrations"
        else:
            return "Évolution des quantités de matière"
        
    def simule(self):
        self.clearCourbesControles()
        if not self.simulationPossible():
            self.configurePlot()
            self.replot()
            return
        
        self.vData=[]; self.pHData=[]; self.derivData=[]
        self.maxpH=0.0
        for i in range (1401):
            pH=0.01*i # pH varie de 0 à 14
            try:
                v=-self.ui.listWidgetBecher.charge(pH)/self.ui.listWidgetBurette.charge(pH,1)
                if v>0:
                    self.vData.append(v)
                    self.pHData.append(pH)
                    if len(self.pHData)>1:
                        self.derivData.append(0.01/(self.vData[-1]-self.vData[-2]))
                    else:
                        self.derivData.append(0.0)
                    if pH > self.maxpH: self.maxpH=pH
            except ZeroDivisionError:
                pass
        self.courbesSysteme()
        self.ajouteSolutes()

        # essaie d'ajuster le volume de burette pour voir la partie intéressante
        try:
            # trouve l'abscisse pour avoir ph < maxpH - 0.5
            i=0
            while self.pHData[i] < self.maxpH - 0.5:
                i+=1
                v= self.vData[i]
            # ajuste le volume versé depuis la burette à 5mL près
            self.maxBurette=5*(1+int(v)/5)
        except:
            pass
        self.configurePlot()
        self.replot()
        return

    def ui_connections(self):
        self.ui.toolButtonBurette.clicked.connect(self.videBurette)
        self.ui.toolButtonBecher.clicked.connect(self.videBecher)
        self.ui.toolButtonPlusV.clicked.connect(self.plusV)
        self.ui.toolButtonMoinsV.clicked.connect(self.moinsV)
        self.ui.toolButtonPlusV_2.clicked.connect(self.plusV)
        self.ui.toolButtonMoinsV_2.clicked.connect(self.moinsV)
        self.ui.PDFbutton.clicked.connect(self.plots[0].exportPDF)
        self.ui.SVGbutton.clicked.connect(self.plots[0].exportSVG)
        self.ui.JPGbutton.clicked.connect(self.plots[0].exportJPG)
        self.ui.TitleButton.clicked.connect(self.plots[0].newTitle)
        self.ui.PDFbutton_2.clicked.connect(self.plots[1].exportPDF)
        self.ui.SVGbutton_2.clicked.connect(self.plots[1].exportSVG)
        self.ui.JPGbutton_2.clicked.connect(self.plots[1].exportJPG)
        self.ui.TitleButton_2.clicked.connect(self.plots[1].newTitle)
        self.ui.razFiltreButton.clicked.connect(self.razFiltre)
        self.ui.actionQuitter.triggered.connect(self.close)
        self.ui.actionEnregistrer_Sous.triggered.connect(self.saveAs)
        self.ui.actionEnregistrer.triggered.connect(self.save)
        self.ui.actionOuvrir.triggered.connect(self.load)
        self.ui.actionExemples.triggered.connect(self.loadExample)
        self.ui.action_propos.triggered.connect(self.apropos)
        self.ui.filtreEdit.textChanged.connect(self.filterChanged)
        self.ui.concentrationsButton.toggled.connect(self.QMchanged)
        self.ui.comboBox_indicator1.currentIndexChanged.connect(self.colors)
        return

    def updateWindowTitle(self):
        """
        Met à jour le titre de la fenêtre
        """
        self.setWindowTitle("Pyacidobasic : %s" % os.path.basename(self.filename))
        
            
    def saveAs(self):
        """
        Enregistre les données du graphique sous un nouveau fichier
        """
        fname=QFileDialog.getSaveFileName (self, QApplication.translate("pyacidobasic","File to save")
, filter=QApplication.translate("pyacidobasic","Acidobasic files [*.acb] (*.acb);; All files (*.* *)"))
        fname=fname[0]
        if fname:
            if not re.match(".*\.\S*$",fname):
                fname+=".acb" #on ajoute un suffixe par défaut
            self.filename=fname
            self.updateWindowTitle()
            self.save()

    def save(self):
        """
        Enregistre les données du graphique
        """
        if self.filename:
            p=pickle.Pickler(open(self.filename,"wb"))
            p.dump(versionString.encode("utf-8"))
            burette=self.ui.listWidgetBurette.dump()
            p.dump(burette)
            p.dump(self.ui.listWidgetBecher.dump())
        else:
            self.saveAs()

    def loadExample(self):
        """
        Propose l'ouverture d'un exemple
        """
        self.load(dir="/usr/share/pyacidobasic")

    def load(self, fname="", dir=""):
        """
        Récupère les données depuis un fichier
        """
        if not fname:
            fname=QFileDialog.getOpenFileName (self, QApplication.translate("pyacidobasic", "File to save"), filter=QApplication.translate("pyacidobasic", "Acidobasic files [*.acb] (*.acb);; All files (*.* *)"), directory=dir)[0]
        if fname:
            self.filename=fname
            p=pickle.Unpickler(open(self.filename,"rb"))
            try:
                versionString=p.load().decode("utf-8")
                if versionString!=versionString:
                    raise EnvironmentError
                self.ui.listWidgetBurette.load(p.load())
                self.ui.listWidgetBecher.load(p.load())
                self.updateWindowTitle()
            except:
                QMessageBox.warning (self, QApplication.translate("pyacidobasic", "Version error"), QApplication.translate("pyacidobasic", "This file is not a valid Pyacidobasic file, version %s.") % version)

    def apropos(self):
        """
        Donne un message d'information au sujet de la licence de pyacidobasic
        """
        global locale
        if locale[:2]=="fr":
            l="fr"
        else:
            l="en"
        msg=licence[l] %version
        QMessageBox.information(self, QApplication.translate("pyacidobasic", "About"), msg)
            
    def QMchanged(self, concentration):
        """
        Fonction de rappel pour le changement d'état du bouton radio de
        concentrations.
        @param concentration booléen vrai si on doit afficher des concentrations
        """
        self.showConcentrations=concentration
        self.clear()
        self.courbesSysteme()
        self.ajouteSolutes()
        self.configurePlot()
        return
        
    def razFiltre(self):
        """
        Remet à zéro le filtre pour choisir les réactifs
        """
        self.ui.filtreEdit.clear()

    def plusV(self):
        if self.maxBurette < 5:
            self.maxBurette+=1
        else:
            self.maxBurette+=5
        self.replot()

    def moinsV(self):
        if self.maxBurette >= 10:
            self.maxBurette-=5
        elif self.maxBurette >=2:
            self.maxBurette-=1
        self.replot()

    def replot(self):
        for pw in self.plots:
            pw.setXRange(0,self.maxBurette)
        self.colors()
        return

    def videBurette(self):
        self.ui.listWidgetBurette.vide()
        self.simule()

    def videBecher(self):
        self.ui.listWidgetBecher.vide()
        self.simule()
       
    def filterChanged(self, newtext):
        """
        Fonction de rappel quand le texte du filtre change
        @param newtext le texte actuel du filtre
        """
        reactifs_setupUi(self.ui,self.abListe, newtext)
       
    def maxConcentration(self):
        """
        @return la concentration du plus concentré des réactifs
        """
        burette=self.ui.listWidgetBurette
        becher=self.ui.listWidgetBecher

        maxi = 0
        if len (burette.contenu)>0:
            maxi=burette.contenu[0].c
        for b in becher.contenu:
            if b.c > maxi :
                maxi=b.c
        if maxi <= 0:
            return None
        else:
            return maxi
        
    def maxQuantite(self):
        """
        @return la quantité du plus concentré des réactifs à considérer
        """
        burette=self.ui.listWidgetBurette
        becher=self.ui.listWidgetBecher

        maxi = 0
        if len (burette.contenu)>0:
            maxi=burette.contenu[0].c*20e-3 # pour 20 mL max de burette !
        for b in becher.contenu:
            if b.c > maxi :
                maxi=b.c*1e-3*b.v
        if maxi <= 0:
            return None
        else:
            return maxi
        
    def vTotal(self):
        """
        @return un vecteur de valeurs de volume total de la solution
        """
        becher=self.ui.listWidgetBecher
        totalBecher=0
        for c in becher.contenu:
            totalBecher += c.v
        return totalBecher*np.ones(len(self.pHData))+self.vData

    def nouvelleCourbe(self, titre="", yAxis='right', color="black",
                       width=5, yData=[],
                       pw=None, modifiable=True, checked=False):
        """
        crée une nouvelle courbe active qu'on peut cacher/montrer
        @param titre label pour la courbe
        @param yaxis Choix de l'axe de référence, "left" ou "right"
        @param color la couleur, "black" par défaut
        @param width l'épaisseur, 1 par défaut
        @param yData la liste de données pour Y (c'est self.vData pour X)
        @param pw un PlotWidget, celui des pH par défaut
        @param modifiable si on doit mettre une case active dans le tableau (vrai par défaut)
        @param checked si la case est pré-cochée (faux par défaut)
        """

        if self.row > 3 and color=="black":
            # couleurs variables
            color=self.varColor()
        else:
            color=QColor(color)
            
        if yAxis=="left":
            index=0
        else:
            index=1

        newCourbe=pw.plot(
            x=self.vData, y=yData,
                  pen=pg.mkPen(color, width=width),
                  name=titre,
                  index=index
        )

        # on crée un curveControl quand c'est sur l'axe gauche
        if yAxis=="left":
            newCourbe.cc=CurveControl(
                None,
                mw=self,
                html=titre,
                color=color,
                modifiable=modifiable,
                checked=checked,
                courbe=newCourbe
            )
            if checked:
                newCourbe.show()
            else:
                newCourbe.hide()
            self.ui.curveControlLayout.addWidget(newCourbe.cc)
        self.row+=1
        return

    def varColor(self):
        """
        @return une nouvelle couleur pour chaque self.row
        """
        r=np.sin(self.row*np.pi/2)
        v=np.sin(self.row*np.pi/3)
        b=np.sin(self.row*np.pi/5)
        r=(1+r)/2*255
        v=(1+v)/2*255
        b=(1+b)/2*255
        return QColor(r,v,b)

    def ajouteSolutes(self):
        """
        Ajoute toutes les espèces présentes, dans le menu du graphique
        """
        burette=self.ui.listWidgetBurette
        becher=self.ui.listWidgetBecher

        buretteCalculee=False
        for c in burette.contenu + becher.contenu:
            for i in range(len(c.formes)):
                if buretteCalculee: # la calcul n'est plus pour la burette
                    yData=c.concentrationParPH(
                        i,self.pHData,self.vTotal(),
                        vBurette=None)
                else:
                    yData=c.concentrationParPH(
                        i,self.pHData,self.vTotal(),
                        vBurette=self.vData)
                    buretteCalculee=True
                if self.showConcentrations :
                    titre="[%s]" %c.formes[i]
                else:
                    # on change les données de concentration en qté de matière
                    yData*=1e-3*self.vTotal()
                    titre="n<sub>%s</sub>" %c.formes[i]
                self.nouvelleCourbe(titre=titre,
                                    yAxis='left',
                                    yData=yData,
                                    pw=self.ui.plotQM,
                                    width=2,
                                    modifiable=True,
                                    checked=True,
                                    )
        return
    
    def clearCourbesControles(self):
        """
        Supprime les courbes et leurs contôles
        """
        self.clear()
        s=self.ui.curveControlLayout
        #nettoyage des anciens contrôles
        child=s.takeAt(0)
        while child:
            child.widget().close()
            del child
            child=s.takeAt(0)

    def clear(self):
        """
        Supprimes courbes et legendes
        """
        #nettoyage des anciennes courbes et remise à zéro des légendes
        for p in self.ui.plotpH, self.ui.plotQM :
            p.clearAll()
        # rétablit l'image de fond pour l'indicateur coloré 
        self.indic=pg.ImageItem()
        self.ui.plotpH.addItem(self.indic)
        return

    def courbesSysteme(self):
        """
        Crée les courbes "système" : pH, dpH/dV, [H<sub>3</sub>O<sup>+</sup>], [HO<sup>-</sup>]
        """
        self.clearCourbesControles()
        self.row=0
        ################### le pH ######################
        self.nouvelleCourbe(titre="pH",
                            color="red",
                            yAxis='left',
                            yData=self.pHData,
                            pw=self.ui.plotpH,
                            modifiable=False,
                            checked=True,
        )
        ###### le pH dans la vue des concentrations ###############
        # il faut mettre le pH à la bonne échelle
        self.nouvelleCourbe(titre="pH",
                            color=QColor(255,0,0,50),
                            yAxis='right',
                            yData=np.array(self.pHData),
                            pw=self.ui.plotQM,
                            modifiable=False,
                            checked=True,
                            width=1,
        )
        ###### dpH/dV dans la vue du pH ###############
        # il faut mettre le dpH/dV  à la bonne échelle
        # car on triche, on le met dans l'axe gauche
        d=np.array(self.derivData)
        reduction=14/max(d)
        if d.min()<0:
            d= 14.0+d # décalage en cas de dosage par un acide !
            titre="14+dpH/dV"
        else:
            titre="dpH/dV"
        self.nouvelleCourbe(titre=titre,
                            color="blue",
                            yAxis='left',
                            yData=reduction*d,
                            pw=self.ui.plotpH,
                            width=1,
                            modifiable=False,
                            checked=True,
        )
        ###### dpH/dV dans la vue des concentrations ###############
        # il faut mettre le dpH/dV  à la bonne échelle
        reduction=14/max(d)
        self.nouvelleCourbe(titre=titre,
                            color=QColor(0,0,255,50),
                            yAxis='right',
                            yData=reduction*d,
                            pw=self.ui.plotQM,
                            width=1,
                            modifiable=False,
                            checked=True,
        )
        ###### oxonium dans la vue du pH ###############
        H3OData=np.exp(- 2.302585093*np.array(self.pHData))
        if self.showConcentrations :
            titre="[H<sub>3</sub>O<sup>+</sup>]"
        else:
            H3OData*=1e-3*self.vTotal()
            titre="n<sub>H<sub>3</sub>O<sup>+</sup></sub>"
        self.nouvelleCourbe(titre=titre,
                            color="green",
                            yData=H3OData,
                            pw=self.ui.plotpH,
                            width=1,
                            yAxis="right",
        )
        ###### oxonium dans la vue des concentrations ###############
        # il faut mettre les oxonimums  à la bonne échelle
        H3OData=np.exp(- 2.302585093*np.array(self.pHData))
        if self.showConcentrations :
            titre="[H<sub>3</sub>O<sup>+</sup>]"
        else:
            H3OData*=1e-3*self.vTotal()
            titre="n<sub>H<sub>3</sub>O<sup>+</sup></sub>"
        self.nouvelleCourbe(titre=titre,
                            color="green",
                            yData=H3OData,
                            yAxis='left',
                            pw=self.ui.plotQM,
                            width=2,
                            modifiable=True,
                            checked=True,
        )
        ###### hydroxydes dans la vue du pH ###############
        HOData=1e-14/np.exp(- 2.302585093*np.array(self.pHData))
        if self.showConcentrations :
            titre="[HO<sup>-</sup>]"
        else:
            HOData*=1e-3*self.vTotal()
            titre="n<sub>HO<sup>-</sup></sub>"
        self.nouvelleCourbe(titre=titre,
                            color="magenta",
                            yData=HOData,
                            pw=self.ui.plotpH,
                            width=1,
                            yAxis="right",
        )
        ###### hydroxyde dans la vue des concentrations ###############
        HOData=1e-14/np.exp(- 2.302585093*np.array(self.pHData))
        if self.showConcentrations :
            titre="[HO<sup>-</sup>]"
        else:
            HOData*=1e-3*self.vTotal()
            titre="n<sub>HO<sup>-</sup></sub>"
        self.nouvelleCourbe(titre=titre,
                            color="magenta",
                            yData=HOData,
                            yAxis='left',
                            pw=self.ui.plotQM,
                            width=2,
                            modifiable=True,
                            checked=True,
        )
        return
        

    def simulationPossible(self):
        """
        @return un boolean vrai si une simulation est possible
        """
        return len(self.ui.listWidgetBecher.contenu)>0 and len(self.ui.listWidgetBurette.contenu)>0

    def event(self, ev):
        if ev.type()==QEvent.User:
            self.simule()
            return True
        else:
            return QMainWindow.event(self,ev)


    def concentrationViewRangeY(self):
        """
        @return l'intervalle de concentration à afficher sur
        l'axe vertical
        """
        if self.showConcentrations:
            maxi=self.maxConcentration()
        else:
            maxi=self.maxQuantite()
        if maxi:
            return (0,1.25*maxi)
        else:
            return (0,0.1)

    def configurePlot(self):
        """
        Choisit les axes et les gradue
        """
        if self.showConcentrations:
            yRightMax=self.maxConcentration()
        else:
            yRightMax=self.maxQuantite()
        self.ui.plotpH.setGrid(
            titre="dosage pHmétrique",
            yRightTitre=self.labelQte(),
            yRightMax=yRightMax,
            yLeftMax=14,
        )
        if self.showConcentrations:
            yLeftMax=self.maxConcentration()
        else:
            yLeftMax=self.maxQuantite()
        self.ui.plotQM.setGrid(
            titre=self.titreQte(),
            yLeftTitre=self.labelQte(),
            yLeftMax=yLeftMax,
            yRightMax=14,
        )
        return

    def colors(self):
        """
        met en place la couleur de fond de l'indicateur coloré choisi
        """
        self.svgFile=os.path.join('/usr/share/pyacidobasic/icons/', self.indicators[self.ui.comboBox_indicator1.currentIndex()])
        img = QImage(self.svgFile)
        img = img.convertToFormat(QImage.Format_ARGB32)
        imgArray = pg.imageToArray(img, copy=True)
        self.indic.setImage(image=imgArray)
        self.indic.setRect(QRect(0,0,20,14))


locale="" # cette variable est globale, elle est initialisée au démarrage



def run():
    global locale
    app = QApplication(sys.argv)

    ###translation##
    locale = QLocale.system().name()
    qtTranslator = QTranslator()

    if qtTranslator.load("qt_" + locale):
        app.installTranslator(qtTranslator)
        
    appTranslator = QTranslator()
    if appTranslator.load(os.path.join(os.path.dirname(__file__),"../lang/pyacidobasic_" + locale)):
        # les traductions en mode développement sont prioritaires
        app.installTranslator(appTranslator)
    elif appTranslator.load("/usr/share/pyacidobasic/lang/pyacidobasic_" + locale):
        # par rapport aux traductions déjà installées par le paquet debian
        app.installTranslator(appTranslator)
        
    w = acidoBasicMainWindow(None,sys.argv, locale=locale)
    w.show()
    sys.exit(app.exec_())
