import sys
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
import datetime, os, re, glob
from subprocess import Popen, PIPE, call

class Monitor(QObject):
    """
    Parameters of 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
    """
    def __init__(
            self,
            command, args,
            mainwindow, index, rindex,
            buttonMessage=None,
            actionMessage=None,
            createfiles=[],
            finishCallback=None,
            environ=None
    ):
        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)
        split = QSplitter(self.mainwindow.ui.centralwidget)
        split.setOrientation(Qt.Vertical)
        split.setChildrenCollapsible(False)
        self.layout.addWidget(split)

        gb1 = QGroupBox()
        gb1.setTitle(self.tr("stdout"))
        layout = QVBoxLayout()
        split.addWidget(gb1)
        gb1.setLayout(layout)
        layout.addWidget(self.stdoutTerminal)

        gb2 = QGroupBox()
        gb2.setTitle(self.tr("stderr"))
        layout = QVBoxLayout()
        split.addWidget(gb2)
        gb2.setLayout(layout)
        layout.addWidget(self.stderrTerminal)
        
        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, iso_filesize = None, 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 iso_filesize the size in bytes to be copied; None by default
        @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"
        if "cloneToSda" in args[0]:
            self.iso_filename = "own"
            self.source = "own"
            self.device = "/dev/sda"
        else:
            self.iso_filename = self.args[self.args.index("--source")+1]
            self.source=args[3]
            self.device=args[4]
        self.iso_filesize = iso_filesize
        if self.iso_filesize == None: # not in own cloning
            self.iso_filesize = os.stat(self.iso_filename).st_size
        self.buttonMessage=self.tr("Cloning is over")
        self.actionMessage=self.tr("Cloning")
        # 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_())
