import unittest
import struct
import sys
import os
import pywintypes
import win32event, win32api
import os
from pywin32_testutil import TestSkipped
import win32com.directsound.directsound as ds
import pythoncom
# next two lines are for for debugging:
# import win32com
# import directsound as ds

WAV_FORMAT_PCM = 1
WAV_HEADER_SIZE = struct.calcsize('<4sl4s4slhhllhh4sl')

def wav_header_unpack(data):
    (riff, riffsize, wave, fmt, fmtsize, format, nchannels, samplespersecond, 
     datarate, blockalign, bitspersample, data, datalength) \
     = struct.unpack('<4sl4s4slhhllhh4sl', data)

    if riff != b'RIFF':
        raise ValueError('invalid wav header')

    if fmtsize != 16 or fmt != b'fmt ' or data != b'data':
        # fmt chuck is not first chunk, directly followed by data chuck
        # It is nowhere required that they are, it is just very common
        raise ValueError('cannot understand wav header')

    wfx = pywintypes.WAVEFORMATEX()
    wfx.wFormatTag = format
    wfx.nChannels = nchannels
    wfx.nSamplesPerSec = samplespersecond
    wfx.nAvgBytesPerSec = datarate
    wfx.nBlockAlign = blockalign
    wfx.wBitsPerSample = bitspersample

    return wfx, datalength

def wav_header_pack(wfx, datasize):
    return struct.pack('<4sl4s4slhhllhh4sl', b'RIFF', 36 + datasize,
                       b'WAVE', b'fmt ', 16,
                       wfx.wFormatTag, wfx.nChannels, wfx.nSamplesPerSec,
                       wfx.nAvgBytesPerSec, wfx.nBlockAlign,
                       wfx.wBitsPerSample, b'data', datasize);

class WAVEFORMATTest(unittest.TestCase):
    def test_1_Type(self):
        'WAVEFORMATEX type'
        w = pywintypes.WAVEFORMATEX()
        self.assertTrue(type(w) == pywintypes.WAVEFORMATEXType)

    def test_2_Attr(self):
        'WAVEFORMATEX attribute access'
        # A wav header for a soundfile from a CD should look like this...
        w = pywintypes.WAVEFORMATEX()
        w.wFormatTag = pywintypes.WAVE_FORMAT_PCM
        w.nChannels = 2
        w.nSamplesPerSec = 44100
        w.nAvgBytesPerSec = 176400
        w.nBlockAlign = 4
        w.wBitsPerSample = 16

        self.assertTrue(w.wFormatTag == 1)
        self.assertTrue(w.nChannels == 2)
        self.assertTrue(w.nSamplesPerSec == 44100)
        self.assertTrue(w.nAvgBytesPerSec == 176400)
        self.assertTrue(w.nBlockAlign == 4)
        self.assertTrue(w.wBitsPerSample == 16)

class DSCAPSTest(unittest.TestCase):
    def test_1_Type(self):
        'DSCAPS type'
        c = ds.DSCAPS()
        self.assertTrue(type(c) == ds.DSCAPSType)

    def test_2_Attr(self):
        'DSCAPS attribute access'
        c = ds.DSCAPS()
        c.dwFlags = 1
        c.dwMinSecondarySampleRate = 2
        c.dwMaxSecondarySampleRate = 3
        c.dwPrimaryBuffers = 4
        c.dwMaxHwMixingAllBuffers = 5
        c.dwMaxHwMixingStaticBuffers = 6
        c.dwMaxHwMixingStreamingBuffers = 7
        c.dwFreeHwMixingAllBuffers = 8
        c.dwFreeHwMixingStaticBuffers = 9
        c.dwFreeHwMixingStreamingBuffers = 10
        c.dwMaxHw3DAllBuffers = 11
        c.dwMaxHw3DStaticBuffers = 12
        c.dwMaxHw3DStreamingBuffers = 13
        c.dwFreeHw3DAllBuffers = 14
        c.dwFreeHw3DStaticBuffers = 15
        c.dwFreeHw3DStreamingBuffers = 16
        c.dwTotalHwMemBytes = 17
        c.dwFreeHwMemBytes = 18
        c.dwMaxContigFreeHwMemBytes = 19
        c.dwUnlockTransferRateHwBuffers = 20
        c.dwPlayCpuOverheadSwBuffers = 21

        self.assertTrue(c.dwFlags == 1)
        self.assertTrue(c.dwMinSecondarySampleRate == 2)
        self.assertTrue(c.dwMaxSecondarySampleRate == 3)
        self.assertTrue(c.dwPrimaryBuffers == 4)
        self.assertTrue(c.dwMaxHwMixingAllBuffers == 5)
        self.assertTrue(c.dwMaxHwMixingStaticBuffers == 6)
        self.assertTrue(c.dwMaxHwMixingStreamingBuffers == 7)
        self.assertTrue(c.dwFreeHwMixingAllBuffers == 8)
        self.assertTrue(c.dwFreeHwMixingStaticBuffers == 9)
        self.assertTrue(c.dwFreeHwMixingStreamingBuffers == 10)
        self.assertTrue(c.dwMaxHw3DAllBuffers == 11)
        self.assertTrue(c.dwMaxHw3DStaticBuffers == 12)
        self.assertTrue(c.dwMaxHw3DStreamingBuffers == 13)
        self.assertTrue(c.dwFreeHw3DAllBuffers == 14)
        self.assertTrue(c.dwFreeHw3DStaticBuffers == 15)
        self.assertTrue(c.dwFreeHw3DStreamingBuffers == 16)
        self.assertTrue(c.dwTotalHwMemBytes == 17)
        self.assertTrue(c.dwFreeHwMemBytes == 18)
        self.assertTrue(c.dwMaxContigFreeHwMemBytes == 19)
        self.assertTrue(c.dwUnlockTransferRateHwBuffers == 20)
        self.assertTrue(c.dwPlayCpuOverheadSwBuffers == 21)

class DSBCAPSTest(unittest.TestCase):
    def test_1_Type(self):
        'DSBCAPS type'
        c = ds.DSBCAPS()
        self.assertTrue(type(c) == ds.DSBCAPSType)

    def test_2_Attr(self):
        'DSBCAPS attribute access'
        c = ds.DSBCAPS()
        c.dwFlags = 1
        c.dwBufferBytes = 2
        c.dwUnlockTransferRate = 3
        c.dwPlayCpuOverhead = 4

        self.assertTrue(c.dwFlags == 1)
        self.assertTrue(c.dwBufferBytes == 2)
        self.assertTrue(c.dwUnlockTransferRate == 3)
        self.assertTrue(c.dwPlayCpuOverhead == 4)

class DSCCAPSTest(unittest.TestCase):
    def test_1_Type(self):
        'DSCCAPS type'
        c = ds.DSCCAPS()
        self.assertTrue(type(c) == ds.DSCCAPSType)

    def test_2_Attr(self):
        'DSCCAPS attribute access'
        c = ds.DSCCAPS()
        c.dwFlags = 1
        c.dwFormats = 2
        c.dwChannels = 4

        self.assertTrue(c.dwFlags == 1)
        self.assertTrue(c.dwFormats == 2)
        self.assertTrue(c.dwChannels == 4)

class DSCBCAPSTest(unittest.TestCase):
    def test_1_Type(self):
        'DSCBCAPS type'
        c = ds.DSCBCAPS()
        self.assertTrue(type(c) == ds.DSCBCAPSType)

    def test_2_Attr(self):
        'DSCBCAPS attribute access'
        c = ds.DSCBCAPS()
        c.dwFlags = 1
        c.dwBufferBytes = 2

        self.assertTrue(c.dwFlags == 1)
        self.assertTrue(c.dwBufferBytes == 2)

class DSBUFFERDESCTest(unittest.TestCase):
    def test_1_Type(self):
        'DSBUFFERDESC type'
        c = ds.DSBUFFERDESC()
        self.assertTrue(type(c) == ds.DSBUFFERDESCType)

    def test_2_Attr(self):
        'DSBUFFERDESC attribute access'
        c = ds.DSBUFFERDESC()
        c.dwFlags = 1
        c.dwBufferBytes = 2
        c.lpwfxFormat = pywintypes.WAVEFORMATEX()
        c.lpwfxFormat.wFormatTag = pywintypes.WAVE_FORMAT_PCM
        c.lpwfxFormat.nChannels = 2
        c.lpwfxFormat.nSamplesPerSec = 44100
        c.lpwfxFormat.nAvgBytesPerSec = 176400
        c.lpwfxFormat.nBlockAlign = 4
        c.lpwfxFormat.wBitsPerSample = 16

        self.assertTrue(c.dwFlags == 1)
        self.assertTrue(c.dwBufferBytes == 2)
        self.assertTrue(c.lpwfxFormat.wFormatTag == 1)
        self.assertTrue(c.lpwfxFormat.nChannels == 2)
        self.assertTrue(c.lpwfxFormat.nSamplesPerSec == 44100)
        self.assertTrue(c.lpwfxFormat.nAvgBytesPerSec == 176400)
        self.assertTrue(c.lpwfxFormat.nBlockAlign == 4)
        self.assertTrue(c.lpwfxFormat.wBitsPerSample == 16)

    def invalid_format(self, c):
        c.lpwfxFormat = 17

    def test_3_invalid_format(self):
        'DSBUFFERDESC invalid lpwfxFormat assignment'
        c = ds.DSBUFFERDESC()
        self.assertRaises(ValueError, self.invalid_format, c)

class DSCBUFFERDESCTest(unittest.TestCase):
    def test_1_Type(self):
        'DSCBUFFERDESC type'
        c = ds.DSCBUFFERDESC()
        self.assertTrue(type(c) == ds.DSCBUFFERDESCType)

    def test_2_Attr(self):
        'DSCBUFFERDESC attribute access'
        c = ds.DSCBUFFERDESC()
        c.dwFlags = 1
        c.dwBufferBytes = 2
        c.lpwfxFormat = pywintypes.WAVEFORMATEX()
        c.lpwfxFormat.wFormatTag = pywintypes.WAVE_FORMAT_PCM
        c.lpwfxFormat.nChannels = 2
        c.lpwfxFormat.nSamplesPerSec = 44100
        c.lpwfxFormat.nAvgBytesPerSec = 176400
        c.lpwfxFormat.nBlockAlign = 4
        c.lpwfxFormat.wBitsPerSample = 16

        self.assertTrue(c.dwFlags == 1)
        self.assertTrue(c.dwBufferBytes == 2)
        self.assertTrue(c.lpwfxFormat.wFormatTag == 1)
        self.assertTrue(c.lpwfxFormat.nChannels == 2)
        self.assertTrue(c.lpwfxFormat.nSamplesPerSec == 44100)
        self.assertTrue(c.lpwfxFormat.nAvgBytesPerSec == 176400)
        self.assertTrue(c.lpwfxFormat.nBlockAlign == 4)
        self.assertTrue(c.lpwfxFormat.wBitsPerSample == 16)

    def invalid_format(self, c):
        c.lpwfxFormat = 17

    def test_3_invalid_format(self):
        'DSCBUFFERDESC invalid lpwfxFormat assignment'
        c = ds.DSCBUFFERDESC()
        self.assertRaises(ValueError, self.invalid_format, c)

class DirectSoundTest(unittest.TestCase):
    # basic tests - mostly just exercise the functions
    def testEnumerate(self):
        '''DirectSoundEnumerate() sanity tests'''

        devices = ds.DirectSoundEnumerate()
        # this might fail on machines without a sound card
        self.assertTrue(len(devices))
        # if we have an entry, it must be a tuple of size 3
        self.assertTrue(len(devices[0]) == 3)

    def testCreate(self):
        '''DirectSoundCreate()'''
        try:
            d = ds.DirectSoundCreate(None, None)
        except pythoncom.com_error as exc:
            if exc.hresult != ds.DSERR_NODRIVER:
                raise
            raise TestSkipped(exc)

    def testPlay(self):
        '''Mesdames et Messieurs, la cour de Devin Dazzle'''
        # look for the test file in various places
        candidates = [
            os.path.dirname(__file__),
            os.path.dirname(sys.argv[0]),
            # relative to 'testall.py' in the win32com test suite.
            os.path.join(os.path.dirname(sys.argv[0]),
                         '../../win32comext/directsound/test'),
            '.',
        ]
        for candidate in candidates:
            fname=os.path.join(candidate, "01-Intro.wav")
            if os.path.isfile(fname):
                break
        else:
            raise TestSkipped("Can't find test .wav file to play")

        with open(fname, 'rb') as f:
            hdr = f.read(WAV_HEADER_SIZE)
            wfx, size = wav_header_unpack(hdr)

            d = ds.DirectSoundCreate(None, None)
            d.SetCooperativeLevel(None, ds.DSSCL_PRIORITY)

            sdesc = ds.DSBUFFERDESC()
            sdesc.dwFlags = ds.DSBCAPS_STICKYFOCUS | ds.DSBCAPS_CTRLPOSITIONNOTIFY
            sdesc.dwBufferBytes = size
            sdesc.lpwfxFormat = wfx

            buffer = d.CreateSoundBuffer(sdesc, None)

            event = win32event.CreateEvent(None, 0, 0, None)
            notify = buffer.QueryInterface(ds.IID_IDirectSoundNotify)

            notify.SetNotificationPositions((ds.DSBPN_OFFSETSTOP, event))

            buffer.Update(0, f.read(size))

            buffer.Play(0)

            win32event.WaitForSingleObject(event, -1)

class DirectSoundCaptureTest(unittest.TestCase):
    # basic tests - mostly just exercise the functions
    def testEnumerate(self):
        '''DirectSoundCaptureEnumerate() sanity tests'''

        devices = ds.DirectSoundCaptureEnumerate()
        # this might fail on machines without a sound card
        self.assertTrue(len(devices))
        # if we have an entry, it must be a tuple of size 3
        self.assertTrue(len(devices[0]) == 3)

    def testCreate(self):
        '''DirectSoundCreate()'''
        try:
            d = ds.DirectSoundCaptureCreate(None, None)
        except pythoncom.com_error as exc:
            if exc.hresult != ds.DSERR_NODRIVER:
                raise
            raise TestSkipped(exc)

    def testRecord(self):
        try:
            d = ds.DirectSoundCaptureCreate(None, None)
        except pythoncom.com_error as exc:
            if exc.hresult != ds.DSERR_NODRIVER:
                raise
            raise TestSkipped(exc)

        sdesc = ds.DSCBUFFERDESC()
        sdesc.dwBufferBytes = 352800 # 2 seconds
        sdesc.lpwfxFormat = pywintypes.WAVEFORMATEX()
        sdesc.lpwfxFormat.wFormatTag = pywintypes.WAVE_FORMAT_PCM
        sdesc.lpwfxFormat.nChannels = 2
        sdesc.lpwfxFormat.nSamplesPerSec = 44100
        sdesc.lpwfxFormat.nAvgBytesPerSec = 176400
        sdesc.lpwfxFormat.nBlockAlign = 4
        sdesc.lpwfxFormat.wBitsPerSample = 16

        buffer = d.CreateCaptureBuffer(sdesc)

        event = win32event.CreateEvent(None, 0, 0, None)
        notify = buffer.QueryInterface(ds.IID_IDirectSoundNotify)

        notify.SetNotificationPositions((ds.DSBPN_OFFSETSTOP, event))

        buffer.Start(0)

        win32event.WaitForSingleObject(event, -1)
        event.Close()

        data = buffer.Update(0, 352800)
        fname=os.path.join(win32api.GetTempPath(), 'test_directsound_record.wav')
        f = open(fname, 'wb')
        f.write(wav_header_pack(sdesc.lpwfxFormat, 352800))
        f.write(data)
        f.close()

if __name__ == '__main__':
    unittest.main()
