import time
import sortedcontainers

class TimerScheduler:

    def __init__(self):
        self._timers_by_expire_time = sortedcontainers.SortedDict()

    def now(self):
        return time.monotonic()

    def schedule(self, timer):
        expire_time = timer.expire_time()
        assert expire_time is not None
        if expire_time in self._timers_by_expire_time:
            self._timers_by_expire_time[expire_time].append(timer)
        else:
            self._timers_by_expire_time[expire_time] = [timer]

    def unschedule(self, timer):
        expire_time = timer.expire_time()
        assert expire_time is not None
        assert expire_time in self._timers_by_expire_time
        timers_with_matching_expire = self._timers_by_expire_time[expire_time]
        assert timer in timers_with_matching_expire
        timers_with_matching_expire.remove(timer)
        if timers_with_matching_expire == []:
            self._timers_by_expire_time.pop(expire_time)

    def expired_timers_pending(self):
        if not self._timers_by_expire_time:
            return False
        now = self.now()
        next_expire_time = self._timers_by_expire_time.peekitem(0)[0]
        if next_expire_time > now:
            return False
        return True

    def trigger_all_expired_timers(self):
        # Trigger all expired timers and return time until next expire
        now = self.now()
        while True:
            if not self._timers_by_expire_time:
                return None
            next_expire_time = self._timers_by_expire_time.peekitem(0)[0]
            if next_expire_time > now:
                return next_expire_time - now
            expired_timers = self._timers_by_expire_time.popitem(0)[1]
            for timer in expired_timers:
                timer.trigger_expire()

    def stop_all_timers(self):
        while self._timers_by_expire_time:
            timers = self._timers_by_expire_time.peekitem(0)[1]
            for timer in timers:
                timer.stop()

TIMER_SCHEDULER = TimerScheduler()

class Timer:

    def __init__(self, interval, expire_function, periodic=True, start=True):
        self._running = False
        self._periodic = periodic
        self._interval = interval
        self._expire_time = None
        self._expire_function = expire_function
        if start:
            self.start()

    def __del__(self):
        self.stop()

    def running(self):
        return self._running

    def interval(self):
        return self._interval

    def expire_time(self):
        return self._expire_time

    def remaining_time_str(self):
        if self._running:
            secs_left = self._expire_time - TIMER_SCHEDULER.now()
            return "{:06f} secs".format(secs_left)
        else:
            return "Stopped"

    def start(self):
        if self._running:
            self.stop()
        self._running = True
        self._expire_time = TIMER_SCHEDULER.now() + self._interval
        TIMER_SCHEDULER.schedule(self)

    def stop(self):
        if self._running:
            TIMER_SCHEDULER.unschedule(self)
            self._running = False
            self._expire_time = None

    def trigger_expire(self):
        if self._expire_function is not None:
            self._expire_function()
        if self._periodic:
            # Next expire is not now + interval but current expire_time + interval because the
            # expire function may be called too late when the system is busy, in which case we
            # try to catch up.
            self._expire_time += self._interval
            TIMER_SCHEDULER.schedule(self)
        else:
            self._running = False
            self._expire_time = None