# ----------------------------------------------------------------------------
# pyglet
# Copyright (c) 2006-2008 Alex Holkner
# Copyright (c) 2008-2020 pyglet contributors
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
#  * Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
#  * Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in
#    the documentation and/or other materials provided with the
#    distribution.
#  * Neither the name of pyglet nor the names of its
#    contributors may be used to endorse or promote products
#    derived from this software without specific prior written
#    permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
# ----------------------------------------------------------------------------
"""
Pythonic interface to DirectSound.
"""
from collections import namedtuple
import ctypes
import weakref

from pyglet.debug import debug_print
from pyglet.window.win32 import _user32

from . import lib_dsound as lib
from .exceptions import DirectSoundNativeError

_debug = debug_print('debug_media')


def _check(hresult):
    if hresult != lib.DS_OK:
        raise DirectSoundNativeError(hresult)


class DirectSoundDriver:
    def __init__(self):
        assert _debug('Constructing DirectSoundDriver')

        self._native_dsound = lib.IDirectSound()
        _check(
            lib.DirectSoundCreate(None, ctypes.byref(self._native_dsound), None)
        )

        # A trick used by mplayer.. use desktop as window handle since it
        # would be complex to use pyglet window handles (and what to do when
        # application is audio only?).
        hwnd = _user32.GetDesktopWindow()
        _check(
            self._native_dsound.SetCooperativeLevel(hwnd, lib.DSSCL_NORMAL)
        )

        self._buffer_factory = DirectSoundBufferFactory(self._native_dsound)
        self.primary_buffer = self._buffer_factory.create_primary_buffer()

    def __del__(self):
        self.primary_buffer = None
        self._native_dsound.Release()

    def create_buffer(self, audio_format):
        return self._buffer_factory.create_buffer(audio_format)

    def create_listener(self):
        return self.primary_buffer.create_listener()


class DirectSoundBufferFactory:
    default_buffer_size = 2.0

    def __init__(self, native_dsound):
        # We only keep a weakref to native_dsound which is owned by
        # interface.DirectSoundDriver
        self._native_dsound = weakref.proxy(native_dsound)

    def create_buffer(self, audio_format):
        buffer_size = int(audio_format.sample_rate * self.default_buffer_size)
        wave_format = self._create_wave_format(audio_format)
        buffer_desc = self._create_buffer_desc(wave_format, buffer_size)
        return DirectSoundBuffer(
                self._create_buffer(buffer_desc),
                audio_format,
                buffer_size)

    def create_primary_buffer(self):
        return DirectSoundBuffer(
                self._create_buffer(self._create_primary_buffer_desc()),
                None,
                0)

    def _create_buffer(self, buffer_desc):
        buf = lib.IDirectSoundBuffer()
        _check(
            self._native_dsound.CreateSoundBuffer(buffer_desc, ctypes.byref(buf), None)
        )
        return buf

    @staticmethod
    def _create_wave_format(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
        return wfx

    @classmethod
    def _create_buffer_desc(cls, wave_format, buffer_size):
        dsbdesc = lib.DSBUFFERDESC()
        dsbdesc.dwSize = ctypes.sizeof(dsbdesc)
        dsbdesc.dwFlags = (lib.DSBCAPS_GLOBALFOCUS |
                           lib.DSBCAPS_GETCURRENTPOSITION2 |
                           lib.DSBCAPS_CTRLFREQUENCY |
                           lib.DSBCAPS_CTRLVOLUME)
        if wave_format.nChannels == 1:
            dsbdesc.dwFlags |= lib.DSBCAPS_CTRL3D
        dsbdesc.dwBufferBytes = buffer_size
        dsbdesc.lpwfxFormat = ctypes.pointer(wave_format)

        return dsbdesc

    @classmethod
    def _create_primary_buffer_desc(cls):
        """Primary buffer with 3D and volume capabilities"""
        buffer_desc = lib.DSBUFFERDESC()
        buffer_desc.dwSize = ctypes.sizeof(buffer_desc)
        buffer_desc.dwFlags = (lib.DSBCAPS_CTRL3D |
                               lib.DSBCAPS_CTRLVOLUME |
                               lib.DSBCAPS_PRIMARYBUFFER)

        return buffer_desc

class DirectSoundBuffer:
    def __init__(self, native_buffer, audio_format, buffer_size):
        self.audio_format = audio_format
        self.buffer_size = buffer_size

        self._native_buffer = native_buffer

        if audio_format is not None and audio_format.channels == 1:
            self._native_buffer3d = lib.IDirectSound3DBuffer()
            self._native_buffer.QueryInterface(lib.IID_IDirectSound3DBuffer,
                                               ctypes.byref(self._native_buffer3d))
        else:
            self._native_buffer3d = None

    def __del__(self):
        self.delete()

    def delete(self):
        if self._native_buffer is not None:
            self._native_buffer.Stop()
            self._native_buffer.Release()
            self._native_buffer = None
            if self._native_buffer3d is not None:
                self._native_buffer3d.Release()
                self._native_buffer3d = None

    @property
    def volume(self):
        vol = lib.LONG()
        _check(
            self._native_buffer.GetVolume(ctypes.byref(vol))
        )
        return vol.value

    @volume.setter
    def volume(self, value):
        _check(
            self._native_buffer.SetVolume(value)
        )

    _CurrentPosition = namedtuple('_CurrentPosition', ['play_cursor', 'write_cursor'])

    @property
    def current_position(self):
        """Tuple of current play position and current write position.
        Only play position can be modified, so setter only accepts a single value."""
        play_cursor = lib.DWORD()
        write_cursor = lib.DWORD()
        _check(
            self._native_buffer.GetCurrentPosition(play_cursor,
                                                   write_cursor)
        )
        return self._CurrentPosition(play_cursor.value, write_cursor.value)

    @current_position.setter
    def current_position(self, value):
        _check(
            self._native_buffer.SetCurrentPosition(value)
        )

    @property
    def is3d(self):
        return self._native_buffer3d is not None

    @property
    def is_playing(self):
        return (self._get_status() & lib.DSBSTATUS_PLAYING) != 0

    @property
    def is_buffer_lost(self):
        return (self._get_status() & lib.DSBSTATUS_BUFFERLOST) != 0

    def _get_status(self):
        status = lib.DWORD()
        _check(
            self._native_buffer.GetStatus(status)
        )
        return status.value

    @property
    def position(self):
        if self.is3d:
            position = lib.D3DVECTOR()
            _check(
                self._native_buffer3d.GetPosition(ctypes.byref(position))
            )
            return position.x, position.y, position.z
        else:
            return 0, 0, 0

    @position.setter
    def position(self, position):
        if self.is3d:
            x, y, z = position
            _check(
                self._native_buffer3d.SetPosition(x, y, z, lib.DS3D_IMMEDIATE)
            )

    @property
    def min_distance(self):
        """The minimum distance, which is the distance from the
        listener at which sounds in this buffer begin to be attenuated."""
        if self.is3d:
            value = lib.D3DVALUE()
            _check(
                self._native_buffer3d.GetMinDistance(ctypes.byref(value))
            )
            return value.value
        else:
            return 0

    @min_distance.setter
    def min_distance(self, value):
        if self.is3d:
            _check(
                self._native_buffer3d.SetMinDistance(value, lib.DS3D_IMMEDIATE)
            )

    @property
    def max_distance(self):
        """The maximum distance, which is the distance from the listener beyond which
        sounds in this buffer are no longer attenuated."""
        if self.is3d:
            value = lib.D3DVALUE()
            _check(
                self._native_buffer3d.GetMaxDistance(ctypes.byref(value))
            )
            return value.value
        else:
            return 0

    @max_distance.setter
    def max_distance(self, value):
        if self.is3d:
            _check(
                self._native_buffer3d.SetMaxDistance(value, lib.DS3D_IMMEDIATE)
            )

    @property
    def frequency(self):
        value = lib.DWORD()
        _check(
            self._native_buffer.GetFrequency(value)
        )
        return value.value

    @frequency.setter
    def frequency(self, value):
        """The frequency, in samples per second, at which the buffer is playing."""
        _check(
            self._native_buffer.SetFrequency(value)
        )

    @property
    def cone_orientation(self):
        """The orientation of the sound projection cone."""
        if self.is3d:
            orientation = lib.D3DVECTOR()
            _check(
                self._native_buffer3d.GetConeOrientation(ctypes.byref(orientation))
            )
            return orientation.x, orientation.y, orientation.z
        else:
            return 0, 0, 0

    @cone_orientation.setter
    def cone_orientation(self, value):
        if self.is3d:
            x, y, z = value
            _check(
                self._native_buffer3d.SetConeOrientation(x, y, z, lib.DS3D_IMMEDIATE)
            )

    _ConeAngles = namedtuple('_ConeAngles', ['inside', 'outside'])
    @property
    def cone_angles(self):
        """The inside and outside angles of the sound projection cone."""
        if self.is3d:
            inside = lib.DWORD()
            outside = lib.DWORD()
            _check(
                self._native_buffer3d.GetConeAngles(ctypes.byref(inside), ctypes.byref(outside))
            )
            return self._ConeAngles(inside.value, outside.value)
        else:
            return self._ConeAngles(0, 0)

    def set_cone_angles(self, inside, outside):
        """The inside and outside angles of the sound projection cone."""
        if self.is3d:
            _check(
                self._native_buffer3d.SetConeAngles(inside, outside, lib.DS3D_IMMEDIATE)
            )

    @property
    def cone_outside_volume(self):
        """The volume of the sound outside the outside angle of the sound projection cone."""
        if self.is3d:
            volume = lib.LONG()
            _check(
                self._native_buffer3d.GetConeOutsideVolume(ctypes.byref(volume))
            )
            return volume.value
        else:
            return 0

    @cone_outside_volume.setter
    def cone_outside_volume(self, value):
        if self.is3d:
            _check(
                self._native_buffer3d.SetConeOutsideVolume(value, lib.DS3D_IMMEDIATE)
            )

    def create_listener(self):
        native_listener = lib.IDirectSound3DListener()
        self._native_buffer.QueryInterface(lib.IID_IDirectSound3DListener,
                                           ctypes.byref(native_listener))
        return DirectSoundListener(self, native_listener)

    def play(self):
        _check(
            self._native_buffer.Play(0, 0, lib.DSBPLAY_LOOPING)
        )

    def stop(self):
        _check(
            self._native_buffer.Stop()
        )

    class _WritePointer:
        def __init__(self):
            self.audio_ptr_1 = ctypes.c_void_p()
            self.audio_length_1 = lib.DWORD()
            self.audio_ptr_2 = ctypes.c_void_p()
            self.audio_length_2 = lib.DWORD()

    def lock(self, write_cursor, write_size):
        assert _debug('DirectSoundBuffer.lock({}, {})'.format(write_cursor, write_size))
        pointer = self._WritePointer()
        _check(
            self._native_buffer.Lock(write_cursor,
                                     write_size,
                                     ctypes.byref(pointer.audio_ptr_1),
                                     pointer.audio_length_1,
                                     ctypes.byref(pointer.audio_ptr_2),
                                     pointer.audio_length_2,
                                     0)
        )
        return pointer

    def unlock(self, pointer):
        _check(
            self._native_buffer.Unlock(pointer.audio_ptr_1,
                                       pointer.audio_length_1,
                                       pointer.audio_ptr_2,
                                       pointer.audio_length_2)
        )


class DirectSoundListener:
    def __init__(self, ds_buffer, native_listener):
        # We only keep a weakref to ds_buffer as it is owned by
        # interface.DirectSound or a DirectSoundAudioPlayer
        self.ds_buffer = weakref.proxy(ds_buffer)
        self._native_listener = native_listener

    def __del__(self):
        self.delete()

    def delete(self):
        if self._native_listener:
            self._native_listener.Release()
            self._native_listener = None

    @property
    def position(self):
        vector = lib.D3DVECTOR()
        _check(
            self._native_listener.GetPosition(ctypes.byref(vector))
        )
        return vector.x, vector.y, vector.z

    @position.setter
    def position(self, value):
        _check(
            self._native_listener.SetPosition(*(list(value) + [lib.DS3D_IMMEDIATE]))
        )

    @property
    def orientation(self):
        front = lib.D3DVECTOR()
        top = lib.D3DVECTOR()
        _check(
            self._native_listener.GetOrientation(ctypes.byref(front), ctypes.byref(top))
        )
        return front.x, front.y, front.z, top.x, top.y, top.z

    @orientation.setter
    def orientation(self, orientation):
        _check(
            self._native_listener.SetOrientation(*(list(orientation) + [lib.DS3D_IMMEDIATE]))
        )


