/*
 * This file is part of sidplayfp, a console SID player.
 *
 * Copyright 2013 Leandro Nini
 * Copyright 2000-2002 Simon White
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */

#include "audiodrv.h"

#ifdef   HAVE_DIRECTX

#include <stdio.h>

#define SAFE_RELEASE(p) { if(p) { (p)->Release(); (p)=NULL; } }

Audio_DirectX::Audio_DirectX() :
    AudioBase("DIRECTX"),
    lpds(0),
    lpDsb(0),
    lpdsNotify(0),
    isOpen(false) {}

Audio_DirectX::~Audio_DirectX()
{
    close();
}

// Need this to setup DirectX
HWND Audio_DirectX::GetConsoleHwnd ()
{   // Taken from Microsoft Knowledge Base
    // Article ID: Q124103
    #define MY_bufSize 1024 // buffer size for console window totles

    TCHAR  pszNewWindowTitle[MY_bufSize]; // contains fabricated WindowTitle
    TCHAR  pszOldWindowTitle[MY_bufSize]; // contains original WindowTitle

    // fetch curent window title
    GetConsoleTitle (pszOldWindowTitle, MY_bufSize);

    // format a "unique" NewWindowTitle
    wsprintf (pszNewWindowTitle, TEXT("%d/%d"), GetTickCount (),
        GetCurrentProcessId ());

    // change the window title
    SetConsoleTitle (pszNewWindowTitle);

    // ensure window title has been updated
    Sleep (40);

    // look for NewWindowTitle
    HWND hwndFound = FindWindow (NULL, pszNewWindowTitle);

    // restore original window title
    SetConsoleTitle (pszOldWindowTitle);
    return hwndFound;
}

bool Audio_DirectX::open (AudioConfig &cfg)
{
    // Assume we have a console.  Use other other
    // if we have a non console Window
    HWND hwnd = GetConsoleHwnd ();
    return open (cfg, hwnd);
}

bool Audio_DirectX::open (AudioConfig &cfg, HWND hwnd)
{
    LPDIRECTSOUNDBUFFER lpDsbPrimary = 0;

    try
    {
        if (isOpen)
        {
            throw error("Audio device already open.");
        }

        lpvData = 0;
        isOpen  = true;

        for (int i = 0; i < AUDIO_DIRECTX_BUFFERS; i++)
            rghEvent[i] = CreateEvent(NULL, FALSE, FALSE, NULL);

        if (FAILED (DirectSoundCreate (NULL, &lpds, NULL)))
        {
            throw error("Could not open audio device.");
        }
        if (FAILED (lpds->SetCooperativeLevel (hwnd, DSSCL_PRIORITY)))
        {
            throw error("Could not set cooperative level.");
        }

        // Primary Buffer Setup
        DSBUFFERDESC dsbdesc;
        memset (&dsbdesc, 0, sizeof(DSBUFFERDESC));
        dsbdesc.dwSize        = sizeof(DSBUFFERDESC);
        dsbdesc.dwFlags       = DSBCAPS_PRIMARYBUFFER;
        dsbdesc.dwBufferBytes = 0;
        dsbdesc.lpwfxFormat   = NULL;

        // Format
        WAVEFORMATEX wfm;
        memset (&wfm, 0, sizeof(WAVEFORMATEX));
        wfm.wFormatTag      = WAVE_FORMAT_PCM;
        wfm.nChannels       = cfg.channels;
        wfm.nSamplesPerSec  = cfg.frequency;
        wfm.wBitsPerSample  = 16;
        wfm.nBlockAlign     = wfm.wBitsPerSample / 8 * wfm.nChannels;
        wfm.nAvgBytesPerSec = wfm.nSamplesPerSec * wfm.nBlockAlign;

        if (FAILED (lpds->CreateSoundBuffer(&dsbdesc, &lpDsbPrimary, NULL)))
        {
            throw error("Unable to create sound buffer.");
        }
        if (FAILED (lpDsbPrimary->SetFormat(&wfm)))
        {
            throw error("Unable to setup required sampling format.");
        }
        lpDsbPrimary->Release ();

        bufSize = cfg.bufSize * wfm.nBlockAlign;

        // Allocate secondary buffers
        memset (&dsbdesc, 0, sizeof(DSBUFFERDESC));
        dsbdesc.dwSize  = sizeof(DSBUFFERDESC);
        dsbdesc.dwFlags = DSBCAPS_CTRLPOSITIONNOTIFY |
            DSBCAPS_GETCURRENTPOSITION2 | DSBCAPS_GLOBALFOCUS | DSBCAPS_CTRLPAN;
        dsbdesc.dwBufferBytes = bufSize * AUDIO_DIRECTX_BUFFERS;
        dsbdesc.lpwfxFormat   = &wfm;

        if (FAILED (lpds->CreateSoundBuffer(&dsbdesc, &lpDsb, NULL)))
        {
            throw error("Could not create sound buffer.");
        }
        lpDsb->Stop();

        // Apparently this is used for timing ------------------------
        DSBPOSITIONNOTIFY rgdscbpn[AUDIO_DIRECTX_BUFFERS];
        // Buffer Start Notification
        // Rev 2.0.4 (saw) - On starting to play a buffer
        for (int i = 0; i < AUDIO_DIRECTX_BUFFERS; i++)
        {   // Track one buffer ahead
            rgdscbpn[i].dwOffset     = bufSize * ((i + 1) % AUDIO_DIRECTX_BUFFERS);
            rgdscbpn[i].hEventNotify = rghEvent[i];
        }

        if (FAILED (lpDsb->QueryInterface (IID_IDirectSoundNotify, (VOID **) &lpdsNotify)))
        {
            throw error("Sound interface query failed.");
        }
        if (FAILED (lpdsNotify->SetNotificationPositions(AUDIO_DIRECTX_BUFFERS, rgdscbpn)))
        {
            throw error("Unable to set up sound notification positions.");
        }
        // -----------------------------------------------------------

        lpDsb->Stop ();
        DWORD dwBytes;
        if (FAILED (lpDsb->Lock (0, bufSize, &lpvData, &dwBytes, NULL, NULL, 0)))
        {
            throw error("Unable to lock sound buffer.");
        }

        // Rev 1.7 (saw) - Set the play position back to the begining
        if (FAILED (lpDsb->SetCurrentPosition(0)))
        {
            throw error("Unable to set play position to start of buffer.");
        }

        // Force precision
        cfg.precision = 16;
        // Update the users settings
        m_frameSize   = wfm.nBlockAlign;
        _settings     = cfg;
        isPlaying     = false;
        _sampleBuffer = (short*)lpvData;
        return true;
    }
    catch(error const &e)
    {
        setError(e.message());

        SAFE_RELEASE (lpDsbPrimary);
        close ();
        return false;
    }
}

bool Audio_DirectX::write (uint_least32_t frames)
{
    if (!isOpen)
    {
        setError("Device not open.");
        return false;
    }

    // get the number of bytes
    DWORD size = frames * m_frameSize;

    // Unlock the current buffer for playing
    lpDsb->Unlock (lpvData, size, NULL, 0);

    // Check to see of the buffer is playing
    // and if not start it off
    if (!isPlaying)
    {
        isPlaying = true;
        if (FAILED (lpDsb->Play (0,0,DSBPLAY_LOOPING)))
        {
            setError("Unable to start playback.");
            return false;
        }
    }

    // Check the incoming event to make sure it's one of our event messages and
    // not something else
    DWORD dwEvt;
    do
    {
        dwEvt  = MsgWaitForMultipleObjects (AUDIO_DIRECTX_BUFFERS, rghEvent, FALSE, INFINITE, QS_ALLINPUT);
        dwEvt -= WAIT_OBJECT_0;
    } while (dwEvt >= AUDIO_DIRECTX_BUFFERS);

//    printf ("Event - %lu\n", dwEvt);

    // Lock the next buffer for filling
    DWORD dwBytes;
    if (FAILED (lpDsb->Lock (size * dwEvt, size, &lpvData, &dwBytes, NULL, NULL, 0)))
    {
        setError("Unable to lock sound buffer.");
        return false;
    }
    _sampleBuffer = (short*)lpvData;
    return true;
}

void Audio_DirectX::reset (void)
{
    DWORD dwBytes;
    if (!isOpen)
         return;

    // Stop play and kill the current music.
    // Start new music data being added at the begining of
    // the first buffer
    lpDsb->Stop ();
    // Rev 1.7 (saw) - Prevents output going silent after reset
    isPlaying = false;
    lpDsb->Unlock (lpvData, bufSize, NULL, 0);

    // Rev 1.4 (saw) - Added as lock can fail.
    if (FAILED (lpDsb->Lock (0, bufSize, &lpvData, &dwBytes, NULL, NULL, 0)))
    {
        setError("Unable to lock sound buffer.");
        return;
    }
    _sampleBuffer = (short*)lpvData;
}

// Rev 1.8 (saw) - Alias fix
void Audio_DirectX::close (void)
{
    if (!isOpen)
        return;

    isOpen        = false;
    _sampleBuffer = NULL;

    if (lpDsb)
    {
        lpDsb->Stop();
        isPlaying = false;
        if (lpvData)
        {
            // Rev 1.4 (iv) - Unlock before we release buffer.
            lpDsb->Unlock (lpvData, bufSize, NULL, 0);
        }
    }

    SAFE_RELEASE (lpdsNotify);
    SAFE_RELEASE (lpDsb);
    SAFE_RELEASE (lpds);

    // Rev 1.3 (Ingve Vormestrand) - Changed "<=" to "<"
    // as closing invalid handle.
    for (int i=0;i < AUDIO_DIRECTX_BUFFERS; i++)
        CloseHandle (rghEvent[i]);
}

void Audio_DirectX::pause (void)
{
    lpDsb->Stop ();
    isPlaying = false;
}

#endif // HAVE_DIRECTX
