/*		DirectMusic
 *
 * Copyright 2002 TransGaming Technologies Inc. 
 *
 */

#include "config.h"
#include <assert.h>
#include <string.h>

#include "wine/debug.h"
#include "winbase.h"
#include "winerror.h"
#include "windef.h"
#include "wingdi.h"
#include "winuser.h"
#include "winnls.h"
#include "mmsystem.h"
#include "ole2.h"
#include "strmif.h"
#include "control.h"
#include "uuids.h"
#include "errors.h"
#include "dsound.h"
#include "dmusici.h"
#include "dmusicf.h"
#include "dmime_private.h"

WINE_DEFAULT_DEBUG_CHANNEL(dmusic);


static HRESULT WINAPI IDirectMusicAudioPathImpl_QueryInterface(
	IDirectMusicAudioPath* iface,REFIID riid,LPVOID *ppobj
) {
	ICOM_THIS(IDirectMusicAudioPathImpl,iface);

	TRACE("(%p,%s,%p)\n",This,debugstr_guid(riid),ppobj);
	if (IsEqualGUID(riid, &IID_IUnknown) ||
	    IsEqualGUID(riid, &IID_IDirectMusicAudioPath)) {
	    *ppobj = (LPVOID)This;
	    This->ref++;
	    return S_OK;
	}
	if (IsEqualGUID(riid, &IID_IDirectMusicObject)) {
	    *ppobj = (LPVOID)&This->lpOVtbl;
	    This->ref++;
	    return S_OK;
	}
	if (IsEqualGUID(riid, &IID_IPersist) ||
	    IsEqualGUID(riid, &IID_IPersistStream)) {
	    *ppobj = (LPVOID)&This->lpPSVtbl;
	    This->ref++;
	    return S_OK;
	}
	FIXME("(%p): no interface for %s\n", This, debugstr_guid(riid));
	return E_NOINTERFACE;
}

static ULONG WINAPI IDirectMusicAudioPathImpl_AddRef(IDirectMusicAudioPath* iface) {
	ICOM_THIS(IDirectMusicAudioPathImpl,iface);
	return ++(This->ref);
}

static ULONG WINAPI IDirectMusicAudioPathImpl_Release(IDirectMusicAudioPath* iface) {
	ICOM_THIS(IDirectMusicAudioPathImpl,iface);
	TRACE("(%p), ref was %ld\n",This,This->ref);
	if (!--(This->ref)) {
		unsigned u;
		IDirectMusicAudioPath_Activate(iface, FALSE);
		for (u=0; u<This->dsbuffers; u++)
			IDirectSoundBuffer8_Release(This->dsbuffer[u]);
		if (This->pPerf)
			IDirectMusicPerformance8_Release(This->pPerf);
		HeapFree(GetProcessHeap(),0,This->dswrite);
		HeapFree(GetProcessHeap(),0,This->dsplay);
		HeapFree(GetProcessHeap(),0,This->dsbuffer);
		HeapFree(GetProcessHeap(),0,This);
		return 0;
	}
	return This->ref;
}

static HRESULT WINAPI IDirectMusicAudioPathImpl_GetObjectInPath(
	IDirectMusicAudioPath* iface,
	DWORD dwPChannel,
	DWORD dwStage,
	DWORD dwBuffer,
	REFGUID guidObject,
	DWORD dwIndex,
	REFGUID iidInterface,
	void** ppObject)
{
	ICOM_THIS(IDirectMusicAudioPathImpl,iface);
	IUnknown *pObj = NULL;
	FIXME("(%p)->(%ld,%ld,%ld,%s,%ld,%s,%p): stub\n", This,
	      dwPChannel, dwStage, dwBuffer, debugstr_guid(guidObject),
	      dwIndex, debugstr_guid(iidInterface), ppObject);

	*ppObject = NULL;

	switch (dwStage) {
	case DMUS_PATH_BUFFER: /* DirectSound buffer */
		TRACE("grab DirectSound buffer\n");
		/* FIXME: check guidObject? */
		/* FIXME: dwPChannel? */
		if (dwBuffer < This->dsbuffers)
			pObj = (IUnknown*)This->dsbuffer[dwBuffer];
		break;
	default:
		FIXME("unhandled dwStage %ld\n", dwStage);
		break;
	}

	if (!pObj) return E_FAIL;
	return IUnknown_QueryInterface(pObj, iidInterface, ppObject);
}

static HRESULT WINAPI IDirectMusicAudioPathImpl_Activate(
	IDirectMusicAudioPath* iface,
	BOOL fActivate)
{
	ICOM_THIS(IDirectMusicAudioPathImpl,iface);
	DWORD c;
	TRACE("(%p)->(%d)\n", This, fActivate);
	if (fActivate) {
		if (This->active) return S_OK;
		This->active = TRUE;
		This->running = FALSE;
		for (c=0; c<This->dsbuffers; c++) {
			LPVOID ptr1, ptr2;
			DWORD size1, size2;
			/* clear buffer */
			IDirectSoundBuffer_Lock(This->dsbuffer[c], 0, 0, &ptr1, &size1, &ptr2, &size2, DSBLOCK_ENTIREBUFFER);
			if (size1) memset(ptr1, 0, size1);
			if (size2) memset(ptr2, 0, size2);
			IDirectSoundBuffer_Unlock(This->dsbuffer[c], ptr1, size1, ptr2, size2);
			This->dsplay[c] = 0;
			This->dswrite[c] = 0;
			/* now ready for playing */
		}
	} else {
		if (!This->active) return S_OK;
		This->active = FALSE;
		for (c=0; c<This->dsbuffers; c++)
			IDirectSoundBuffer_Stop(This->dsbuffer[c]);
	}
	return S_OK;
}

static ICOM_VTABLE(IDirectMusicAudioPath) dmapvt = 
{
	ICOM_MSVTABLE_COMPAT_DummyRTTIVALUE
	IDirectMusicAudioPathImpl_QueryInterface,
	IDirectMusicAudioPathImpl_AddRef,
	IDirectMusicAudioPathImpl_Release,
	IDirectMusicAudioPathImpl_GetObjectInPath,
	IDirectMusicAudioPathImpl_Activate,
	(void*)0xdeaf6007, /* SetVolume */
	(void*)0xdeaf6008  /* ConvertPChannel */
};


/***********************************************************************
 *		DirectMusicCreateStandardAudioPath
 */
HRESULT DirectMusicCreateStandardAudioPath(IDirectMusicPerformance8 *pPerf,
					   IDirectSound *pDS, DMUS_AUDIOPARAMS *pParams,
					   DWORD dwType, DWORD dwPChannelCount,
					   IDirectMusicAudioPath **ppObj)
{
	IDirectMusicAudioPathImpl** ippDM=(IDirectMusicAudioPathImpl**)ppObj;
	WAVEFORMATEX wfx;
	DSBUFFERDESC desc;
	HRESULT hr;

	TRACE("(%p,%ld,%ld,%p)\n", pPerf, dwType, dwPChannelCount, ppObj);

	if (ippDM == NULL)
		return E_INVALIDARG;

	/* Allocate memory */
	*ippDM = (IDirectMusicAudioPathImpl*)HeapAlloc(GetProcessHeap(),HEAP_ZERO_MEMORY,sizeof(IDirectMusicAudioPathImpl));
	if (*ippDM == NULL)
		return E_OUTOFMEMORY;

	ICOM_VTBL(*ippDM)	= &dmapvt;
	(*ippDM)->ref		= 1;
	IDirectMusicPerformance8_AddRef(pPerf);
	(*ippDM)->pPerf		= pPerf;

	memset(&wfx, 0, sizeof(wfx));
	wfx.wFormatTag = WAVE_FORMAT_PCM;
	wfx.nSamplesPerSec = pParams->dwSampleRate;
	wfx.wBitsPerSample = 16;
	wfx.nChannels = 1;
	wfx.nBlockAlign = wfx.nChannels * 2;
	wfx.nAvgBytesPerSec = wfx.nBlockAlign * wfx.nSamplesPerSec;
	memset(&desc, 0, sizeof(desc));
	desc.dwSize = sizeof(desc);
	desc.dwFlags = DSBCAPS_GLOBALFOCUS | DSBCAPS_CTRLVOLUME;
	desc.lpwfxFormat = &wfx;
	/* 4 seconds should do? */
	desc.dwBufferBytes = wfx.nAvgBytesPerSec * 4;

	/* create dsound buffers */
	switch (dwType) {
	case DMUS_APATH_DYNAMIC_STEREO: {
		desc.dwFlags |= DSBCAPS_CTRLFREQUENCY | DSBCAPS_CTRLPAN;
		wfx.nChannels = 2;
		wfx.nBlockAlign = wfx.nChannels * 2;
		wfx.nAvgBytesPerSec = wfx.nBlockAlign * wfx.nSamplesPerSec;
		(*ippDM)->dsbuffers = 1;
		(*ippDM)->dsbuffer = HeapAlloc(GetProcessHeap(),HEAP_ZERO_MEMORY,sizeof(IDirectSoundBuffer8*)*1);
		(*ippDM)->dsplay = HeapAlloc(GetProcessHeap(),HEAP_ZERO_MEMORY,sizeof(DWORD)*1);
		(*ippDM)->dswrite = HeapAlloc(GetProcessHeap(),HEAP_ZERO_MEMORY,sizeof(DWORD)*1);
		hr = IDirectSound_CreateSoundBuffer(pDS, &desc, &(*ippDM)->dsbuffer[0], NULL);
		if (FAILED(hr)) goto fail;
		break;
	}
	default:
		FIXME("audio path type %ld not supported\n", dwType);
		hr = E_FAIL;
		goto fail;
	}

	return DS_OK;
fail:
	IDirectMusicAudioPath_Release(*ppObj);
	return hr;
}

/* FIXME: the synth shouldn't be in dmime.dll, but in dmsynth.dll,
 * and should not be part of the audiopath implementation.
 * Should fix this for when we need something more than wave playback ability. */
HRESULT DirectMusicRunSynth(IDirectMusicAudioPathImpl *This,
			    DWORD c,
			    LPVOID out,
			    DWORD len,
			    LPWAVEFORMATEX wfx)
{
	DWORD v;
	REFERENCE_TIME rtStart, rtEnd, rtLen;
	DWORD rtRemStart, rtRemEnd;
	DWORD dwStart, dwEnd;

	if (c) return S_OK; /* only first buffer supported for now */

	rtRemStart = This->rtRem;
	rtLen = (((REFERENCE_TIME)len) * 10000000) + rtRemStart;
	rtRemEnd = rtLen % wfx->nAvgBytesPerSec;
	rtLen /= wfx->nAvgBytesPerSec;

	rtStart = This->rtTime;
	rtEnd = rtStart + rtLen;

	TRACE("mixing time is from %Ld to %Ld\n", rtStart, rtEnd);

	memset(out, 0, len);

	for (v=0; v<VOICES; v++) if (This->voice[v].pStream) {
		if (This->voice[v].rtEnd <= rtStart) {
			IStream_Release(This->voice[v].pStream);
			This->voice[v].pStream = NULL;
			continue;
		}
		if (This->voice[v].rtStart >= rtEnd) continue;
		/* calculate where to insert sound */
		if (This->voice[v].rtStart <= rtStart) dwStart = 0;
		else {
			dwStart = ((This->voice[v].rtStart - rtStart) * wfx->nAvgBytesPerSec + rtRemStart) / 10000000;
			dwStart -= dwStart % wfx->nBlockAlign;
		}
		/* calculate where to end sound */
		if (This->voice[v].rtEnd > rtEnd) dwEnd = len;
		else {
			dwEnd = ((This->voice[v].rtEnd - rtStart) * wfx->nAvgBytesPerSec + rtRemStart) / 10000000;
			dwEnd -= dwStart % wfx->nBlockAlign;
		}
		/* mix in wave data */
		IStream_Write(This->voice[v].pStream, wfx, sizeof(*wfx), NULL);
		IStream_Read(This->voice[v].pStream, ((LPBYTE)out) + dwStart, dwEnd - dwStart, NULL);
	}

	This->rtTime = rtEnd;
	This->rtRem = rtRemEnd;

	return S_OK;
}

HRESULT DirectMusicAdjustTime(IDirectMusicAudioPathImpl *This,
			      DWORD c,
			      REFERENCE_TIME rtBegin,
			      REFERENCE_TIME rtEnd,
			      DWORD len,
			      DWORD bps)
{
	static const REFERENCE_TIME margin = 1000000; /* 100ms */
	REFERENCE_TIME rtTime, rtLen;

	if (c) return S_OK; /* only first buffer supported for now */

	TRACE("mixahead len=%ld, measured times=%Ld-%Ld\n", len, rtBegin, rtEnd);

	rtLen = (((REFERENCE_TIME)len) * 10000000) / bps;
	rtTime = This->rtTime - rtLen;

	rtBegin -= margin;
	rtEnd += margin;

	if (rtTime < rtBegin) {
		FIXME("play position behind (%Ld < %Ld), resync needed\n", rtTime, rtBegin);
	}
	if (rtTime > rtEnd) {
		FIXME("play position ahead (%Ld > %Ld), resync needed\n", rtTime, rtEnd);
	}

	return S_OK;
}

HRESULT DirectMusicRunAudioPath(IDirectMusicAudioPath *iface, DWORD dwLatency)
{
	ICOM_THIS(IDirectMusicAudioPathImpl,iface);
	REFERENCE_TIME rtBegin, rtEnd;
	DSBCAPS caps;
	WAVEFORMATEX wfx;
	DWORD c, play, write, lwrite, nwrite;
	LPVOID ptr1, ptr2;
	DWORD size1, size2;

	TRACE("(%p,%ld)\n", This, dwLatency);
	if (!This) return S_FALSE;
	if (!This->active) return S_OK;
	for (c=0; c<This->dsbuffers; c++) {
		caps.dwSize = sizeof(caps);
		IDirectSoundBuffer_GetCaps(This->dsbuffer[c], &caps);
		IDirectSoundBuffer_GetFormat(This->dsbuffer[c], &wfx, sizeof(wfx), NULL);
		IDirectMusicPerformance8_GetTime(This->pPerf, &rtBegin, NULL);
		IDirectSoundBuffer_GetCurrentPosition(This->dsbuffer[c], &play, &write);
		IDirectMusicPerformance8_GetTime(This->pPerf, &rtEnd, NULL);
		This->dsplay[c] = play;
		if (!This->running) continue;

		nwrite = wfx.nAvgBytesPerSec * dwLatency / 1000;
		nwrite -= nwrite % wfx.nBlockAlign;
		nwrite += This->dsplay[c];
		while (nwrite >= caps.dwBufferBytes) nwrite -= caps.dwBufferBytes;
		lwrite = This->dswrite[c];

		DirectMusicAdjustTime(This, c, rtBegin, rtEnd,
				      ((lwrite < play) ? caps.dwBufferBytes : 0) + lwrite - play,
				      wfx.nAvgBytesPerSec);

		if (lwrite == nwrite) continue;

		IDirectSoundBuffer_Lock(This->dsbuffer[c], lwrite,
					((nwrite < lwrite) ? caps.dwBufferBytes : 0) + nwrite - lwrite,
					&ptr1, &size1,
					&ptr2, &size2,
					0);
		if (size1) DirectMusicRunSynth(This, c, ptr1, size1, &wfx);
		if (size2) DirectMusicRunSynth(This, c, ptr2, size2, &wfx);
		IDirectSoundBuffer_Unlock(This->dsbuffer[c], ptr1, size1, ptr2, size2);
		This->dswrite[c] = nwrite;
	}
	return S_OK;
}

HRESULT DirectMusicPlayInAudioPath(IDirectMusicAudioPath *iface, DMUS_PMSG *pMsg, DWORD dwLatency)
{
	ICOM_THIS(IDirectMusicAudioPathImpl,iface);
	DMUS_WAVE_PMSG *pWaveMsg = (DMUS_WAVE_PMSG*)pMsg;
	DWORD c;

	TRACE("(%p,%p,%ld)\n", This, pMsg, dwLatency);
	if (!This) return S_FALSE;
	if (pMsg->dwType != DMUS_PMSGT_WAVE) return S_OK;

	/* find free voice */
	for (c=0; c<VOICES && This->voice[c].pStream; c++);
	if (c >= VOICES) {
		ERR("can't play wave, all voices occupied\n");
		return S_OK;
	}
	/* insert wave into voice */
	if (pWaveMsg->dwFlags & DMUS_PMSGF_MUSICTIME) {
		This->voice[c].rtStart = 0;
		IDirectMusicPerformance8_MusicToReferenceTime(This->pPerf,
							      pWaveMsg->mtTime,
							      &This->voice[c].rtStart);
		This->voice[c].rtEnd = pWaveMsg->rtTime;
		IDirectMusicPerformance8_MusicToReferenceTime(This->pPerf,
							      pWaveMsg->mtTime + pWaveMsg->rtDuration,
							      &This->voice[c].rtEnd);
	} else {
		This->voice[c].rtStart = pWaveMsg->rtTime;
		This->voice[c].rtEnd = pWaveMsg->rtTime + pWaveMsg->rtDuration;
	}
	TRACE("queued %p in voice %ld: start=%Ld, end=%Ld\n",
	      pWaveMsg->punkUser, c, This->voice[c].rtStart, This->voice[c].rtEnd);
	/* grab custom stream interface from waveform (this is probably not
	 * what MS does, but their ways are undocumented, so shame on them) */
	IUnknown_QueryInterface(pWaveMsg->punkUser, &CLSID_DirectSoundWave, (void**)&This->voice[c].pStream);

	/* start playback if necessary */
	if (!This->running) {
		/* it's hard to synchronize playback precisely, this'll do for now */
		IDirectMusicPerformance8_GetTime(This->pPerf, &This->rtTime, NULL);
		This->rtRem = 0;
		This->running = TRUE;
		DirectMusicRunAudioPath(iface, dwLatency);
		for (c=0; c<This->dsbuffers; c++) {
			IDirectSoundBuffer_Play(This->dsbuffer[c], 0, 0, DSBPLAY_LOOPING);
		}
		TRACE("started playback at %Ld\n", This->rtTime);
	}
	return S_OK;
}

HRESULT DirectMusicStopAllInAudioPath(IDirectMusicAudioPath *iface)
{
	ICOM_THIS(IDirectMusicAudioPathImpl,iface);
	DWORD c;

	TRACE("(%p)\n", This);
	if (!This) return S_FALSE;

	/* stop all voices */
	for (c=0; c<VOICES; c++) if (This->voice[c].pStream) {
		IStream_Release(This->voice[c].pStream);
		This->voice[c].pStream = NULL;
	}
	return S_OK;
}
