from time import perf_counter

from ..Qt import QtCore, QtGui, QtWidgets

__all__ = ['ProgressDialog']


class ProgressDialog(QtWidgets.QProgressDialog):
    """
    Extends QProgressDialog:
    
      * Adds context management so the dialog may be used in `with` statements
      * Allows nesting multiple progress dialogs

    Example::

        with ProgressDialog("Processing..", minVal, maxVal) as dlg:
            # do stuff
            dlg.setValue(i)   ## could also use dlg += 1
            if dlg.wasCanceled():
                raise Exception("Processing canceled by user")
    """
    
    allDialogs = []
    
    def __init__(self, labelText, minimum=0, maximum=100, cancelText='Cancel', parent=None, wait=250, busyCursor=False, disable=False, nested=False):
        """
        ============== ================================================================
        **Arguments:**
        labelText      (required)
        cancelText     Text to display on cancel button, or None to disable it.
        minimum
        maximum
        parent       
        wait           Length of time (im ms) to wait before displaying dialog
        busyCursor     If True, show busy cursor until dialog finishes
        disable        If True, the progress dialog will not be displayed
                       and calls to wasCanceled() will always return False.
                       If ProgressDialog is entered from a non-gui thread, it will
                       always be disabled.
        nested         (bool) If True, then this progress bar will be displayed inside
                       any pre-existing progress dialogs that also allow nesting.
        ============== ================================================================
        """
        # attributes used for nesting dialogs
        self.nestedLayout = None
        self._nestableWidgets = None
        self._nestingReady = False
        self._topDialog = None
        self._subBars = []
        self.nested = nested

        # for rate-limiting Qt event processing during progress bar update
        self._lastProcessEvents = None
        
        isGuiThread = QtCore.QThread.currentThread() == QtCore.QCoreApplication.instance().thread()
        self.disabled = disable or (not isGuiThread)
        if self.disabled:
            return

        noCancel = False
        if cancelText is None:
            cancelText = ''
            noCancel = True
            
        self.busyCursor = busyCursor

        QtWidgets.QProgressDialog.__init__(self, labelText, cancelText, minimum, maximum, parent)
        
        # If this will be a nested dialog, then we ignore the wait time
        if nested is True and len(ProgressDialog.allDialogs) > 0:
            self.setMinimumDuration(2**30)
        else:
            self.setMinimumDuration(wait)
            
        self.setWindowModality(QtCore.Qt.WindowModality.WindowModal)
        self.setValue(self.minimum())
        if noCancel:
            self.setCancelButton(None)
        
    def __enter__(self):
        if self.disabled:
            return self
        if self.busyCursor:
            QtWidgets.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.CursorShape.WaitCursor))
        
        if self.nested and len(ProgressDialog.allDialogs) > 0:
            topDialog = ProgressDialog.allDialogs[0]
            topDialog._addSubDialog(self)
            self._topDialog = topDialog
            topDialog.canceled.connect(self.cancel)
        
        ProgressDialog.allDialogs.append(self)
        
        return self

    def __exit__(self, exType, exValue, exTrace):
        if self.disabled:
            return
        if self.busyCursor:
            QtWidgets.QApplication.restoreOverrideCursor()
            
        if self._topDialog is not None:
            self._topDialog._removeSubDialog(self)
        
        ProgressDialog.allDialogs.pop(-1)

        self.setValue(self.maximum())
        
    def __iadd__(self, val):
        """Use inplace-addition operator for easy incrementing."""
        if self.disabled:
            return self
        self.setValue(self.value()+val)
        return self

    def _addSubDialog(self, dlg):
        # insert widgets from another dialog into this one.
        
        # set a new layout and arrange children into it (if needed).
        self._prepareNesting()
        
        bar, btn = dlg._extractWidgets()
        
        # where should we insert this widget? Find the first slot with a 
        # "removed" widget (that was left as a placeholder)
        inserted = False
        for i,bar2 in enumerate(self._subBars):
            if bar2.hidden:
                self._subBars.pop(i)
                bar2.hide()
                bar2.setParent(None)
                self._subBars.insert(i, bar)
                inserted = True
                break
        if not inserted:
            self._subBars.append(bar)
            
        # reset the layout
        while self.nestedLayout.count() > 0:
            self.nestedLayout.takeAt(0)
        for b in self._subBars:
            self.nestedLayout.addWidget(b)
            
    def _removeSubDialog(self, dlg):
        # don't remove the widget just yet; instead we hide it and leave it in 
        # as a placeholder.
        bar, btn = dlg._extractWidgets()
        bar.hide()

    def _prepareNesting(self):
        # extract all child widgets and place into a new layout that we can add to
        if self._nestingReady is False:
            # top layout contains progress bars + cancel button at the bottom
            self._topLayout = QtWidgets.QGridLayout()
            self.setLayout(self._topLayout)
            self._topLayout.setContentsMargins(0, 0, 0, 0)
            
            # A vbox to contain all progress bars
            self.nestedVBox = QtWidgets.QWidget()
            self._topLayout.addWidget(self.nestedVBox, 0, 0, 1, 2)
            self.nestedLayout = QtWidgets.QVBoxLayout()
            self.nestedVBox.setLayout(self.nestedLayout)
            
            # re-insert all widgets
            bar, btn = self._extractWidgets()
            self.nestedLayout.addWidget(bar)
            self._subBars.append(bar)
            self._topLayout.addWidget(btn, 1, 1, 1, 1)
            self._topLayout.setColumnStretch(0, 100)
            self._topLayout.setColumnStretch(1, 1)
            self._topLayout.setRowStretch(0, 100)
            self._topLayout.setRowStretch(1, 1)
            
            self._nestingReady = True

    def _extractWidgets(self):
        # return:
        #   1. a single widget containing the label and progress bar
        #   2. the cancel button
        
        if self._nestableWidgets is None:
            widgets = [ch for ch in self.children() if isinstance(ch, QtWidgets.QWidget)]
            label = [ch for ch in self.children() if isinstance(ch, QtWidgets.QLabel)][0]
            bar = [ch for ch in self.children() if isinstance(ch, QtWidgets.QProgressBar)][0]
            btn = [ch for ch in self.children() if isinstance(ch, QtWidgets.QPushButton)][0]
            
            sw = ProgressWidget(label, bar)
            
            self._nestableWidgets = (sw, btn)
            
        return self._nestableWidgets
    
    def resizeEvent(self, ev):
        if self._nestingReady:
            # don't let progress dialog manage widgets anymore.
            return
        return super().resizeEvent(ev)

    ## wrap all other functions to make sure they aren't being called from non-gui threads
    
    def setValue(self, val):
        if self.disabled:
            return
        QtWidgets.QProgressDialog.setValue(self, val)
        
        # Qt docs say this should happen automatically, but that doesn't seem
        # to be the case.
        if self.windowModality() == QtCore.Qt.WindowModality.WindowModal:
            now = perf_counter()
            if self._lastProcessEvents is None or (now - self._lastProcessEvents) > 0.2:
                QtWidgets.QApplication.processEvents()
                self._lastProcessEvents = now
        
    def setLabelText(self, val):
        if self.disabled:
            return
        QtWidgets.QProgressDialog.setLabelText(self, val)
    
    def setMaximum(self, val):
        if self.disabled:
            return
        QtWidgets.QProgressDialog.setMaximum(self, val)

    def setMinimum(self, val):
        if self.disabled:
            return
        QtWidgets.QProgressDialog.setMinimum(self, val)
        
    def wasCanceled(self):
        if self.disabled:
            return False
        return QtWidgets.QProgressDialog.wasCanceled(self)

    def maximum(self):
        if self.disabled:
            return 0
        return QtWidgets.QProgressDialog.maximum(self)

    def minimum(self):
        if self.disabled:
            return 0
        return QtWidgets.QProgressDialog.minimum(self)


class ProgressWidget(QtWidgets.QWidget):
    """Container for a label + progress bar that also allows its child widgets
    to be hidden without changing size.
    """
    def __init__(self, label, bar):
        QtWidgets.QWidget.__init__(self)
        self.hidden = False
        self.layout = QtWidgets.QVBoxLayout()
        self.setLayout(self.layout)
        
        self.label = label
        self.bar = bar
        self.layout.addWidget(label)
        self.layout.addWidget(bar)
        
    def eventFilter(self, obj, ev):
        return ev.type() == QtCore.QEvent.Type.Paint
    
    def hide(self):
        # hide label and bar, but continue occupying the same space in the layout
        for widget in (self.label, self.bar):
            widget.installEventFilter(self)
            widget.update()
        self.hidden = True
