
from __future__ import absolute_import

import heapq
import logging
import threading
import time

from vdsm.common import concurrent


class Scheduler(object):


    _log = logging.getLogger("Scheduler")

    def __init__(self, name="Scheduler", clock=time.time):
        self._name = name
        self._clock = clock
        self._cond = threading.Condition(threading.Lock())
        self._running = False
        self._calls = []
        self._thread = concurrent.thread(self._run, name=self._name,
                                         log=self._log)

    def start(self):
        self._log.debug("Starting scheduler %s", self._name)
        with self._cond:
            if self._running:
                raise AssertionError("Scheduler already running")
            self._running = True
            self._thread.start()

    def stop(self, wait=False):
        self._log.debug("Stopping scheduler %s", self._name)
        with self._cond:
            self._running = False
            self._cond.notify()
        if wait:
            self._thread.join()

    def schedule(self, delay, callable):
        deadline = self._clock() + delay
        call = ScheduledCall(deadline, callable)
        with self._cond:
            if not self._running:
                raise AssertionError("Scheduler not running")
            heapq.heappush(self._calls, call)
            if self._calls[0] is call:
                self._cond.notify()
        return call

    def _run(self):
        self._log.debug("started")
        try:
            self._loop()
            self._log.debug("stopped")
        finally:
            self._cancel_calls()

    def _loop(self):
        while True:
            with self._cond:
                if not self._running:
                    return
                delay = self._time_until_deadline()
                if delay > 0.0:
                    self._cond.wait(delay)
                    if not self._running:
                        return
                expired = self._pop_expired_calls()
            for call in expired:
                call._execute()

    def _time_until_deadline(self):
        if len(self._calls) > 0:
            return self._calls[0]._deadline - self._clock()
        return self.DEFAULT_DELAY

    def _pop_expired_calls(self):
        now = self._clock()
        expired = []
        while len(self._calls) > 0:
            call = self._calls[0]
            if call._deadline > now:
                break
            heapq.heappop(self._calls)
            if call.valid():
                expired.append(call)
        return expired

    def _cancel_calls(self):
        with self._cond:
            for call in self._calls:
                call.cancel()


class ScheduledCall(object):

    __slots__ = ('_deadline', '_callable')

    _log = logging.getLogger("Scheduler")

    def __init__(self, deadline, callable):
        self._deadline = deadline
        self._callable = callable

    def cancel(self):
        self._callable = _INVALID

    def valid(self):
        return self._callable is not _INVALID

    def _execute(self):
        try:
            self._callable()
        except Exception:
            self._log.exception("Unhandled exception in %s", self._callable)
        finally:
            self._callable = _INVALID


    def __lt__(self, other):
        return self._deadline < other._deadline


def _INVALID():
    pass
