#!/usr/bin/python
# $Id:$

import ctypes
import math
import sys
import threading
import time

import pyglet

_debug = pyglet.options['debug_media']

import mt_media

from . import lib_dsound as lib
from pyglet.window.win32 import user32, kernel32


class DirectSoundException(mt_media.MediaException):
    pass


def _db(gain):
    """Convert linear gain in range [0.0, 1.0] to 100ths of dB."""
    if gain <= 0:
        return -10000
    return max(-10000, min(int(1000 * math.log(min(gain, 1))), 0))


class DirectSoundWorker(mt_media.MediaThread):
    _min_write_size = 9600

    # Time to wait if there are players, but they're all full.
    _nap_time = 0.05

    # Time to wait if there are no players.
    _sleep_time = None

    def __init__(self):
        super().__init__()
        self.players = set()

    def run(self):
        while True:
            # This is a big lock, but ensures a player is not deleted while
            # we're processing it -- this saves on extra checks in the
            # player's methods that would otherwise have to check that it's
            # still alive.
            if _debug:
                print('DirectSoundWorker run attempt acquire')

            self.condition.acquire()

            if _debug:
                print('DirectSoundWorker run acquire')

            if self.stopped:
                self.condition.release()
                break
            sleep_time = -1

            if self.players:
                player = None
                write_size = 0
                for p in self.players:
                    s = p.get_write_size()
                    if s > write_size:
                        player = p
                        write_size = s

                if write_size > self._min_write_size:
                    player.refill(write_size)
                else:
                    sleep_time = self._nap_time
            else:
                sleep_time = self._sleep_time

            self.condition.release()
            if _debug:
                print('DirectSoundWorker run release')

            if sleep_time != -1:
                self.sleep(sleep_time)

        if _debug:
            print('DirectSoundWorker exiting')

    def add(self, player):
        if _debug:
            print('DirectSoundWorker add', player)
        self.condition.acquire()
        self.players.add(player)
        self.condition.notify()
        self.condition.release()
        if _debug:
            print('return DirectSoundWorker add', player)

    def remove(self, player):
        if _debug:
            print('DirectSoundWorker remove', player)
        self.condition.acquire()
        try:
            self.players.remove(player)
        except KeyError:
            pass
        self.condition.notify()
        self.condition.release()
        if _debug:
            print('return DirectSoundWorker remove', player)


class DirectSoundAudioPlayer(mt_media.AbstractAudioPlayer):
    # How many bytes the ring buffer should be
    _buffer_size = 44800 * 1

    # Need to cache these because pyglet API allows update separately, but
    # DSound requires both to be set at once.
    _cone_inner_angle = 360
    _cone_outer_angle = 360

    def __init__(self, source_group, player):
        super().__init__(source_group, player)

        # Locking strategy:
        # All DirectSound calls should be locked.  All instance vars relating
        # to buffering/filling/time/events should be locked (used by both
        # application and worker thread).  Other instance vars (consts and
        # 3d vars) do not need to be locked.
        self._lock = threading.RLock()

        # Desired play state (may be actually paused due to underrun -- not
        # implemented yet).
        self._playing = False

        # Up to one audio data may be buffered if too much data was received
        # from the source that could not be written immediately into the
        # buffer.  See refill().
        self._next_audio_data = None

        # Theoretical write and play cursors for an infinite buffer.  play
        # cursor is always <= write cursor (when equal, underrun is
        # happening).
        self._write_cursor = 0
        self._play_cursor = 0

        # Cursor position of end of data.  Silence is written after
        # eos for one buffer size.
        self._eos_cursor = None

        # Indexes into DSound circular buffer.  Complications ensue wrt each
        # other to avoid writing over the play cursor.  See get_write_size and
        # write().
        self._play_cursor_ring = 0
        self._write_cursor_ring = 0

        # List of (play_cursor, MediaEvent), in sort order
        self._events = list()

        # List of (cursor, timestamp), in sort order (cursor gives expiry
        # place of the timestamp)
        self._timestamps = list()

        audio_format = source_group.audio_format

        wfx = lib.WAVEFORMATEX()
        wfx.wFormatTag = lib.WAVE_FORMAT_PCM
        wfx.nChannels = audio_format.channels
        wfx.nSamplesPerSec = audio_format.sample_rate
        wfx.wBitsPerSample = audio_format.sample_size
        wfx.nBlockAlign = wfx.wBitsPerSample * wfx.nChannels // 8
        wfx.nAvgBytesPerSec = wfx.nSamplesPerSec * wfx.nBlockAlign

        dsbdesc = lib.DSBUFFERDESC()
        dsbdesc.dwSize = ctypes.sizeof(dsbdesc)
        dsbdesc.dwFlags = (lib.DSBCAPS_GLOBALFOCUS |
                           lib.DSBCAPS_GETCURRENTPOSITION2 |
                           lib.DSBCAPS_CTRLFREQUENCY |
                           lib.DSBCAPS_CTRLVOLUME)
        if audio_format.channe
ls == 1:
            dsbdesc.dwFlags |= lib.DSBCAPS_CTRL3D
        dsbdesc.dwBufferBytes = self._buffer_size
        dsbdesc.lpwfxFormat = ctypes.pointer(wfx)

        # DSound buffer
        self._buffer = lib.IDirectSoundBuffer()
        driver._dsound.CreateSoundBuffer(dsbdesc,
                 
                        ctypes.byref(self._buffer),
                                         None)

        if audio_format.channels == 1:
            self._buffer3d = lib.IDirectSound3DBuffer()
            self._buffer.QueryInterface(lib.IID_IDirectSound3DBuffer,
                                        ctypes.byref(self._buffer3d))
        else:
            self._buffer3d = None

        self._buffer.SetCurrentPosition(0)

        self.refill(self._buffer_size)

    def __del__(self):
        try:
            self.delete()
        except:
            pass

    def delete(self):
        if driver and driver.worker:
            driver.worker.remove(self)

        self.lock()

        self._buffer.Stop()
        self._buffer.Release()
        self._buffer = None
        if self._buffer3d:
            self._buffer3d.Release()
            self._buffer3d = None

        self.unlock()

    def lock(self):
        self._lock.acquire()

    def unlock(self):
        self._lock.release()

    def play(self):
        if _debug:
            print('DirectSound play')
        driver.worker.add(self)

        self.lock()
        if not self._playing:
            self._playing = True

            self._buffer.Play(0, 0, lib.DSBPLAY_LOOPING)
        self.unlock()
        if _debug:
            print('return DirectSound play')

    def stop(self):
        if _debug:
            print('DirectSound stop')
        driver.worker.remove(self)

        self.lock()
        if self._playing:
            self._playing = False

            self._buffer.Stop()
        self.unlock()
        if _debug:
            print('return DirectSound stop')

    def clear(self):
        if _debug:
            print('DirectSound clear')
        self.lock()
        self._buffer.SetCurrentPosition(0)
        self._play_cursor_ring = self._write_cursor_ring = 0
        self._play_cursor = self._write_cursor
        self._eos_cursor = None
        self._next_audio_data = None
        del self._events[:]
        del self._timestamps[:]
        self.unlock()

    def refill(self, write_size):
        self.lock()
        while write_size > 0:
            if _debug:
                print('refill, write_size =', write_size)
            # Get next audio packet (or remains of last one)
            if self.