#! /usr/bin/python3

from PyQt5.QtWidgets import QMainWindow, QDialog, QApplication, \
    QPlainTextEdit, QFrame, QVBoxLayout, QPushButton, QLabel, QFileDialog
from PyQt5.QtGui import QIcon, QPixmap
from PyQt5.QtCore import Qt, QEvent, QTimer, QLocale, QTranslator, QLibraryInfo

import sys, os, re
from subprocess import run
from xml.dom import minidom
import tempfile

BASEDIR = os.path.dirname(__file__)
sys.path.insert(0, BASEDIR)
UNITS_ERR = re.compile(r"ERROR at (\d+) : (.*)")
UNITS_EXPR = re.compile(r"\s*([-+])?\s*([.0-9]+)\s*(.*)")

ICONOK = QIcon()
ICONBAD = QIcon()
ICONCHECK = QIcon()

from ui_main import Ui_MainWindow
from ui_userGuide import Ui_UserGuide
from ui_about import Ui_About

TEMPLATE = r"""
\documentclass[preview]{{standalone}}
\begin{{document}}
${expr}$
\end{{document}}
"""

def makeImages(latex, scale = 5.0):
    """
    makes two images : one in SVG format, the second one in PNG format
    @param latex the formula in LaTeX language
    @param scale defines scaling for the image, defaults to 5.0
    @return a SVG str and a PNG bytes
    """
    MOREHEIGHT = 2 # to increase the SVG viewBox a little
    doc = TEMPLATE.format(expr=latex)
    with tempfile.NamedTemporaryFile(prefix="units_") as temp:
        tempdirName = temp.name
    run(["mkdir", tempdirName])
    texfileName = os.path.join(tempdirName,"t.tex")
    with open(texfileName,"w") as texfile:
        texfile.write(doc)
    run(["pdflatex", f"-output-directory={tempdirName}", texfileName],
        capture_output=True)
    pdffileName = os.path.join(tempdirName,"t.pdf")
    svgfileName = os.path.join(tempdirName,"t.svg")
    run(["pdf2svg", pdffileName, svgfileName],
        capture_output=True)
    doc = minidom.parse(svgfileName)
    svg = doc.documentElement
    width = float(svg.getAttribute("width")[:-2])   # drop the final "pt"
    height = float(svg.getAttribute("height")[:-2]) # drop the final "pt"
    svg.setAttribute("width", f"{scale*width}pt")
    svg.setAttribute("height", f"{scale*height}pt")
    svg.setAttribute("viewBox", f"0 0 {scale*width} {scale*height+MOREHEIGHT}")
    clip_paths = svg.getElementsByTagName("clipPath")
    for c in clip_paths:
        p=c.parentNode
        p.removeChild(c)
    groups = svg.getElementsByTagName("g")
    for g in groups:
        if g.parentNode is svg:
            # uppermost group: transform it
            g.setAttribute("transform", f"scale( {scale} ) translate(0 {MOREHEIGHT/10})")
        for attr in ("clip-path", "clip-rule"):
            if attr in g.attributes:
                g.removeAttribute(attr)
    svgImage = svg.toxml()
    process = run(
        ["rsvg-convert"],
        input = svgImage.encode("utf-8"), capture_output=True)
    pngImage = process.stdout
    run(["rm", "-rf", tempdirName])
    return svgImage, pngImage


class UserGuideDialog(QDialog, Ui_UserGuide):
    def __init__(self, parent = None):
        super().__init__(parent)
        self.setupUi(self)
        return
    
class AboutDialog(QDialog, Ui_About):
    def __init__(self, parent = None):
        super().__init__(parent)
        self.setupUi(self)
        return
    
class MainMaster(QMainWindow, Ui_MainWindow):

    def __init__(self, parent=None, lang="en"):
        super().__init__(parent)
        self.lang= lang
        self.setupUi(self)
        self.addRenderWidgets()
        self.connectSignals()
        return

    def addRenderWidgets(self):
        ## adding some widgets: LaTeX code viewer/editor
        self.LaTeX = QPlainTextEdit("", self.scrollAreaWidgetContents)
        self.scrollAreaWidgetContents.layout().addWidget(self.LaTeX)
        self.LaTeX.hide()
        ### PNG file viewer/saver
        self.pngFrame = QFrame(self.scrollAreaWidgetContents)
        self.scrollAreaWidgetContents.layout().addWidget(self.pngFrame)
        layout = QVBoxLayout()
        self.pngFrame.setLayout(layout)
        self.pngLabel = QLabel("Here should be the PNG image")
        layout.addWidget(self.pngLabel)
        self.pngButton = QPushButton(self.tr("Save PNG"))
        layout.addWidget(self.pngButton)
        self.pngFrame.hide()
        self.png = None
        ### SVG file viewer
        self.svgFrame = QFrame(self.scrollAreaWidgetContents)
        self.scrollAreaWidgetContents.layout().addWidget(self.svgFrame)
        layout = QVBoxLayout()
        self.svgFrame.setLayout(layout)
        self.svgLabel = QLabel("Here should be the SVG image")
        layout.addWidget(self.svgLabel)
        self.svgButton = QPushButton(self.tr("Save SVG"))
        layout.addWidget(self.svgButton)
        self.svgFrame.hide()
        self.svg = None
        
        return

    def connectSignals(self):
        # signals from the menu
        self.actionQuit.triggered.connect(self.close)
        self.actionAbout.triggered.connect(self.showAbout)
        self.actionUser_Guide.triggered.connect(self.userGuide)
        self.checkButton.clicked.connect(self.check)
        self.input.textChanged.connect(self.resetCheckGroup)
        self.input.installEventFilter(self)
        self.unitCombo.activated.connect(self.convert)
        self.formatChooser.activated.connect(self.render)
        self.pngButton.clicked.connect(self.savePNG)
        self.svgButton.clicked.connect(self.saveSVG)
        return

    def savePNG(self):
        """
        Opens a save dialog to save a PNG file
        """
        filename, filter = QFileDialog.getSaveFileName(
            self, caption=self.tr("Choose a PNG file name"),
            filter=self.tr("PNG Images (*.png)"))
        if filename:
            with open(filename, "wb") as pngfile:
                pngfile.write(self.png)
        return

    def saveSVG(self):
        """
        Opens a save dialog to save a PNG file
        """
        filename, filter = QFileDialog.getSaveFileName(
            self, caption=self.tr("Choose a SVG file name"),
            filter=self.tr("SVG Scalable Images (*.svg)"))
        if filename:
            with open(filename, "w") as pngfile:
                pngfile.write(self.svg)
        return

    def render(self, index):
        """
        callback function to render the expression
        @param index : the current index of self.formatChooser
        """
        ok = self.check()
        # formatChooser: 0 => None (choose a format), 1 => LaTeX,
        #                2 => PNG, 3 => SVG
        if index == 0:
            self.switchScrollAreaContent(None)
        elif index == 1:
            self.LaTeX.setPlainText("")
            self.switchScrollAreaContent("LaTeX")
            if not ok:
                return
            else:
                expr=self.input.text().strip()
                process = run(["units-filter", "-l"],
                              input=expr.encode("utf-8"), capture_output=True)
                reply = process.stdout.decode("utf-8")
                self.LaTeX.setPlainText(reply)
        elif index == 2:
            self.switchScrollAreaContent("PNG")
            if not ok:
                return
            else:
                expr=self.input.text().strip()
                process = run(["units-filter", "-l"],
                              input=expr.encode("utf-8"), capture_output=True)
                reply = process.stdout.decode("utf-8")
                self.svg, self.png = makeImages(reply)
                p = QPixmap()
                p.loadFromData(self.png)
                self.pngLabel.setPixmap(p)
        elif index == 3:
            self.switchScrollAreaContent("SVG")
            if not ok:
                return
            else:
                expr=self.input.text().strip()
                process = run(["units-filter", "-l"],
                              input=expr.encode("utf-8"), capture_output=True)
                reply = process.stdout.decode("utf-8")
                self.svg, self.png = makeImages(reply)
                p = QPixmap()
                p.loadFromData(self.png)
                self.svgLabel.setPixmap(p)
        return
        
        
    def switchScrollAreaContent(self, which = None):
        """
        shows one of the widgets which are in the scroll area
        @param which (None by default); may be None, "Latex", "PNG" or "SVG"
        """
        content = {
            None:    self.labelLogo,
            "LaTeX": self.LaTeX,
            "PNG":   self.pngFrame,
            "SVG":   self.svgFrame,
        }
        for key, widget in content.items():
            if key == which:
                widget.show()
            else:
                widget.hide()
        return

    def convert(self, index):
        """
        Callback to convert a value
        @param index the index of the choosen unit in the combo's list
        """
        unit = self.unitCombo.itemText(index)
        txt = (f"1 {unit}").encode("utf-8")
        process = run(["units-filter"], input=txt, capture_output=True)
        reply = process.stdout.decode("utf-8")
        m = UNITS_ERR.match(reply)
        if m:
            # the wanted unit is not correct
            self.setConvertGroup(
                index, self.tr("Bad unit: {unit}").format(unit=unit), "bad")
            self.unitCombo.removeItem(index)
            self.unitMessage.setText(self.tr("Bad unit: {unit}").format(unit=unit))
            return
        # the wanted unit does exist
        inputText = f"{self.input.text().strip()}:{unit.strip()}"
        process = run(["units-filter", "-o"],
                      input=inputText.encode("utf-8"), capture_output=True)
        reply = process.stdout.decode("utf-8")
        m = UNITS_ERR.match(reply)
        if m:
            # the wanted unit is not matching the input
            m = UNITS_EXPR.match(self.unit.text().strip())
            msg = self.tr("Wanted unit {unit} does not match {baseUnit}").format(
                unit=unit, baseUnit=m.group(3))
            self.setConvertGroup(0, msg, "mismatch")
        else:
            msg = self.tr("Sucessfully converted to {unit}").format(unit=unit)
            self.input.setText(reply)
            self.setConvertGroup(0, msg, "ok")
            self.input.setStyleSheet("color: red; background: lightgreen;")
            QTimer.singleShot(1500, self.normInput)
        return

    def normInput(self):
        """
        callback function to put a normal stylesheet for self.input
        """
        self.input.setStyleSheet("color: black; background: white;")
        return

    def setConvertGroup(self, index, msg, status):
        """
        updates the convert group
        @param index the index of an item in the combo box
        @param msg a message for the lineEdit
        @param status mays be 'ok', 'bad', 'mismatch'
        """
        okCSS = "color: black; background: white;"
        alertCSS = "color: darkred; background: #fff8f8;"
        mismatchCSS = "color: pink; background: #fffff8;"
        if status == 'ok':
            self.unitMessage.setStyleSheet(okCSS)
        elif status == 'bad':
            self.unitCombo.removeItem(index)
            self.unitMessage.setStyleSheet(alertCSS)
        else: # status == 'mismatch' ?
            self.unitMessage.setStyleSheet(mismatchCSS)
        self.unitMessage.setText(msg)
        return
        
    def eventFilter(self, source, event):
        """
        if one type Return or Enter in self.input, 
        check the expression
        """
        if event.type() == QEvent.KeyPress and source is self.input and \
           event.key() in (Qt.Key_Enter, Qt.Key_Return):
            self.check()
        return super().eventFilter(source, event)

    def check(self):
        """
        Check the given expression
        @return True if the expression is correct
        """
        expr = self.input.text().strip()
        ok = True
        if len(expr) == 0:
            self.checkMessage.setText(self.tr("Empty expression"))
            ok = False
        else:
            process = run(["units-filter"],
                          input=expr.encode("utf-8"), capture_output=True)
            reply = process.stdout.decode("utf-8")
            m = UNITS_ERR.match(reply)
            if m:
                # error
                ok = False
                self.setCheckGroup(expr, int(m.group(1)), m.group(2))
            else:
                # fine
                self.setCheckGroup(reply)
        return ok

    def resetCheckGroup(self):
        self.setCheckGroup("", reset=True)
        return

    def setCheckGroup(self, txt, n=-1, errmsg="", reset=False):
        """
        sets both self.checkButton and self.checkMessage;
        resets also the convert group
        @param txt an expression
        @param n the position of an error
        @param errmsg an errortype message (may be "syntax error")
        @param reset if True, both widgets become neutral
        """
        self.setConvertGroup(0, "", "ok") # blanks self.unitMessage
        
        okCSS = "color: black; background: white;"
        alertCSS = "color: darkred; background: #fff8f8;"
        
        if reset:
            self.checkButton.setText(self.tr("???"))
            self.checkButton.setIcon(ICONCHECK)
            self.checkMessage.setStyleSheet(okCSS)
            self.checkMessage.setText("")
        else:
            if n >=0:
                self.checkButton.setText(self.tr("Error at {place}").format(place=n))
                self.checkButton.setIcon(ICONBAD)
                self.checkMessage.setStyleSheet(alertCSS)
                self.checkMessage.setText(
                    f"{errmsg} : {txt[:n-1]}😠{txt[n-1:]}")
            else:
                self.checkButton.setText(self.tr("Syntax is correct"))
                self.checkButton.setIcon(ICONOK)
                self.checkMessage.setStyleSheet(okCSS)
                self.checkMessage.setText(self.to_SIunits(txt))
        return

    def to_SIunits(self,txt):
        """
        @param txt a space-separated sequence of numbers
          the first one is a float value, subsequent numbers
          are powers to be applied to basic SI units
        """
        SI = ("m", "kg", "s", "A", "K", "mol", "cd")
        split = re.split(" +", txt)
        val = split[0]
        split = split[1:]
        result=[]
        for unit, power in zip(SI,split):
            if power == "0":
                continue
            elif power == "1":
                result.append(unit)
            else:
                result.append(f"{unit}^{power}")
        unit = ".".join(result)
        if not unit:
            unit = self.tr("(dimensionless)")
        return f"[SI:] {val} {unit}"

    def bestHelpFile(self, pattern):
        result = os.path.join(BASEDIR, pattern.format(lang=self.lang))
        if not os.path.exists(result):
            result = os.path.join(BASEDIR, pattern.format(lang=self.lang[:2]))
        if not os.path.exists(result):
            result = os.path.join(BASEDIR, pattern.format(lang="en"))
        return result
    
    def showAbout(self):
        d = AboutDialog()
        helpFile = self.bestHelpFile("about.{lang}.html")
        with open(helpFile) as h:
            d.textEdit.setText(h.read())
        d.show()
        d.exec_()
        return

    def userGuide(self):
        d = UserGuideDialog()
        helpFile = self.bestHelpFile("userGuide.{lang}.html")
        with open(helpFile) as h:
            d.textEdit.setText(h.read())
        d.show()
        d.exec_()
        return
    
def run_master():
    app = QApplication(sys.argv)
    ICONOK.addPixmap(QPixmap(":/img/ok.png"), QIcon.Normal, QIcon.Off)
    ICONBAD.addPixmap(QPixmap(":/img/bad.png"), QIcon.Normal, QIcon.Off)
    ICONCHECK.addPixmap(QPixmap(":/img/check.png"), QIcon.Normal, QIcon.Off)

    # translation stuff
    lang=QLocale.system().name()
    t=QTranslator()
    t.load("lang/"+lang, os.path.dirname(__file__))
    app.installTranslator(t)
    t1=QTranslator()
    t1.load("qt_"+lang,
            QLibraryInfo.location(QLibraryInfo.TranslationsPath))
    app.installTranslator(t1)

    m = MainMaster(lang=lang)
    m.show()
    sys.exit(app.exec_())
    
if __name__ == "__main__":
    run_master()
