#! /usr/bin/python3

import sys, os, re
# allow local modules like ressource_rc
sys.path.append(os.path.dirname(__file__))

from PyQt5.QtWidgets import QApplication, QMainWindow, QDialog, \
    QAbstractScrollArea, QPushButton
from PyQt5.QtCore import QTranslator, QLocale, Qt, pyqtSignal, QLibraryInfo

from ui_live_clone import Ui_MainWindow
from ui_about import Ui_Dialog as Ui_AboutDialog
from ui_packageEditor import Ui_PackageEditor

from dbus.mainloop.glib import DBusGMainLoop, threads_init
import gi
gi.require_version('UDisks', '2.0')
import usbDisk2

from tools import Tool, MountPoint, FileTreeSelectorModel, MyItem
from wizards import ToolWizard, CloneWizard, RunWizard
from monitor import Monitor
from subprocess import Popen, PIPE
from markdown import markdownFromFile

from datetime import datetime
import io
from subprocess import call

# 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 MyMain(QMainWindow):
    """
    La fenêtre principale de l'application
    """

    keysChanged        = pyqtSignal(str) # means that some USB stick has changed
    newmonitor        = pyqtSignal(Monitor)
    monitorfinished   = pyqtSignal(Monitor)
    monitorclosed     = pyqtSignal(Monitor)
    monitorout        = pyqtSignal(Monitor, str)
    monitorerr        = pyqtSignal(Monitor, str)
    
    def __init__(self, lesCles, owndisk, parent=None):
        """
        Constructeur de la fenêtre principale
        :param lesCles: une instance de usbDisk2.Available
        :param owndisk: une chaîne vide ou quelque chose comme "sdc"
        :type  owndisk: str
        :param parent: fenêtre parente, None par défaut
        """
        QMainWindow.__init__(self, parent)
        self.lesCles=lesCles
        self.owndisk=owndisk
        self.wd=os.path.abspath(os.path.dirname(__file__))
        self.monitors={}
        self.ui=Ui_MainWindow()
        self.ui.setupUi(self)
        self.initTable()
        self.initTab()
        self.initHelpTab()
        
        self.about=QDialog()
        self.about.ui=Ui_AboutDialog()
        self.about.ui.setupUi(self.about)
        self.initAbout()

        self.ui.helpButton.clicked.connect(self.showHelp)
        self.ui.quitButton.clicked.connect(self.close)
        self.ui.toolsButton.clicked.connect(self.toolAction)
        self.ui.cloneButton.clicked.connect(self.cloneAction)
        self.ui.cloneButton1.clicked.connect(self.cloneAction)
        self.ui.runButton.clicked.connect(self.runAction)
        
        self.keysChanged.connect(self.update_keys)
        self.newmonitor.connect(self.new_monitor)
        self.monitorfinished.connect(self.monitor_finished)
        self.monitorclosed.connect(self.monitor_closed)
        self.monitorout.connect(self.monitor_out)
        self.monitorerr.connect(self.monitor_err)
        self.update_keys() # check already connected USB sticks
        return

    def runAction(self):
        """
        Lance un virtualiseur qemu-kvm avec un des disques
        """
        devices=self.not_self_keys(format="udev")
        wiz=RunWizard(self, devices, self.lesCles)
        is_ok = wiz.exec_()
        disks=wiz.field("disks")
        result={}
        if is_ok:
            for d in disks.split(","):
                result[d]=Tool().runDisk(d, self)
        return result

    def cloneAction(self, event):
        """
        Fonction de rappel pour self.ui.toolsButtoncloneButton ; sert à cloner
        les disques
        """
        wiz=CloneWizard(self)
        wiz.exec_()
        return
        
        
    def toolAction(self, event):
        """
        Fonction de rappel pour self.ui.toolsButton
        """
        wiz=ToolWizard(self)
        wiz.exec_()
        return

    def showHelp(self):
        """
        Montre l'aide intégrée
        """
        self.about.show()
        self.ui.tabWidget.setCurrentIndex(0) # ensure the help tab is visible
        return

    def rindexFromDev(self, dev):
        """
        :param dev: un périphérique comme "/dev/sdc"
        :type  dev: str
        :return: l'index de la ligne du widget table qui contient ce
        périphérique, ou None s'il n'est pas là.
        """
        t=self.ui.tableWidget
        rindex=0
        found=False
        for rindex in range(t.rowCount()):
            if t.item(rindex,0).data(0) == dev:
                found=True
                break
        if not found:
            return None
        return rindex
    
    def mdToEdit(self, path, edit):
        """
Remplit une instance de QTextEdit avec un fichier Markdown
        :param path: le chemin du fichier .md, relatif à ce fichier source.
        :type  path: str
        :param edit: le widget d'affichage
        :type  edit: QTextEdit
        """
        path=os.path.join(self.wd, path)
        html=io.BytesIO()
        markdownFromFile(input=path, output=html)
        html.seek(0)
        edit.setHtml(html.read().decode("utf-8"))
        return        

    def initAbout(self):
        """
        Remplit le dialogue "À propos" avec un texte localisé
        """
        self.mdToEdit(self.tr("HelpAbout.md"), self.about.ui.textEdit)
        return
    
    def initHelpTab(self):
        """
        Met en place le fichier d'aide dans le bon onglet
        """
        self.mdToEdit(self.tr("HelpTab.md"), self.ui.textEdit)
        return
    
    def monitor_finished(self, monitor):
        """
        Fonction de rappel pour quand un monitor a fini son job.
        """
        t=self.ui.tableWidget
        t.setItem(monitor.rindex,2,
                  MyItem(self.tr("Clone ready ... Tab #{0}").format(monitor.index)))
        t.resizeColumnsToContents()
        return

    def new_monitor(self, monitor):
        """
        Fonction de rappel pour quand un nouveau moniteur devient prêt
        :param monitor: le nouveau moniteur
        :type  monitor: Monitor
        """
        self.update_monitored_row(monitor)
        return
        
    def monitor_closed(self, monitor):
        """
        Fonction de rappel quand un moniteur est refermé
        :param monitor: le nouveau moniteur
        :type  monitor: Monitor
        """
        device = self.monitors.pop(monitor)
        w=self.ui.tabWidget.widget(monitor.index)
        self.ui.tabWidget.removeTab(monitor.index)
        if w: w.close()
        t=self.ui.tableWidget
        t.setItem(monitor.rindex,2,
                  MyItem(self.tr("Tab closed.")))
        t.resizeColumnsToContents()
        return

    def monitor_out(self, monitor, text):
        """
        Callback function when a monitor sends a text which appeared in stdout
        """
        print("out", monitor, text)
        return
    
    def monitor_err(self, monitor, text):
        """
        Callback function when a monitor sends a text which appeared in stderr
        """
        if monitor.category == "clone_monitor":
            msg = monitor.progresstext(text)
            if msg:
                self.update_monitored_row(monitor, msg)
        return
    

    def makeRow(self, rindex, disk, already):
        """
        Crée une ligne dans la vue table pour un disque
        :param index: numéro de la ligne
        :param disk: l'instance de disque
        :param already: est Vrai si on sait que la première colonne est déjà
        correcte, et que la ligne existe déjà.
        :param dic: un dictionnaire disque => partitions
        :return: une nouvelle valeur pour l'index, augmentée si une ligne a été
        créée.
        """

        def description(partition_ud):
            dev=os.path.basename(partition_ud.path)
            label=partition_ud.label
            label1=label.split(" ")[0]
            if len(label) > 11:
                label=label1[:8]+"..."
            if partition_ud.label:
                return "{}({})".format(dev, label)
            else:
                return dev
            
        t=self.ui.tableWidget
        dic=self.lesCles.disksDict()
        nextIndex=rindex
        shortDisk=os.path.join("/dev",os.path.basename(disk))
        shortParts=self.lesCles.parts_summary(disk)
        if not already:
            t.insertRow(rindex)
            t.setItem(rindex,0,MyItem(shortDisk))
            nextIndex=rindex+1
            # create the button if it is not already there
            button=QPushButton(self.tr("Clone to {}").format(shortDisk))
            b_function=lambda:self.clonage("/dev/{}".format(shortDisk), button, rindex)
            button.clicked.connect(b_function)
            t.setCellWidget(rindex,3,button)
        t.setItem(rindex,1,MyItem(", ".join(shortParts)))
        ## check whether there is still a Monitor with a process for this device
        for m, devicepath in self.monitors.items():
            if shortDisk == os.path.basename(devicepath):
                self.update_monitored_row(m, button)
        return nextIndex

    def update_monitored_row(self, m, msg = ""):
        """
        Quelques trucs à lancer pour la mise à jour d'une ligne de tableau
        si elle est déjà en relation à un quelconque moniteur
        :param m: le nouveau moniteur
        :type  m: Monitor
        :param msg: an extra message (defaults to "")
        """
        t=self.ui.tableWidget
        r=m.rindex
        i=m.index
        t.setItem(
            r, 2,
            MyItem(self.tr("{} ... Tab #{} {}").format(m.actionMessage, i, msg))
        )
        t.resizeColumnsToContents()
        return

    def not_self_keys(self, format=None):
        """
        :param format: si ça vaut "udev" on renverra un chemin udev plutôt
        qu'un chemin uDisks2
        :return: une liste de chemins de disques USB, sauf pour le disque
        USB qui porte éventuellement le système actuellement en fonction.
        """
        if format=="udev":
            return [os.path.join("/dev", os.path.basename(d)) \
                    for d in  sorted(self.lesCles.disksDict()) \
                    if not self.owndisk or self.owndisk not in d]
        # return the uDisks path by default
        return [d for d in  sorted(self.lesCles.disksDict()) \
                if not self.owndisk or self.owndisk not in d]
    
    def update_keys(self):
        """
        Fonction de rappel pour quand un disque ou une clé USB est
        connectée ou déconnectée ou quand ses partitions sont modifiées.
        """
        # builds the list of USB sticks, except the stick enventually
        # used to boot the system
        disks=self.not_self_keys()
        t=self.ui.tableWidget
        rindex=0
        ## first, delete rows with disk references which are no longer valid
        for rindex in list(range(t.rowCount()))[::-1]:
            if t.item(rindex,0).data(0) not in disks:
                t.removeRow(rindex)
        ## then, add new disks and update existing ones
        if disks:
            self.statusBar().showMessage(self.tr("Current USB disks: {}").format(", ".join([os.path.basename(d) for d in disks])))
        else:
            self.statusBar().showMessage(self.tr("No USB stick. Please plug a USB flash disk."))
        for disk in disks:
            devDisk=os.path.join("/dev", os.path.basename(disk))
            while rindex < t.rowCount() and \
                  t.item(rindex,0).data(0) < devDisk:
                ## skip the lines before the place of devDisk
                rindex+=1
            already = rindex < t.rowCount() and \
                t.item(rindex,0).data(0) == devDisk
            rindex=self.makeRow(rindex, disk, already)
        t.resizeColumnsToContents()
        return

    def initTable(self):
        """
        Initialise le tableau des périphériques
        (autres que la clé USB vive éventuellement bootée)
        """
        t=self.ui.tableWidget
        t.setColumnCount(3)
        t.setHorizontalHeaderLabels([
            self.tr("Device"), self.tr("Partitions"),self.tr("Status")
        ])
        t.setSizeAdjustPolicy(QAbstractScrollArea.AdjustToContents)
        return

    def initTab(self):
        """
        Initialise le QTabWidget dans l'interface utilisateur
        retire le widget numéro 1, qui provient du Designer.
        Attention, le widget numéro 0 est spécial : il contient l'aide.
        """
        w=self.ui.tabWidget.widget(1)
        self.ui.tabWidget.removeTab(1)
        w.close()
        return

def main():
    """
    Entrée principale du programme
    """
    app = QApplication(sys.argv)
    # i18n stuff
    locale = QLocale.system().name()
    translation="live_clone_{}.ts".format(locale)
    langPath=os.path.join(os.path.abspath(os.path.dirname(__file__)),"lang",translation)
    translator = QTranslator(app)
    translator.load(langPath)
    app.installTranslator(translator)
    t1=QTranslator(app)
    t1.load(QLocale(), "qt", "_", QLibraryInfo.location(QLibraryInfo.TranslationsPath))
    app.installTranslator(t1)
    t2=QTranslator(app)
    t2.load(QLocale(), "qtbase", "_", QLibraryInfo.location(QLibraryInfo.TranslationsPath))
    app.installTranslator(t2)
    #############
    lesCles=usbDisk2.Available()
    p=Popen("ls /usr/lib/live/mount/persistence", shell=True, stdout=PIPE, stderr=PIPE)
    out, err = p.communicate()
    shortDiskNames=out.decode("utf8").split()
    ownDisk=""
    if shortDiskNames:
        # if we have ['sda1', 'sda3'], this should give "sda"
        ownDisk=shortDiskNames[0][:-1]
    w = MyMain(lesCles, ownDisk)
    # addHook is not designed to work with a method inside an object
    # so, let us define the hook outside the main window, and relay
    # a signal to the main window
    # the slot keysChanged in MyMain accepts a string parameter
    # which is the current device if the system is booted from it
    def show_keys(man, obj):
        if lesCles.modified: # filter to detect only USB stick objects
            w.keysChanged.emit(ownDisk)
        lesCles.modified=False
        return
    lesCles.addHook('object-added',   show_keys)
    lesCles.addHook('object-removed', show_keys)
    
    w.show()
    
    sys.exit(app.exec_())

if __name__ == '__main__':
    main()
