/*
 * UAE - The Un*x Amiga Emulator
 *
 * Win32 interface
 *
 * Copyright 1997 Mathias Ortmann
 */
#include <windows.h>
#include <stdlib.h>
#include <stdarg.h>
#ifdef _MSC_VER
#include <mmsystem.h>
#include <ddraw.h>
#include <dsound.h>
#include <commctrl.h>
#include <commdlg.h>
#else
#include "winstuff.h"
#endif
#include <stdio.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <io.h>

#include "sysconfig.h"
#include "sysdeps.h"

#include <ctype.h>
#include <assert.h>
#include "config.h"
#include "options.h"
#include "threaddep/penguin.h"
#include "uae.h"
#include "gensound.h"
#include "sounddep/sound.h"
#include "events.h"
#include "memory.h"
#include "custom.h"
#include "readcpu.h"
#include "newcpu.h"
#include "xwin.h"
#include "osdep/win32.h"
#include "gui.h"

char *sndptr, *sndptrmax, soundneutral;

static MMRESULT uPeriodicTimerID = 0;

static WAVEFORMATEX wavfmt;

static HWAVEOUT hWaveOut = NULL, hSoundCompleteEvent = NULL;

static HGLOBAL hData;
LPSTR lpData;

static HGLOBAL hWaveHdr[ 2 ];
static LPWAVEHDR lpWaveHdr[ 2 ];
static BOOL sound_stopped = TRUE;
static frame_time_t basevsynctime = 0;
signed long bufsamples;
unsigned int samplecount;
DWORD soundbufsize;

int timer_available = 0;

static HANDLE hVBlankEvent;

MMRESULT idVBlankTimer;

typedef enum
{
    waveout_looping,
    waveout_dblbuff,
    dxsound_looping,
    dxsound_dblbuff
} win32_sound_style_e;

win32_sound_style_e win32_sound_style = waveout_looping;

static int num_sound_buffers = 1;
static int which_sound_buffer = 0;
static int sound_flushes  = 0;

MMTIME mmtime;

static HMODULE hDSound = NULL;

typedef HRESULT (WINAPI *DirectSoundCreateFuncPtr)(GUID FAR *, LPDIRECTSOUND FAR *, IUnknown FAR *);
static DirectSoundCreateFuncPtr pDirectSoundCreate = NULL;

extern HWND hAmigaWnd;

LPDIRECTSOUND lpDS = NULL;
LPDIRECTSOUNDBUFFER lpDSBprimary = NULL;
LPDIRECTSOUNDBUFFER lpDSB[ 2 ] = { NULL, NULL };

char *DSError( HRESULT error )
{
    switch( error )
    {
    case DSERR_ALLOCATED:
        return "Allocated";

    case DSERR_UNINITIALIZED:
        return "Uninitialized";

    case DSERR_BADFORMAT:
        return "Bad Format";

    case DSERR_INVALIDCALL:
        return "Invalid Call";

    case DSERR_INVALIDPARAM:
        return "Invalid Parameter";

    case DSERR_OUTOFMEMORY:
        return "Out of Memory";

    case DSERR_PRIOLEVELNEEDED:
        return "Priority Level Needed";

    case DSERR_UNSUPPORTED:
        return "Unsupported";

    default:
        return "Unknown";
    }
}

int setup_sound (void)
{
    sound_available = 1;
    return 1;
}

void close_sound (void)
{
    int i;

    if( win32_sound_style >= dxsound_looping )
    {
        for( i = 0; i < num_sound_buffers; i++ )
        {
            if( lpDSB[i] )
            {
                IDirectSoundBuffer_Release( lpDSB[i] );
                lpDSB[i] = NULL;
            }
        }
        if( lpDSBprimary )
        {
            IDirectSoundBuffer_Release( lpDSBprimary );
            lpDSBprimary = NULL;
        }
        if( lpDS )
        {
            IDirectSound_Release( lpDS );
            lpDS = NULL;
        }
        if( hDSound )
            FreeLibrary( hDSound );
    }
    else
    {
        if( hWaveOut )
        {
	        waveOutReset( hWaveOut );
            for( i = 0; i < num_sound_buffers; i++ )
            {
    	        waveOutUnprepareHeader( hWaveOut, lpWaveHdr[i], sizeof( WAVEHDR ) );
                GlobalUnlock( hWaveHdr[i] );
	            GlobalFree( hWaveHdr[i] );
            }
	        waveOutClose( hWaveOut );
            GlobalUnlock( hData );
            GlobalFree( hData );
	        hWaveOut = NULL;
        }
        if( hSoundCompleteEvent )
        {
            CloseHandle( hSoundCompleteEvent );
            hSoundCompleteEvent = NULL;
        }
    }
}

static __inline__ void dxsound_play( void )
{
    HRESULT hr;
    LPVOID lpvPtr1, lpvPtr2;
    DWORD dwBytes1, dwBytes2, status = 0;

    if( num_sound_buffers == 1 )
    {
         /* Get the big looping IDirectSoundBuffer_Play() rolling here, but only once at startup */
        if( sound_flushes == 1 )
        {
            hr = IDirectSoundBuffer_Play( lpDSB[0], 0, 0, DSBPLAY_LOOPING );
        }

        /* Lock the DirectSoundBuffer here, for soundbufsize bytes from the write-cursor position onwards */
        hr = IDirectSoundBuffer_Lock( lpDSB[0], 0, soundbufsize, &lpvPtr1, &dwBytes1, &lpvPtr2, &dwBytes2, DSBLOCK_FROMWRITECURSOR );
        if( hr == DSERR_BUFFERLOST )
        {
            IDirectSoundBuffer_Restore( lpDSB[0] );
            hr = IDirectSoundBuffer_Lock( lpDSB[0], 0, soundbufsize, &lpvPtr1, &dwBytes1, &lpvPtr2, &dwBytes2, DSBLOCK_FROMWRITECURSOR );
        }
        if (hr == DS_OK) 
        {
            /* Copy our audio-data in, and unlock the buffer - it will be played automatically */
            memcpy (lpvPtr1, lpData, dwBytes1);
            if (lpvPtr2 != NULL) {
                memcpy (lpvPtr2, lpData+dwBytes1, dwBytes2);
            }
            hr = IDirectSoundBuffer_Unlock( lpDSB[0], lpvPtr1, dwBytes1, lpvPtr2, dwBytes2 );
        }
        else
        {
            write_log( "SoundLock() failure: %s\n", DSError( hr ) );
        }
    }
    else /* Double-buffering, with wait-for-completion synchronization */
    {
        /* Lock the DirectSoundBuffer here, for soundbufsize bytes from position zero onwards */
        hr = IDirectSoundBuffer_Lock( lpDSB[ which_sound_buffer ], 0, soundbufsize, &lpvPtr1, &dwBytes1, &lpvPtr2, &dwBytes2, 0 );
        if( hr == DSERR_BUFFERLOST )
        {
            IDirectSoundBuffer_Restore( lpDSB[ which_sound_buffer ] );
            hr = IDirectSoundBuffer_Lock( lpDSB[ which_sound_buffer ], 0, soundbufsize, &lpvPtr1, &dwBytes1, &lpvPtr2, &dwBytes2, 0 );
        }
        if (hr == DS_OK) 
        {
            /* Copy our audio-data in, and unlock the buffer - it will be played automatically */
            memcpy (lpvPtr1, lpData + which_sound_buffer*soundbufsize, dwBytes1);
            if (lpvPtr2 != NULL) {
                memcpy (lpvPtr2, lpData + (which_sound_buffer*soundbufsize) + dwBytes1, dwBytes2);
            }
            hr = IDirectSoundBuffer_Unlock( lpDSB[ which_sound_buffer ], lpvPtr1, dwBytes1, lpvPtr2, dwBytes2 );
        }
        else
        {
            write_log( "SoundLock() failure: %s\n", DSError( hr ) );
        }
        hr = IDirectSoundBuffer_SetCurrentPosition( lpDSB[ which_sound_buffer ], 0 );

        /* Check the status of the other playing buffer */
        if( sound_flushes > 1 )
        {
            hr = IDirectSoundBuffer_GetStatus( lpDSB[ !which_sound_buffer ], &status );
            if( hr == DS_OK )
            {
                if( status & DSBSTATUS_PLAYING )
                {
                    while( ( IDirectSoundBuffer_GetStatus( lpDSB[ !which_sound_buffer ], &status ) == DS_OK ) &&
                           ( status & DSBSTATUS_PLAYING ) )
                    {
                        ;
                    }
                }
            }
        }

        hr = IDirectSoundBuffer_Play( lpDSB[ which_sound_buffer ], 0, 0, 0 );
        if (hr != DS_OK) 
        {
            write_log( "SoundBuffer_Play() failure: %s\n", DSError( hr ) );
        }
    }
}

static __inline__ void waveout_play( void )
{
    if( num_sound_buffers == 1 )
    {
        /* Get the big looping waveOutWrite() rolling here, but only once at startup */
        if( sound_flushes == 1 )
        {
            lpWaveHdr[0]->lpData = lpData;
            lpWaveHdr[0]->dwBufferLength = soundbufsize;
            lpWaveHdr[0]->dwFlags = WHDR_BEGINLOOP | WHDR_ENDLOOP;
            lpWaveHdr[0]->dwLoops = 0x7FFFFFFFL;
            waveOutPrepareHeader( hWaveOut, lpWaveHdr[0], sizeof( WAVEHDR ) );
            if( waveOutWrite( hWaveOut, lpWaveHdr[0], sizeof( WAVEHDR ) ) )
            { 
                waveOutUnprepareHeader( hWaveOut, lpWaveHdr[0], sizeof( WAVEHDR ) );
                write_log( "Failed to write to sound card\n" );
            }
        }
    }
    else /* Double-buffering, with wait-for-completion synchronization */
    {
        lpWaveHdr[which_sound_buffer]->lpData = lpData + which_sound_buffer*soundbufsize;
        lpWaveHdr[which_sound_buffer]->dwBufferLength = soundbufsize;
        lpWaveHdr[which_sound_buffer]->dwFlags = 0;
        lpWaveHdr[which_sound_buffer]->dwLoops = 0;
        waveOutPrepareHeader( hWaveOut, lpWaveHdr[which_sound_buffer], sizeof(WAVEHDR) ); 
        
        /* Check the status of the other playing buffer */
        if( sound_flushes > 1 )
        {
            if( !( lpWaveHdr[!which_sound_buffer]->dwFlags & WHDR_DONE ) )
            {
                WaitForSingleObject( hSoundCompleteEvent, INFINITE );
            }
        }
        ResetEvent( hSoundCompleteEvent );
        if( waveOutWrite( hWaveOut, lpWaveHdr[which_sound_buffer], sizeof(WAVEHDR) ) )
        { 
            waveOutUnprepareHeader( hWaveOut, lpWaveHdr[which_sound_buffer], sizeof(WAVEHDR) );
            write_log( "Failed to write to sound card\n" );
        } 
    }
}

static __inline__ void startsound( void )
{
    if( sound_stopped == TRUE )
    {
        sound_stopped = FALSE;
        if( win32_sound_style >= dxsound_looping )
        {
            dxsound_play();
        }
        else
        {
            waveout_play();
        }
    }
}

static __inline__ void stopsound( void )
{
    int i;
    HRESULT hr;

    if( sound_stopped == FALSE )
    {
        sndptr = lpData + which_sound_buffer*soundbufsize;
        samplecount = 0;

        if( win32_sound_style >= dxsound_looping )
        {
            for( i = 0; i < num_sound_buffers; i++ )
            {
                hr = IDirectSoundBuffer_Stop( lpDSB[i] );
                if( hr != DS_OK )
                {
                    write_log( "SoundStop() failure: %s\n", DSError( hr ) );
                }
                else
                    write_log( "Sound Stopped...\n" );
            }
        }
        else
        {
            memset( lpData, soundneutral, soundbufsize*num_sound_buffers );
            waveOutReset( hWaveOut );
            for( i = 0; i < num_sound_buffers; i++ )
                waveOutUnprepareHeader( hWaveOut, lpWaveHdr[i], sizeof( WAVEHDR ) );
        }
        sound_stopped = TRUE;
    }
}

/* Use this to pause Win32 sound output */
void pause_sound( void )
{
    if( currprefs.produce_sound > 1 )
    {
        stopsound();
    }
}

/* Use this to resume Win32 sound output */
void resume_sound( void )
{
    if( ( currprefs.produce_sound > 1 ) && sound_stopped )
    {
        sound_flushes = 1; /* Pretend we're starting from scratch */
        startsound();
    }
}
    
void flush_sound_buffer( void )
{
    sound_flushes++;

    startsound();

    if( num_sound_buffers == 2 )
    {
        if( which_sound_buffer == 0 )
        {
            which_sound_buffer = 1;
            sndptr = lpData + soundbufsize*(num_sound_buffers-1);
        }
        else
        {
            which_sound_buffer = 0;
            sndptr = lpData;
        }
        sndptrmax = sndptr + soundbufsize;
    }
}

static HWND dsound_tmpw;
static int init_sound_win32(void);

int dsound_newwindow (HWND w)
{
    HRESULT hr;

    if( win32_sound_style < dxsound_looping )
        return 1;

#if 0
    if (lpDS == 0)
	return 1;

    if (w == 0)
    {
	    w = dsound_tmpw;
        write_log( "DirectSound using temp window.\n" );
    }
    else
        write_log( "DirectSound using window 0x%x.\n", w );

    hr = IDirectSound_SetCooperativeLevel (lpDS, w, DSSCL_PRIORITY );
    if (hr != DS_OK) {
	write_log( "SetCooperativeLevel() failure: %s\n", DSError (hr));
	return 0;
    }
#else
    if (currprefs.produce_sound < 2)
        return 1;

    if (w == 0)
    {
        stopsound();
        close_sound();
        write_log ( "Sound shutting down...\n" );
    }
    else
    {
        init_sound_win32();
        write_log ( "Sound starting up...\n" );
        if( win32_sound_style >= dxsound_looping ) 
        {
            hr = IDirectSound_SetCooperativeLevel (lpDS, w, DSSCL_PRIORITY );
            if (hr != DS_OK) {
                write_log( "SetCooperativeLevel() failure: %s\n", DSError (hr));
                return 0;
            }
        }
    }
#endif
    return 1;
}

static int init_sound_win32 (void)
{
    HRESULT hr;
    DSBUFFERDESC sound_buffer;
    MMRESULT mmres;
    int i;

    if (lpDS || hWaveOut)
        return 0;

    if( currprefs.produce_sound < 1 ) 
    {
        write_log( "Sound output disabled.\n" );
        return 1;
    }

    if( win32_sound_style == waveout_dblbuff )
    {
        num_sound_buffers = 2;
        if( ( hSoundCompleteEvent = CreateEvent( NULL, TRUE, FALSE, NULL ) ) == NULL )
        {
            write_log( "Sound handling couldn't create event.  Switching to waveOut-looping mode.\n" );
            win32_sound_style = waveout_looping;
            num_sound_buffers = 1;
        }
    }
    else if( win32_sound_style == dxsound_dblbuff )
    {
        num_sound_buffers = 2;
    }
    else
    {
        num_sound_buffers = 1;
    }

    wavfmt.wFormatTag = WAVE_FORMAT_PCM;
    wavfmt.nChannels = 1 + currprefs.stereo;
    wavfmt.nSamplesPerSec = currprefs.sound_freq;
    wavfmt.wBitsPerSample = currprefs.sound_bits;
    wavfmt.nBlockAlign = currprefs.sound_bits / 8 * wavfmt.nChannels;
    wavfmt.nAvgBytesPerSec = wavfmt.nBlockAlign * currprefs.sound_freq;

    soundneutral = currprefs.sound_bits == 8 ? 128 : 0;
    bufsamples = ( ( win32_sound_style >= dxsound_looping ) ? 4096:2048) * currprefs.sound_freq / 11025;
    soundbufsize = bufsamples * wavfmt.nBlockAlign;

    if( !( hData = (LPSTR)GlobalAlloc( GMEM_MOVEABLE | GMEM_SHARE | GMEM_ZEROINIT, num_sound_buffers*soundbufsize ) ) )
    {
	    write_log( "Failed to allocate sound buffer!\n" );
	    return 0;
    }
    if( !( lpData = GlobalLock(hData) ) )
    {
        write_log( "Failed to lock sound buffer!\n" );
        return 0;
    }

    if( win32_sound_style >= dxsound_looping )
    {
        hDSound = LoadLibrary( "DSOUND.DLL" );
        if (hDSound == 0) 
        {
            write_log ("You have to install DirectX on your system before you can use UAE.\n"
                       "Refer to the documentation for further details.\n");
            gui_message("You have to install DirectX on your system before you can use UAE.\n"
                        "Refer to the documentation for further details.\n");
            return 0;
        }
        pDirectSoundCreate = (DirectSoundCreateFuncPtr)GetProcAddress (hDSound, "DirectSoundCreate");

        basevsynctime = vsynctime;
        sndptrmax = lpData+soundbufsize;
        hr = pDirectSoundCreate( NULL, &lpDS, NULL );
        if (hr != DS_OK) 
        {
            write_log( "DirectSoundCreate() failure: %s\n", DSError (hr));
            return 0;
        }

        dsound_tmpw = CreateWindowEx( WS_EX_ACCEPTFILES,
                                      "PCsuxRox",
                                      "Argh",
                                      WS_CAPTION,
                                      CW_USEDEFAULT, CW_USEDEFAULT,
                                      10, 10,
                                      NULL,
                                      NULL,
                                      0,
                                      NULL);

        if (! dsound_newwindow (dsound_tmpw))
            return 0;

        memset (&sound_buffer, 0, sizeof( DSBUFFERDESC ));
        sound_buffer.dwSize = sizeof( DSBUFFERDESC );
        sound_buffer.dwFlags = DSBCAPS_PRIMARYBUFFER;
        sound_buffer.dwBufferBytes = 0;
        sound_buffer.lpwfxFormat = NULL;

        hr = IDirectSound_CreateSoundBuffer (lpDS, &sound_buffer, &lpDSBprimary, NULL);
        if( hr != DS_OK ) 
        {
            write_log( "CreateSoundBuffer() failure: %s\n", DSError( hr ) );
            return 0;
        }

        hr = IDirectSoundBuffer_SetFormat (lpDSBprimary, &wavfmt);
        if( hr != DS_OK ) 
        {
            write_log( "SetFormat() failure: %s\n", DSError (hr));
            return 0;
        }
        sound_buffer.dwBufferBytes = soundbufsize;
        sound_buffer.lpwfxFormat = &wavfmt;
        sound_buffer.dwFlags = DSBCAPS_CTRLDEFAULT | DSBCAPS_STATIC;
        for( i = 0; i < num_sound_buffers; i++ )
        {
            hr = IDirectSound_CreateSoundBuffer( lpDS, &sound_buffer, &lpDSB[i], NULL );
            if (hr != DS_OK) 
            {
                write_log ("CreateSoundBuffer() failure: %s\n", DSError (hr));
                return 0;
            }
	    
            hr = IDirectSoundBuffer_SetVolume (lpDSB[i], 0);
            if (hr != DS_OK) 
            {
                write_log( "SetVolume() 2 failure: %s\n", DSError (hr));
                return 0;
            }
        }
    }
    else
    {
        mmtime.wType = TIME_SAMPLES;

        for( i = 0; i < num_sound_buffers; i++ )
        {
            if( !( hWaveHdr[i] = GlobalAlloc( GMEM_MOVEABLE | GMEM_SHARE | GMEM_ZEROINIT, (DWORD)sizeof( WAVEHDR ) ) ) )
            {
                write_log( "Failed to allocate wave header!\n" );
                return 0;
            }
            if( !( lpWaveHdr[i] = GlobalLock( hWaveHdr[i] ) ) )
            {
                write_log( "Failed to lock wave header!\n" );
                return 0;
            }
        }
        if( mmres = waveOutOpen( &hWaveOut, WAVE_MAPPER, &wavfmt, (LONG)hSoundCompleteEvent, 0, (hSoundCompleteEvent ? CALLBACK_EVENT:CALLBACK_NULL) | WAVE_ALLOWSYNC ) )
        {
            write_log( "Sound device failed to open with error code %d.\n", mmres );
            gui_message ("Could not open sound-device");
            return 0;
        }
    }

    basevsynctime = vsynctime;
    sndptrmax = lpData + soundbufsize;
    sndptr = lpData;
    samplecount = 0;
    memset( lpData, soundneutral, soundbufsize*num_sound_buffers );

    return currprefs.sound_freq;
}

void timer_error( void )
{
    write_log( "High-resolution timer unavailable.  System-friendly timing disabled\n" );
    timer_available = 0;
}

void CALLBACK OneShotTimer( UINT wTimerID, UINT msg, DWORD dwUsers, DWORD dw1, DWORD dw2 )
{
    SetEvent( hVBlankEvent );
}

void expire_timer( void )
{
    if( timer_available )
    {
        timeKillEvent( idVBlankTimer );
        idVBlankTimer = 0;
        SetEvent( hVBlankEvent );
    }
}

void adjust_sound_timing (void)
{
    static DWORD last;
    signed long diff;
    if( ( currprefs.m68k_speed == 0 ) || regs.stopped )
    {
        if( idVBlankTimer != 0 )
        {
            WaitForSingleObject( hVBlankEvent, INFINITE );
        }
        if( currprefs.produce_sound )
            while( (long int)( read_processor_time() - vsyncmintime ) < 0 );
        if( timer_available )
        {
            idVBlankTimer = timeSetEvent( (1000/vblank_hz) - (currprefs.produce_sound ? 2:0), 1, OneShotTimer, 0, TIME_ONESHOT );
        }
    }

    if( currprefs.produce_sound < 2 || win32_sound_style != waveout_looping )
        return;

    waveOutGetPosition (hWaveOut, &mmtime, sizeof (mmtime));
    if (!mmtime.u.sample)
	mmtime.u.sample++;

    if (last == mmtime.u.sample)
	return;
    last = mmtime.u.sample;

    samplecount >>= currprefs.stereo;

    diff = samplecount - last;

    if (diff < -bufsamples || diff > bufsamples) {
	samplecount = last;
	sndptr = lpData + (samplecount % bufsamples) * wavfmt.nBlockAlign;
    }
    if (diff < currprefs.win32_sound_tweak)
    {
	    vsynctime = basevsynctime * 15 / 16;
    }
    else
    {
	    vsynctime = basevsynctime * 17 / 16;
    }
    samplecount <<= currprefs.stereo;

    return;
}

void timer_init( void )
{
    // check result code
    hVBlankEvent = CreateEvent(NULL,FALSE,FALSE,NULL);

    // We need slightly-less-than-20-ms timing
    if( !timeBeginPeriod(1) ||
        !timeBeginPeriod(2) ||
        !timeBeginPeriod(3) ||
        !timeBeginPeriod(6) ||
        !timeBeginPeriod(9) )
        timer_available = 1;
    else
        timer_error();
}

void timer_cleanup( void )
{
    expire_timer();
    CloseHandle( hVBlankEvent );
}

static int rate;

void update_sound (void)
{
    if( rate >= 11050 )
    {
#if 1
        sample_evtime = (long) maxhpos * maxvpos * vblank_hz / rate;
#endif
    }
    basevsynctime = vsynctime;
}     

int init_sound (void)
{
    if ((rate = init_sound_win32 ()) < 2)
	return rate;

    if (currprefs.sound_bits == 16) {
	init_sound_table16 ();
	sample_handler = currprefs.stereo ? sample16s_handler : sample16_handler;
    } else {
	init_sound_table8 ();
	sample_handler = currprefs.stereo ? sample8s_handler : sample8_handler;
    }

    write_log( "%s driver found and configured for %d bits at %d Hz %s\n",
        ( win32_sound_style >= dxsound_looping ) ? "DirectSound" : "WaveOut", currprefs.sound_bits, rate,
        currprefs.stereo ? "stereo" : "" );

    return 1;
}
