"""
Some routines to implement the Tools actions
"""
from subprocess import call, Popen, PIPE
from PyQt5.QtWidgets import QMessageBox, QFileSystemModel, QVBoxLayout, \
    QTreeView, QWidget, QPushButton, QDialog, QTableWidgetItem
from PyQt5.QtCore import QObject, QDir, Qt
import gi
gi.require_version('UDisks', '2.0')
from usbDisk2 import uDisk2
import tempfile, os, re
from ui_selectBackupData import Ui_BackupDialog

class FileTreeSelectorModel(QFileSystemModel):
    """
    This code is influenced by a reply to the question
    https://stackoverflow.com/questions/51338059/qfilesystemmodel-qtreeview-traverse-model-filesystem-tree-prior-to-view-e
    """
    def __init__(self, parent=None, rootpath='/', verbose=False):
        QFileSystemModel.__init__(self, None)
        self.root_path      = rootpath
        self.checks         = {}
        self.nodestack      = []
        self.parent_index   = self.setRootPath(self.root_path)
        self.root_index     = self.index(self.root_path)

        self.setFilter(QDir.AllEntries | QDir.Hidden | QDir.NoDot)
        self.directoryLoaded.connect(self._loaded)
        self.verbose=verbose

    def log(self, *messages):
        if self.verbose:
            print(*messages)
        return

    def _loaded(self, path):
        self.log('_loaded', path)

    def data(self, index, role=Qt.DisplayRole):
        if role != Qt.CheckStateRole:
            return QFileSystemModel.data(self, index, role)
        else:
            if index.column() == 0:
                return self.checkState(index)

    def flags(self, index):
            return QFileSystemModel.flags(self, index) | Qt.ItemIsUserCheckable

    def checkState(self, index):
        if index in self.checks:
            return self.checks[index]
        else:
            return Qt.Unchecked # not checked by default

    def setData(self, index, value, role):
        if (role == Qt.CheckStateRole and index.column() == 0):
            self.checks[index] = value
            self.log('setData(): {}'.format(value))
            return True
        return QFileSystemModel.setData(self, index, value, role)

    def traverseDirectory(
            self,
            parentindex,
            callback=None,
            stack=[]
    ):
        """
        recurse the directories and files visible in the view.
        @param parentindex the index of the dir or file being recursed
        @param callback a function which will be called with args
        parentindex and stack
        @param stack a list of previously recursed indices
        """
        self.log('traverseDirectory():')
        callback(parentindex, stack)
        if self.hasChildren(parentindex):
            self.log('|children|: {}'.format(self.rowCount(parentindex)))
            for childRow in range(self.rowCount(parentindex)):
                childIndex = parentindex.child(childRow, 0)
                self.log('child[{}]: recursing'.format(childRow))
                self.traverseDirectory(childIndex, callback=callback, stack=stack+[parentindex])
        else:
            self.log('no children')
        return

    def traverseDirectoryWhileUnchecked(
            self,
            parentindex,
            callback=None,
            stack=[]
    ):
        """
        recurse the directories and files visible in the view, and
        stops recursion wherever an item is checked.
        @param parentindex the index of the dir or file being recursed
        @param callback a function which will be called with args
        parentindex and stack
        @param stack a list of previously recursed indices
        """
        self.log('traverseDirectory():')
        callback(parentindex, stack)
        if self.checkState(parentindex)==Qt.Unchecked and \
           self.hasChildren(parentindex):
            self.log('|children|: {}'.format(self.rowCount(parentindex)))
            for childRow in range(self.rowCount(parentindex)):
                childIndex = parentindex.child(childRow, 0)
                self.log('child[{}]: recursing'.format(childRow))
                self.traverseDirectoryWhileUnchecked(childIndex, callback=callback, stack=stack+[parentindex])
        else:
            self.log('no children')
        return

    def printIndex(self, index):
        self.log('model printIndex(): {}'.format(self.filePath(index)))


class MountPoint:
    """
    Implements a mount point which will be associated with a file system
    and can be either permanent or temporary; it can be used in "with" 
    statements, ensuring that the file system is mounted in inside the block
    controlled by the statement, and unmounted when the block is finished
    if it was not mounted previously.
    """
    def __init__(self, p):
        """
        the constructor
        @param p a single partition of a disk, 
        given either as a string (/dev/something or
        /org/freedesktop/UDisks2/block_devices/something)
        or as a uDisk2 instance
        """
        orgPath="/org/freedesktop/UDisks2/block_devices"
        if type(p) == str:
            if p.startswith(orgPath):
                self._path=p
            elif p.startswith("/dev/"):
                self._path=os.path.join(orgPath, os.path.basename(p))
            else:
                raise Exception("Not a partition path: {}".format(p))
            devStuff=os.path.join("/dev", os.path.basename(self._path))
            self._partition=uDisk2(self.path, device=devStuff)
        elif isinstance(p, uDisk2):
            # reread the current properties, which might be volatile
            self._partition=uDisk2(p.path, device=p.devStuff)
            self._path=p.path
        else:
            raise Exception("Expected some other argument or type : {} type={}".format(p, type(p)))
        self._capacity = self._partition.capacity
        self._mountpoint=None
        self._temp=None
        return

    def __str__(self):
        return self._mountpoint

    def __repr__(self):
        return "MountPoint(path={_path}, dir={_mountpoint})".format(**self.__dict__)

    @property
    def path(self):
        return self._path

    @property
    def partition(self):
        return self._partition

    @property
    def mountpoint(self):
        return self._mountpoint

    @property
    def is_temporary(self):
        return self._temp

    @property
    def capacity(self):
        return self._capacity

    def enter_(self):
        pipe=Popen(
            "mount| grep {}".format(self._partition.devStuff),
            stdout=PIPE,
            stderr=PIPE,
            shell=True
        )
        out, err = pipe.communicate()
        out=out.decode("utf-8").strip()
        if out:
            m=re.match(r"{} on (.+) type .*".format(self._partition.devStuff), out)
            # the partition is mounted, no need to umount it
            self._mountpoint = m.group(1)
            self._temp = False
        elif self._partition.capacity: # only for existing partitions
            mp=""
            with tempfile.TemporaryDirectory(prefix="live_clone_") as temp:
                mp=temp
                # temp disappears when this block is over
            call("mkdir {}".format(mp), shell=True)
            call("mount {} {}".format(self._partition.devStuff, mp), shell=True)
            self._mountpoint = mp
            self._temp = True
        else:
            self._mountpoint = ""
        return
    
    def __enter__(self):
        """
        callback activated when one enters a With statement
        @return self
        """
        self.enter_()
        return self

    def exit_(self):
        if self._temp:
            # temporary mount points are dismissed
            call("umount {}".format(self._partition.devStuff), shell=True)
            call("rmdir {}".format(self._mountpoint), shell=True)
        return
    
    def __exit__(self, type, value, tb):
        """
        Callback to exit a With statement; unmounts temporary mounts
        @param type type of Exception if any
        @param value parameter of the exception
        @param tb an TraceBack instance
        @return True is no exception occured
        """
        self.exit_()
        return not tb    
        
                
class Tool(QObject):
    """
    This class is a subclass of QObject to inherit translation functions.
    real tools at its methods
    """
    
    def runDisk(self, d, window, memory="1G"):
        """
        run a disk in a virtual machine
        @param d a disk given as a uDisk2 instance
        @param window a QMainWindow instance
        @param memory the amount of RAM left to the virtual machine (default=1G)
        """
        cmd="(kvm -m {} -drive file={},format=raw &)".format(memory, d)
        call(cmd, shell=True)
        return

    def select_backup(self, d, window):
        """
        Work out a list of paths to directories one wants to backu
        @param d a disk given as a /dev/sdx string
        @param window a QMainWindow instance
         """
        ## the path to personal data is:
        ## <mount point>/rw/home
        ## the partition to mount is the partition #3
        with MountPoint(d+"3") as mp:
            rootpath=os.path.join(str(mp), "rw", "home")
            d=QDialog(window)
            d.ui=Ui_BackupDialog()
            d.ui.setupUi(d)
            d.model = FileTreeSelectorModel(rootpath=rootpath)
            d.model.setRootPath(rootpath)
            d.ui.tree.setModel(d.model)
            d.ui.tree.setRootIndex(d.model.parent_index)

            d.ui.tree.setAnimated(False)
            d.ui.tree.setIndentation(20)
            d.ui.tree.setSortingEnabled(True)

            d.ui.tree.setWindowTitle("Dir View")

            ok=d.exec_()
            to_save=[]
            def recurseFiles():
                def cb(i, stack):
                    if d.model.checkState(i)==Qt.Checked:
                        pathdata=[index.data() for index in stack+[i]]
                        to_save.append(os.path.join(*pathdata))
                    return
                i=d.model.parent_index
                d.model.traverseDirectoryWhileUnchecked(i,callback=cb)
                return
            if ok:
                recurseFiles()
            ### the result is in to_save
        return

    def erase_persistence(self, p, window):
        """
        Erase persistence data from a partition
        @param p a partition given as a uDisk2 instance
        @parm window a QMainWindow instance
        """
        if os.getuid()!= 0:
            # we are not root
            QMessageBox.critical(
                window,
                self.tr("Cannot erase persistence data"),
                self.tr("One needs root priviledge to erase persistence data")
            )
            return
        with MountPoint(p) as mp:
            # erase directories work and rw from the persistence partition
            call("cd {}; rm -rf work rw".format(mp), shell=True)
            confFile=os.path.join(str(mp),"persistence.conf")
            if not os.path.exists(confFile):
                # the file persistence.conf must be eventually restored
                call("cd {}; echo '/ union' > persistence.conf".format(mp), shell=True)
        QMessageBox.information(
            window,
            self.tr("Persistence erased"),
            self.tr("Persistence data have been erased from {}").format(p.devStuff)
        )
        return

class MyItem(QTableWidgetItem):
    """
    non-editable QTableWidgetItem items
    """
    def __init__(self, text):
        QTableWidgetItem.__init__(self,text)
        self.setFlags(self.flags() & ~Qt.ItemIsEditable)
        return

