# emacs: -*- mode: python; py-indent-offset: 4; tab-width: 4; indent-tabs-mode: nil -*-
# ex: set sts=4 ts=4 sw=4 noet:
# ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##
#
#   See COPYING file distributed along with the datalad package for the
#   copyright and license terms.
#
# ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##
"""Progress bar implementations to be used.

Should not be imported until we know that interface needs it
"""

import humanize
import sys
import time

from .. import lgr

#
# Haven't found an ideal progress bar yet, so to make things modular etc
# we will provide our interface and adapters for few popular ones
#


class ProgressBarBase(object):
    """Base class for any progress bar"""

    def __init__(self, label=None, fill_text=None, total=None, out=None, unit='B'):
        self.label = label
        self.fill_text = fill_text
        self.total = total
        self.unit = unit
        self.out = out
        self._current = 0

    def refresh(self):
        """Force update"""
        pass

    def update(self, size, increment=False, total=None):
        if total:
            self.total = total
        if not size:
            return
        if increment:
            self._current += size
        else:
            self._current = size

    @property
    def current(self):
        return self._current

    @current.setter
    def current(self, value):
        assert value >= 0, "Total cannot be negative"
        self._current = value

    def start(self, initial=0):
        self._current = initial

    def finish(self, partial=False):
        """

        Parameters
        ----------
        partial: bool
          To signal that finish is called possibly before the activity properly
          finished, so .total count might have not been reached

        Returns
        -------

        """
        pass

    def clear(self):
        pass

    def set_desc(self, value):
        pass  # to override in subclass on how to handle description


class SilentProgressBar(ProgressBarBase):
    def __init__(self, label='', fill_text=None, total=None, unit='B', out=sys.stdout):
        super(SilentProgressBar, self).__init__(total=total)


class LogProgressBar(ProgressBarBase):
    """A progress bar which logs upon completion of the item

    Note that there is also :func:`~datalad.log.log_progress` which can be used
    to get progress bars when attached to a tty but incremental log messages
    otherwise (as opposed to just the final log message provided by
    `LogProgressBar`).
    """

    def __init__(self, *args, **kwargs):
        super(LogProgressBar, self).__init__(*args, **kwargs)
        # I think we never generate progress bars unless we are at the beginning
        # of reporting something lengthy.  .start is not always invoked so
        # we cannot reliably set it there instead of the constructor (here)
        self._start_time = time.time()

    @staticmethod
    def _naturalfloat(x):
        """Return string representation of a number for human consumption

        For abs(x) <= 1000 would use 'scientific' (%g) notation, and for the
        larger a regular int (after rounding)
        """
        return ('%g' % x) if abs(x) <= 1000 else '%i' % int(round(x))

    def _naturalsize(self, x):
        if self.unit == 'B':
            return humanize.naturalsize(x)
        else:
            return '%s%s' % (self._naturalfloat(x), self.unit or '')

    @staticmethod
    def _naturaldelta(x):
        # humanize is too human for little things
        return humanize.naturaldelta(x) \
            if x > 2 \
            else LogProgressBar._naturalfloat(x) + ' sec'

    def finish(self, partial=False):
        msg, args = ' %s ', [self.label]

        if partial:
            # that is the best we know so far:
            amount = self.current
            if self.total is not None:
                if amount != self.total:
                    perc_done = 100. * amount / self.total
                    if perc_done <= 100:
                        msg += "partially (%.2f%% of %s) "
                        args += [
                            perc_done,
                            self._naturalsize(self.total)
                        ]
                    else:
                        # well well -- we still probably have some issue with
                        # over-reporting when getting data from datalad-archives
                        # Instead of providing non-sense % here, just report
                        # our best guess
                        msg += "possibly partially "
                else:
                    # well -- that means that we did manage to get all of it
                    pass
            else:
                msg += "possibly partially "
            msg += "done"
        else:
            # Are we "finish"ed because interrupted or done?
            amount = self.total
            if amount:
                msg += '%s done'
                args += [self._naturalsize(amount)]
            else:
                msg += "done"

        dt = float(time.time() - self._start_time)

        if dt:
            msg += ' in %s'
            args += [self._naturaldelta(dt)]

            if amount:
                speed = amount / dt
                msg += ' at %s/sec'
                args += [self._naturalsize(speed)]

        lgr.info(msg, *args)


progressbars = {
    # let for compatibility, use "none" instead
    'silent':  SilentProgressBar,
    'none':  SilentProgressBar,
    'log': LogProgressBar,
}


try:
    from tqdm import tqdm
    from datalad.support.external_versions import external_versions
    from datalad.utils import updated

    class tqdmProgressBar(ProgressBarBase):
        """Adapter for tqdm.ProgressBar"""

        backend = 'tqdm'
        _frontends = {
            None: tqdm,
            'ipython': None  # to be loaded
        }

        # TQDM behaved a bit suboptimally with older versions -- either was
        # completely resetting time/size in global pbar, or not updating
        # "slave" pbars, so we had to
        # set miniters to 1, and mininterval to 0, so it always updates
        # amd smoothing to 0 so it produces at least consistent average.
        # But even then it is somewhat flawed.
        # Newer versions seems to behave more consistently so do not require
        # those settings
        _default_pbar_params = \
            dict(smoothing=0, miniters=1, mininterval=0.1) \
            if external_versions['tqdm'] < '4.10.0' \
            else dict(mininterval=0.1)

        # react to changes in the terminal width
        if external_versions['tqdm'] >= '2.1':
            _default_pbar_params['dynamic_ncols'] = True

        def __init__(self, label='', fill_text=None,
                     total=None, unit='B', out=sys.stdout, leave=False,
                     frontend=None):
            """

            Parameters
            ----------
            label
            fill_text
            total
            unit
            out
            leave
            frontend: (None, 'ipython'), optional
              tqdm module to use.  Could be tqdm_notebook if under IPython
            """
            super(tqdmProgressBar, self).__init__(label=label,
                                                  total=total,
                                                  unit=unit)

            if frontend not in self._frontends:
                raise ValueError(
                    "Know only about following tqdm frontends: %s. Got %s"
                    % (', '.join(map(str, self._frontends)),
                       frontend))

            tqdm_frontend = self._frontends[frontend]
            if not tqdm_frontend:
                if frontend == 'ipython':
                    from tqdm import tqdm_notebook
                    tqdm_frontend = self._frontends[frontend] = tqdm_notebook
                else:
                    lgr.error(
                        "Something went wrong here, using default tqdm frontend for %s",
                        frontend)
                    tqdm_frontend = self._frontends[frontend] = self._frontends[None]

            self._tqdm = tqdm_frontend
            self._pbar_params = updated(
                self._default_pbar_params,
                dict(desc=label, unit=unit,
                     unit_scale=True, total=total, file=out,
                     leave=leave,
                     ))
            self._pbar = None

        def _create(self, initial=0):
            if self._pbar is None:
                self._pbar = self._tqdm(initial=initial, **self._pbar_params)

        @staticmethod
        def _reset_kludge(pbar, total=None):
            # tqdm didn't have reset() until v4.32.0 (c7015f4,
            # 2019-05-10).  This code below is copied from
            # tqdm.reset() in v4.46.1, replacing "self" with "pbar".
            # It hasn't changed since added in c7015f4.
            #
            # TODO: Drop this and set a minimum tqdm version once
            # Debian stable has v4.32.0.
            pbar.last_print_n = pbar.n = 0
            pbar.last_print_t = pbar.start_t = pbar._time()
            if total is not None:
                pbar.total = total
            pbar.refresh()

        def update(self, size, increment=False, total=None):
            self._create()
            if total is not None:
                # only a reset can change the total of an existing pbar
                try:
                    self._pbar.reset(total)
                except AttributeError:
                    self._reset_kludge(self._pbar, total)
                # we need to (re-)advance the pbar back to the old state
                self._pbar.update(self.current)
                # an update() does not (reliably) trigger a refresh, hence
                # without the next, the pbar may still show zero progress
                if not size:
                    # whenever a total is changed, we need a refresh. If there is
                    # no progress update, we do it here, else we'll do it after
                    # the progress update
                    self._pbar.refresh()
                # if we set a new total and also advance the progress bar:
            if not size:
                return
            inc = size - self.current
            try:
                self._pbar.update(size if increment else inc)
                if total:
                    # refresh to new total and progress
                    self._pbar.refresh()
            except ValueError:
                # Do not crash entire process because of some glitch with
                # progressbar update
                # TODO: issue a warning?
                pass
            super(tqdmProgressBar, self).update(size,
                                                increment=increment,
                                                total=total)

        def start(self, initial=0):
            super(tqdmProgressBar, self).start(initial=initial)
            self._create(initial=initial)

        def refresh(self):
            super(tqdmProgressBar, self).refresh()
            # older tqdms might not have refresh yet but I think we can live
            # without it for a bit there
            if hasattr(self._tqdm, 'refresh'):
                self._pbar.refresh()

        def finish(self, clear=False, partial=False):
            """

            Parameters
            ----------
            clear : bool, optional
              Explicitly clear the progress bar. Note that we are
              creating them with leave=False so they should disappear on their
              own and explicit clear call should not be necessary

            Returns
            -------

            """
            if clear:
                self.clear()
            # be tolerant to bugs in those
            try:
                if self._pbar is not None:
                    self._pbar.close()
            finally:
                self._pbar = None
            try:
                super(tqdmProgressBar, self).finish()
            except Exception as exc:  # pragma: no cover
                #lgr.debug("Finishing tqdmProgresBar thrown %s", str_exc(exc))
                pass

        def clear(self):
            try:
                self._pbar.clear()
            except:
                # if has none -- we can't do anything about it for now ;)
                # 4.7.4 seems to have it
                pass

        def set_desc(self, value):
            self._pbar.desc = value


    progressbars['tqdm'] = tqdmProgressBar
except ImportError:  # pragma: no cover
    pass

assert len(progressbars), "We need tqdm library to report progress"


class AnnexSpecialRemoteProgressBar(ProgressBarBase):
    """Hook up to the special remote and report progress back to annex"""

    def __init__(self, *args, **kwargs):
        # not worth passing anything since we don't care about anything
        remote = kwargs.get('remote')
        super(AnnexSpecialRemoteProgressBar, self).__init__()
        self.remote = remote

    def update(self, *args, **kwargs):
        super(AnnexSpecialRemoteProgressBar, self).update(*args, **kwargs)
        # now use stored value
        if self.remote:
            self.remote.progress(self.current)

progressbars['annex-remote'] = AnnexSpecialRemoteProgressBar
