# from the Python Standard Library
from bisect import bisect

# from the Twisted library
from twisted.internet import reactor
from twisted.python import log

# XXXXX think about persistent clock skew
# XXXXX measure (callLater lag, jitter, clock skew)
# XXXXX unit test
# XXXXX maximize playout buffer and minimize jitter buffer

PLAYOUT_BUFFER_SECONDS=0.8 # stuff up to this many seconds worth of packets into the audio output buffer
JITTER_BUFFER_SECONDS=0.8 # store up to this many seconds worth of packets in the jitter buffer before switching to playout mode
CATCHUP_TRIGGER_SECONDS=1.4 # if we have this many or more seconds worth of packets, drop the oldest ones in order to catch up

EPSILON=0.0001

import time

# DEBUG=False
DEBUG=True

import datetime, time
def ts():
    return datetime.datetime.fromtimestamp(time.time()).isoformat(' ')

def is_run(packets, i, runsecs):
    """
    Returns True iff packets contains a run of sequential packets starting at index i and extending at least runsecs seconds in aggregate length.
    """
    runbytes = runsecs * 16000
    firstseq = packets[i][0]
    runbytessofar = 0
    i2 = 0
    while runbytessofar < runbytes:
        if len(packets) <= i + i2:
            return False
        if packets[i + i2][0] != firstseq + i2:
            return False
        log.msg("%s runbytessofar: %s, packets[%s + %s]: %s, len(...): %s" % (ts(), runbytessofar, i, i2, packets[i + i2], len(packets[i + i2][1]),))
        runbytessofar += len(packets[i + i2][1])
        i2 += 1
    return True

class Playout:
    def __init__(self, medialayer):
        self.medialayer = medialayer
        self.b = [] # (seqno, bytes,)
        self.s = 0 # the sequence number of the (most recent) packet which has gone to the output device
        self.drytime = None # the time at which the audio output device will have nothing to play
        self.refillmode = True # we start in refill mode
        self.nextcheckscheduled = None
        self.st = time.time() 

    def _schedule_next_check(self, delta, t=time.time):
        if self.nextcheckscheduled:
            return
        self.nextcheckscheduled = reactor.callLater(delta, self._do_scheduled_check)
        if DEBUG:
            print "%s xxxxx scheduling next check. now: %0.3f, then: %0.3f, drytime: %0.3f" % (ts(), t() - self.st, self.nextcheckscheduled.getTime() - self.st, self.drytime - self.st,)

    def _do_scheduled_check(self, t=time.time):
        if DEBUG:
            print "%s xxxxxxx doing scheduled check at %0.3f == %0.3f late" % (ts(), t() - self.st, t()-self.nextcheckscheduled.getTime())
        self.nextcheckscheduled = None
        self._consider_playing_out_sample()

    def _consider_playing_out_sample(self, t=time.time, newsampseqno=None):
        if not self.b or self.b[0][0] != (self.s + 1):
            # We don't have a packet ready to play out.
            if t() >= self.drytime:
                self._switch_to_refill_mode()
            return

        if self.drytime and (t() >= self.drytime) and ((not newsampseqno) or (newsampseqno != (self.s + 1))):
            print "%s xxxxxxxxx output device ran dry unnecessarily! now: %0.3f, self.drytime: %s, nextseq: %s, newsampseqno: %s" % (ts(), t() - self.st, self.drytime - self.st, self.b[0][0], newsampseqno,)

        # While the output device would run dry within PLAYOUT_BUFFER_SECONDS from now, then play out another sample.
        while (t() + PLAYOUT_BUFFER_SECONDS >= self.drytime) and self.b and self.b[0][0] == (self.s + 1):
            (seq, bytes,) = self.b.pop(0)
            self.medialayer._d.write(bytes)
            self.s = seq
            packetlen = len(bytes) / float(16000)
            if self.drytime is None:
                self.drytime = t() + packetlen
            else:
                self.drytime = max(self.drytime + packetlen, t() + packetlen)
            if DEBUG:
                print "%s xxxxx played %s, playbuflen ~= %0.3f, jitterbuf: %d:%s" % (ts(), seq, self.drytime and (self.drytime - t()) or 0, len(self.b), map(lambda x: x[0], self.b),)

        # If we filled the playout buffer then come back and consider refilling it after it has an open slot big enough to hold the next packet.  (If we didn't just fill it then when the next packet comes in from the network self.write() will invoke self._consider_playing_out_sample().)
        if self.b and self.b[0][0] == (self.s + 1):
            # Come back and consider playing out again after we've played out an amount of audio equal to the next packet.
            self._schedule_next_check(len(self.b[0][1]) / float(16000) + EPSILON)


    def _switch_to_refill_mode(self):
        self.refillmode = True
        self._consider_switching_to_play_mode()

    def _consider_switching_to_play_mode(self):
        # If we have enough sequential packets ready, then we'll make them be the current packets and switch to play mode.
        for i in range(len(self.b) - 1):
            if is_run(self.b, i, JITTER_BUFFER_SECONDS):
                self.b = self.b[i:]
                self.s = self.b[0][0] - 1 # prime it for the next packet
                self.refillmode = False
                self._consider_playing_out_sample()
                return
        if DEBUG:
            print "%s xxxxxxxxxxxxxxxxxx no runs of size %s found in %s" % (ts(), JITTER_BUFFER_SECONDS, map(lambda x: x[0], self.b),)

    def write(self, bytes, seq, t=time.time):
        assert isinstance(bytes, basestring)

        if not bytes:
            return 0

        i = bisect(self.b, (seq, bytes,))
        if i > 0 and self.b[i-1][0] == seq:
            print "%s xxx duplicate packet %s" % (ts(), seq,)
            return

        self.b.insert(i, (seq, bytes,))

        if DEBUG:
            print "%s xxxxx added  %s, playbuflen ~= %0.3f, jitterbuf: %d:%s" % (ts(), seq, self.drytime and (self.drytime - t()) or 0, len(self.b), map(lambda x: x[0], self.b),)
        if self.refillmode:
            self._consider_switching_to_play_mode()
        else:
            self._consider_playing_out_sample(newsampseqno=seq)
            if self.b and (self.b[0][0] == self.s + 1) and is_run(self.b, 0, CATCHUP_TRIGGER_SECONDS):
                (seq, bytes,) = self.b.pop(0) # catch up
                print "%s xxxxxxx catchup! dropping %s" % (ts(), seq,)
                self.s = self.b[0][0] - 1 # prime it for the next packet


