#!/usr/bin/env python
"""
Name :          qconcurrency.widgets._progressbar_.py
Created :       Apr 17, 2017
Author :        Will Pittman
Contact :       willjpittman@gmail.com
________________________________________________________________________________
Description :   ProgressBar, that can be updated from a separate thread.
________________________________________________________________________________
"""
#builtin
from __future__    import unicode_literals
from __future__    import absolute_import
from __future__    import division
from __future__    import print_function
from   collections import Iterable
import functools
import uuid
import copy
import logging
import threading
#external
from Qt            import QtWidgets
#internal
from qconcurrency.threading_  import ThreadedTask, SoloThreadedTask

#!TODO: http://stackoverflow.com/questions/7108715/how-to-show-hide-a-child-qwidget-with-a-motion-animation

logger = logging.getLogger(__name__)


class _ProgressSoloThreadedTask( SoloThreadedTask ):
    """
    customized :py:obj:`qconcurrency.threading_.SoloThreadedTask` that
    assigns a new jobid for each thread it starts, and each thread's progress
    is measured entirely independently.

    This way, there is no race-condition where cancelling one thread can knock out
    progress on *all* threads.
    """
    def __init__(self, progressbar, callback, signals=None, connections=None, mutex_expiry=5000 ):
        self._progressbar = progressbar
        SoloThreadedTask.__init__(self,
            callback     = callback,
            signals      = signals,
            connections  = connections,
            mutex_expiry = mutex_expiry,
        )

    def start(self, expiryTimeout=-1, threadpool=None, wait=False, *args, **kwds ):
        """
        Wraps :py:meth:`qconcurrency.threading_.SoloThreadedTask.start` ,
        adding signal-connections so that they update a progressbar.

        See Also:

            * :py:meth:`qconcurrency.threading_.SoloThreadedTask.start`
        """

        jobid = uuid.uuid4().hex

        connections = self._connections.copy()
        if not connections:
            connections = {}

        progbar_connections = {
            'incr_progress' : functools.partial( self._progressbar.incr_progress, jobid=jobid ),
            'add_progress'  : functools.partial( self._progressbar.add_progress,  jobid=jobid ),
            'returned'      : functools.partial( self._progressbar._handle_return_or_abort, jobid=jobid ),
            'exception'     : functools.partial( self._progressbar._handle_return_or_abort, jobid=jobid ),
        }

        for signal in progbar_connections:
            if signal in connections:
                connections[ signal ].append( progbar_connections[signal] )
            else:
                connections[ signal ] = [ progbar_connections[signal] ]


        SoloThreadedTask.start(self,
                           expiryTimeout = expiryTimeout,
                           threadpool    = threadpool,
                           wait          = wait,
                           _connections  = connections,
                           *args,**kwds
                       )



class ProgressBar( QtWidgets.QWidget ):
    """
    A ProgressBar designed to track progress of several threads,
    that is hidden automatically whenever all threads have exited
    (by unhandled exception, or return).

    Each thread is assigned it's own `jobid`, so that it's progress
    is tracked entirely independently of all other pending tasks.
    (my hope is that if errors appear in total-calculated progress in the
    codebase, this will lessen the appearance of an error for the user - each
    thread's progress being set to 100% once it exits).
    """
    def __init__(self):
        QtWidgets.QWidget.__init__(self)

        self._progress = {}         # { jobid: {'total':10, 'current':3} }
        self._cancelled_jobids = [] # rolling log of cancelled jobids (so later unhandled progress is ignored)

        self._progressbar = None    # the ProgressBar Widget
        self.setHidden(True)

        self._initui()

    def _initui(self):

        # Create Widgets
        layout = QtWidgets.QHBoxLayout()
        self._progressbar = QtWidgets.QProgressBar()
        self._reset_btn   = QtWidgets.QPushButton('reset progress')

        # Position Widgets
        self.setLayout( layout )
        layout.addWidget( self._progressbar )
        layout.addWidget( self._reset_btn )

        # Connections
        self._reset_btn.clicked.connect( self.reset )

    def add_progress(self, amount, jobid=None):
        """
        Adds to the total number of required steps
        required to complete.
        """

        with threading.Lock():
            if jobid in self._cancelled_jobids:
                return

            if jobid not in self._progress:
                self._progress[ jobid ] = {'total':amount, 'current':0}
            else:
                self._progress[ jobid ]['total'] += amount

            self.refresh_progress()
            self.setHidden(False)

    def refresh_progress(self):
        """
        ReCalculates current/total progress, and updates the progressbar
        """

        with threading.Lock():
            total_progress   = 0
            current_progress = 0
            for jobid in list(self._progress.keys()):
                job_progress = self._progress[ jobid ]

                if   jobid in self._cancelled_jobids:
                    self._progress.pop(jobid)

                elif job_progress['current'] == job_progress['total']:
                    self._progress.pop(jobid)

                else:
                    total_progress   += self._progress[ jobid ]['total']
                    current_progress += self._progress[ jobid ]['current']

            self._progressbar.setMaximum( total_progress   )
            self._progressbar.setValue(   current_progress )

            if total_progress <= current_progress:
                self.setHidden(True)

    def incr_progress(self, amount, jobid):
        """
        Completes a particular number of steps for
        a particular jobid.
        """

        with threading.Lock():
            if jobid in self._cancelled_jobids:
                return

            if amount == None:
                amount = 1

            if jobid not in self._progress:
                return

            self._progress[ jobid ]['current'] += amount
            self.refresh_progress()

    def reset(self, jobid=None ):
        """
        If not `jobid`, sets all required progress-steps back to 0.
        Otherwise, removes all required progress associated with that
        jobid.
        """

        with threading.Lock():
            if not jobid:
                self._progress = {}
                self._progressbar.reset()

            elif jobid in self._progress:
                self._progress.pop( jobid )

            self.refresh_progress()

    def new_task(self, callback, signals=None, *args, **kwds ):
        """