import sys
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
import datetime, os, re

class Monitor(QObject):
    def __init__(
            self,
            command, args,
            mainwindow, index, rindex,
            buttonMessage=None,
            actionMessage=None,
            createfiles=[],
            finishCallback=None,
            environ=None
    ):
        """
        the constructor
        @param command an executable
        @param args the command line args for the executable
        @param mainwindow a QWidget which should have signals monitorfinished
        and monitorclosed, and a QTabWidget in mainwindow.ui.tabWidget
        @param index the index of the wanted tab
        @param rindex the row index to access the table of devices
        @param buttonMessage the text for the button which appears at
        the end of the process
        @param actionMessage a short word to define the ongoing process
        @param createfiles a list of files which might be created: one can
        change their ownership if we are running as SUDO
        @param finishCallback a callback function which should be activated
        when the monitor finishes. This function takes no argument.
        @param environ a shell environment; it can be user to get variables
        SUDO_UID and SUDO_GID
        """
        QObject.__init__(self)
        self.category = "simple_monitor"
        self.index=index
        self.rindex=rindex
        self.command=command
        self.args=args
        self.emit_err = False
        self.emit_out = False
        if buttonMessage==None:
            buttonMessage=self.tr("Close this monitor")
        self.buttonMessage=buttonMessage
        if actionMessage==None:
            actionMessage=self.tr("Working")
        self.actionMessage=actionMessage
        self.createfiles=createfiles
        self.finishCallback=finishCallback
        self.environ=environ
        self.w=mainwindow.ui.tabWidget.widget(index)
        mainwindow.ui.tabWidget.setCurrentIndex(index)
        self.table=mainwindow.ui.tableWidget
        self.mainwindow=mainwindow
        self.process = QProcess(self.w)
        self.process.readyReadStandardOutput.connect(self.readOutput)
        self.process.readyReadStandardError.connect(self.readErr)
        self.process.finished.connect(self.finish)
        self.stdoutTerminal = QPlainTextEdit(self.w)
        self.stderrTerminal = QPlainTextEdit(self.w)
        self.cursor=QTextCursor(self.stdoutTerminal.document());
        self.err_cursor=QTextCursor(self.stderrTerminal.document());
        self.layout = QVBoxLayout(self.w)
        self.layout.addWidget(QLabel(self.tr("stdout")))
        self.layout.addWidget(self.stdoutTerminal, stretch=4)
        self.layout.addWidget(QLabel(self.tr("stderr")))
        self.layout.addWidget(self.stderrTerminal, stretch=1)
        return

    def start(self):
        self.process.start(self.command, self.args)
        # updates row rindex in self.table
        self.mainwindow.newmonitor.emit(self)
        return

    def downgrade_user(self):
        if self.environ and \
           "SUDO_UID" in self.environ and \
           "SUDO_GID" in self.environ:
            ## it is possible to downgrade the owner of the file
            for fname in self.createfiles:
                os.chown(
                    fname,
                    int(os.environ["SUDO_UID"]),
                    int(os.environ["SUDO_GID"])
                )
        self.createfiles=[]
        return

    def close(self):
        d=datetime.datetime.now()
        fname, _ =QFileDialog.getSaveFileName(
            self.mainwindow,
            self.tr("Save the messages?"),
            "{action}-log-{date}.log".format(
                action=self.actionMessage,
                date=d.strftime("%Y-%m-%d--%H-%M")
            ),
            self.tr("Log files (*.log);;All files (*)")
        )
        if fname:
            self.createfiles.append(fname)
            with open(fname, "w") as outfile:
                outfile.write("# STDOUT #\n")
                outfile.write(self.stdoutTerminal.toPlainText())
                outfile.write("\n# STDERR #\n")
                outfile.write(self.stderrTerminal.toPlainText())
        self.downgrade_user()
        self.mainwindow.monitorclosed.emit(self)
        return True
        
    def finish(self):
        button=QPushButton(self.buttonMessage)
        button.clicked.connect(self.close)
        self.layout.addWidget(button)
        self.downgrade_user()
        if self.finishCallback:
            self.finishCallback()
        self.mainwindow.monitorfinished.emit(self)

    def read(self, source, cursor, edit):
        """
        reads a QByteArray from source and writes it into a QPlainTextEdit
        widget
        @param source: either an instance of QProcess.readAllStandardOutput
        or an instance of QProcess.readAllStandardError
        @param cursor a QTextCursor instance
        @param edit a QPlainTextEdit instance
        @return the text line which was just read
        """
        text= bytes(source()).decode("utf-8")
        if text.startswith("\r"):
            ## carriage return
            cursor.select(cursor.LineUnderCursor)
            cursor.removeSelectedText()
            cursor.insertText(text[1:])
        else:
            cursor.insertText(text)
        edit.verticalScrollBar().setValue(edit.verticalScrollBar().maximum())
        return text
    
    def readOutput(self):
        text = self.read(self.process.readAllStandardOutput,
                         self.cursor,
                         self.stdoutTerminal)
        if self.emit_out:
            self.mainwindow.monitorout.emit(self,text)
        return
    
    def readErr(self):
        text = self.read(self.process.readAllStandardError,
                         self.err_cursor,
                         self.stderrTerminal)
        if self.emit_err:
            self.mainwindow.monitorerr.emit(self,text)
        return
    
# a pattern for the output of dd when copying the ISO image
# this pattern reports group(1) = bytes copied so far,
# group(2) = duration in seconds, group(3) = copy speed in GB/s
ddPattern=re.compile(r"^(\d+).*\(.*\).*, (\d+) s, ([.,\d]+) .*/s *$")

class CloneMonitor(Monitor):
    def __init__(self, command, args, mainwindow, index, rindex, environ=None):
        """
        the constructor
        @param command an executable
        @param args the command line args for the executable
        args[3] is the source to clone, args[4] is the targetted device
        @param mainwindow a QWidget which should have signals endmonitor
        and closemonitor, and a QTabWidget in mainwindow.ui.tabWidget
        @param index the index of the wanted tab
        @param rindex the row index to access the table of devices
        @param environ a shell environment; it can be user to get variables
        SUDO_UID and SUDO_GID
        """
        
        Monitor.__init__(self, command, args, mainwindow, index, rindex,
                         environ=environ
        )
        self.category = "clone_monitor"
        self.iso_filename = self.args[self.args.index("--source")+1]
        self.iso_filesize = os.stat(self.iso_filename).st_size
        self.buttonMessage=self.tr("Cloning is over")
        self.actionMessage=self.tr("Cloning")
        self.source=args[3]
        self.device=args[4]
        # this will trigger monitor_err events in the main window
        self.emit_err = True 
        return

    def progresstext(self, text):
        """
        given a text which comes from stderr, works out a progress message
        related to dd's work
        @param text a text issued by dd
        @return a progress message
        """
        def to_hms(t):
            t = int(t)
            h = t // 3600
            t = t - 3600 * h
            m = t // 60
            s = t - 60 * m
            return "{h:02d}:{m:02d}:{s:02d}".format(h=h, m=m, s=s)
        if "\r" in text:
            lines = [l for l in text.split("\r") if l]
            if lines:
                text=lines[0]
            else:
                return ""
        m = ddPattern.match(text)
        if m:
            bc = int(m.group(1)) # bytes copied
            eta = (self.iso_filesize - bc) / bc * float(m.group(2))
            return "{percent:4.2f}%, encore {eta}".format(
                percent = bc/self.iso_filesize*100,
                eta = to_hms(eta)
            )
        else:
            return ""

    
if __name__ == "__main__":
    app = QApplication(sys.argv)
    main = Monitor('ls', ['-lR', '../..'])
    main.show()
    sys.exit(app.exec_())
