from mirte.core import Module

from heapq import heappush, heappop

import time
import select
import socket
import threading


class Scheduler(Module):

    class Entry:

        def __init__(self, time, action):
            self.time = time
            self.action = action

        def __cmp__(self, other):
            return cmp(self.time, other.time)

    def __init__(self, *args, **kwargs):
        super(Scheduler, self).__init__(*args, **kwargs)
        self.running = True
        self.sp = socket.socketpair()
        self.queue = []
        # estimated gap between threadPool.execute and execution
        self.gap = 0.0
        # estimated oversleep on time.sleep
        self.delay = 0.0
        self.n_calibration_calls = 0
        self.lock = threading.Lock()

    def _calibrate(self, staged_at):
        diff = time.time() - staged_at
        self.n_calibration_calls += 1
        if abs(diff) < self.calibration_goal:
            self.l.info(
                'calibration done: try: %s; diff: %s < %s; delay: %s; gap: %s'
                % (self.n_calibration_calls,
                    diff,
                    self.calibration_goal,
                    self.delay,
                    self.gap))
            return
        if self.n_calibration_calls % 20 == 0:
            self.l.info(
                'calibration: try: %s; diff: %s >= %s; delay: %s; gap: %s'
                % (self.n_calibration_calls,
                   diff,
                   self.calibration_goal,
                   self.delay,
                   self.gap))
        next = time.time() + self.calibration_delay
        self.plan(next, self._calibrate, next)

    def run(self):
        self._calibrate(time.time())
        self.lock.acquire()
        while self.running:
            if len(self.queue) == 0:
                timeout = None
            else:
                timeout = (self.queue[0].time - time.time()
                           - self.gap)
                if timeout <= 0:
                    event = heappop(self.queue)
                    self._stage(event)
                    continue
            self.lock.release()
            rl, wl, xl = select.select([self.sp[1]], [], [],
                                       timeout)
            if self.sp[1] in rl:
                self.sp[1].recv(4096)
            self.lock.acquire()
        self.lock.release()

    def _stage(self, event):
        self.threadPool.execute_named(
            self._sleep_for,
            'scheduler _sleep_for',
            event,
            time.time()
        )

    def _sleep_for(self, event, staged_at):
        self.gap = .2 * 10 * (time.time() - staged_at) + .8 * self.gap
        tosleep = event.time - time.time() - self.delay
        if tosleep > 0:
            time.sleep(tosleep)
        self.delay = 0.8 * self.delay + \
            0.2 * (time.time() - event.time + self.delay)
        event.action()

    def plan(self, time, func, *args, **kwargs):
        def action():
            func(*args, **kwargs)
        with self.lock:
            heappush(self.queue, Scheduler.Entry(time, action))
        self._wake()

    def _wake(self):
        self.sp[0].send('!')

    def stop(self):
        with self.lock:
            self.running = False
        self._wake()
# vim: et:sta:bs=2:sw=4:
