#! /usr/bin/python3

import sys, os
# 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


class MyMain(QMainWindow):

    keysChanged  = pyqtSignal(str) # means that some USB stick has changed
    newmonitor = pyqtSignal(Monitor)
    endmonitor   = pyqtSignal(Monitor)
    closemonitor = pyqtSignal(Monitor)
    
    def __init__(self, lesCles, owndisk, parent=None):
        """
        Main window for the application
        @param lesCles an instance of usbDisk2.Available
        @param owndisk an empty string or something like "sdc"
        @param parent parent window, defaults to None
        """
        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.endmonitor.connect(self.monitor_finished)
        self.closemonitor.connect(self.monitor_closed)
        self.update_keys() # check already connected USB sticks
        return

    def runAction(self):
        """
        Run one drive in a virtual machine provided by qemu-kvm
        """
        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):
        """
        callback for self.ui.toolsButtoncloneButton
        """
        wiz=CloneWizard(self)
        wiz.exec_()
        return
        
        
    def toolAction(self, event):
        """
        callback for self.ui.toolsButton
        """
        wiz=ToolWizard(self)
        wiz.exec_()
        return

    def showHelp(self):
        self.about.show()
        self.ui.tabWidget.setCurrentIndex(0) # ensure the help tab is visible
        return

    def rindexFromDev(self, dev):
        """
        @para dev a device
        @return the index of the row of the table widget which contains
        the device, or None if it is not found
        """
        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):
        """
        Feeds a QTextEdit instance with a Markdown file
        @param path the path to the .md file, relative to this file
        @param edit a QTextEdit instance
        """
        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):
        """
        Feeds the Dialog with a localized text
        """
        self.mdToEdit(self.tr("HelpAbout.md"), self.about.ui.textEdit)
        return
    
    def initHelpTab(self):
        """
        converts the Markdown help file and writes it into the help tab
        """
        self.mdToEdit(self.tr("HelpTab.md"), self.ui.textEdit)
        return
    
    def monitor_finished(self, monitor):
        """
        callback triggered when a monitor finished its 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):
        """
        callback triggered when a new monitor is ready
        @param monitor a Monitor instance
        """
        self.update_monitored_row(monitor)
        return
        
    def monitor_closed(self, monitor):
        """
        callback triggered when a monitor is closed
        @param monitor a Monitor instance
        """
        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 makeRow(self, rindex, disk, already):
        """
        Makes a row in the table view for a disk
        @param index of a row
        @param disk the disk instance
        @param already is True when the first colums is already correct,
        and the row already exists
        @param dic a dictionary disk => partitions
        @return a new value for the index, increased when a row is created
        """

        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):
        """
        Some things to run to update a row table if it is already
        related to some monitor
        @param m an instance of Monitor
        """
        t=self.ui.tableWidget
        r=m.rindex
        i=m.index
        t.setItem(
            r, 2,
            MyItem(self.tr("{} ... Tab #{}").format(m.actionMessage, i))
        )
        t.resizeColumnsToContents()
        return

    def not_self_keys(self, format=None):
        """
        @param format if this is "udev", will return udev paths
        rather than uDisks2 paths
        @return a list of USB paths, except the own disk which was booted
        as a freeduc system
        """
        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):
        """
        callback function used when a USB stick is plugged in or off
        or when its partitins are modified.
        """
        # 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):
        """
        initialize the table of devices
        """
        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):
        """
        initialize the QTabWidget in the user interface
        removes the widget #1, which comes from Designer
        widget #0 is special: it contains some help
        """
        w=self.ui.tabWidget.widget(1)
        self.ui.tabWidget.removeTab(1)
        w.close()
        return

def main():
    """
    The main call
    """
    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()
