/*
 * sound.c - Win32 port specific code
 *
 * Copyright (C) 2000 Krzysztof Nikiel
 * Copyright (C) 2000-2003 Atari800 development team (see DOC/CREDITS)
 *
 * This file is part of the Atari800 emulator project which emulates
 * the Atari 400, 800, 800XL, 130XE, and 5200 8-bit computers.
 *
 * Atari800 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.
 *
 * Atari800 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 Atari800; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
*/

#include "config.h"

#ifdef SOUND

#include <windows.h>
#ifdef DIRECTX
# define DIRECTSOUND_VERSION 0x0500
# include <dsound.h>
#endif
#include <stdio.h>
#include <stdlib.h>
#include "sound.h"
#include "main.h"
#include "pokeysnd.h"
#include "atari.h"
#include "log.h"

#define MIXBUFSIZE	0x1000
#define WAVSHIFT	9
#define WAVSIZE		(1 << WAVSHIFT)
int buffers = 0;

enum {SOUND_NONE, SOUND_DX, SOUND_WAV};

static int usesound = TRUE;
static int issound = SOUND_NONE;
static int dsprate = 44100;
static int snddelay = 40;	/* delay in milliseconds */
static int snddelaywav = 100;
static int bit16 = FALSE;

static HANDLE event;
static HWAVEOUT wout;
static WAVEHDR *waves;

#ifdef DIRECTX
static int wavonly = FALSE;
static DWORD sbufsize = 0;
static int samples = 0; 	/* #samples to be in play buffer */
static UBYTE mixbuf[MIXBUFSIZE];
static DWORD bufpos = 0;

static LPDIRECTSOUND lpDS = NULL;
static LPDIRECTSOUNDBUFFER pDSB = NULL;

static int initsound_dx(void)
{
  DWORD i;
  int err;
  DSBUFFERDESC dsBD =
  {0};
  WAVEFORMATEX wfx;
  DSBCAPS bc;

  LPVOID pMem1, pMem2;
  DWORD dwSize1, dwSize2;

  if (issound != SOUND_NONE)
    return 0;

  if ((err = DirectSoundCreate(NULL, &lpDS, NULL)) < 0)
    return err;
  if ((err = IDirectSound_SetCooperativeLevel(lpDS, hWndMain,
					      DSSCL_EXCLUSIVE)) < 0)
    goto end;

  dsBD.dwSize = sizeof(dsBD);
  dsBD.dwFlags = DSBCAPS_GETCURRENTPOSITION2;
  dsBD.dwBufferBytes = 0x4000;
  dsBD.lpwfxFormat = &wfx;

  wfx.wFormatTag = WAVE_FORMAT_PCM;
#ifdef STEREO_SOUND
  wfx.nChannels = POKEYSND_stereo_enabled ? 2 : 1;
#else
  wfx.nChannels = 1;
#endif /* STEREO_SOUND */
  wfx.nSamplesPerSec = dsprate;
  wfx.nAvgBytesPerSec = dsprate * wfx.nChannels * (bit16 ? 2 : 1);
  wfx.nBlockAlign = wfx.nChannels * (bit16 ? 2 : 1);
  wfx.wBitsPerSample = bit16 ? 16 : 8;
  wfx.cbSize = 0;

  if ((err = IDirectSound_CreateSoundBuffer(lpDS, &dsBD, &pDSB, NULL)) < 0)
    goto end;

  bc.dwSize = sizeof(bc);
  IDirectSoundBuffer_GetCaps(pDSB, &bc);
  sbufsize = bc.dwBufferBytes;

  if ((err = IDirectSoundBuffer_Lock(pDSB, 0, sbufsize,
			       &pMem1, &dwSize1, &pMem2, &dwSize2,
			       DSBLOCK_FROMWRITECURSOR)) < 0)
    goto end;

  for (i = 0; i < dwSize1 >> 1; i++)
    *((short *) pMem1 + i) = 0;
  for (i = 0; i < dwSize2 >> 1; i++)
    *((short *) pMem2 + i) = 0;
  IDirectSoundBuffer_Unlock(pDSB, pMem1, dwSize1, pMem2, dwSize2);

  IDirectSoundBuffer_Play(pDSB, 0, 0, DSBPLAY_LOOPING);

  POKEYSND_Init(POKEYSND_FREQ_17_EXACT, (UWORD) dsprate, (UBYTE) wfx.nChannels, (bit16 ? POKEYSND_BIT16 : 0));

  samples = dsprate * snddelay / 1000;

  IDirectSoundBuffer_GetCurrentPosition(pDSB, 0, &bufpos);

  issound = SOUND_DX;
  return 0;

end:
  MessageBox(hWndMain, "DirectSound Init FAILED", myname, MB_OK);
  Sound_Exit();
  return err;
}

static void uninitsound_dx(void)
{
  if (issound != SOUND_DX)
    return;

  if (lpDS)
    {
      if (pDSB)
	{
	  IDirectSoundBuffer_Stop(pDSB);
	  IDirectSoundBuffer_Release(pDSB);
	  pDSB = NULL;
	}
      IDirectSound_Release(lpDS);
      lpDS = NULL;
    }
  issound = SOUND_NONE;
}

static void sound_update_dx(void)
{
  DWORD wc;
  int d1;
  LPVOID pMem1, pMem2;
  DWORD dwSize1, dwSize2;
  signed short *p1s;
  signed short *p2s;
  UBYTE *p1b;
  UBYTE *p2b;
  signed short *pbufs;
  UBYTE *pbufb;
  int s1, s2;
  int err;
  int i;
  int samplesize = bit16 ? 2 : 1;

#ifdef STEREO_SOUND
  if (POKEYSND_stereo_enabled) samplesize *= 2;
#endif

  if (issound != SOUND_DX)
    return;

  IDirectSoundBuffer_GetCurrentPosition(pDSB, 0, &wc);

  d1 = (wc - bufpos);
	if ((DWORD) abs(d1) > (sbufsize >> 1)) {
		if (d1 < 0)
			d1 += sbufsize;
		else
			d1 -= sbufsize;
	}
  if (d1 < (-samples * samplesize)) // there is more than necessary bytes filled?
    return;

  d1 = (samples * samplesize) + d1; // bytes to fill
  d1 = (d1 / samplesize) * samplesize; //round to a sample pair

  if (d1 > (sizeof(mixbuf) / sizeof(mixbuf[0])))
    {
      d1 = (sizeof(mixbuf) / sizeof(mixbuf[0]));
    }

  if ((err = IDirectSoundBuffer_Lock(pDSB,
				     bufpos,
				     d1,
				     &pMem1, &dwSize1,
				     &pMem2, &dwSize2,
				     0)) < 0)
    {
      if (err == DSERR_BUFFERLOST)
	Sound_Continue();
      return;
    }

  s1 = dwSize1;
  s2 = dwSize2;
  p1s = (signed short *)pMem1;
  p2s = (signed short *)pMem2;
  p1b = (UBYTE *)pMem1;
  p2b = (UBYTE *)pMem2;
  bufpos += (s1 + s2);
  if (bufpos >= sbufsize)
    bufpos -= sbufsize;

  if (bit16)
  {
    s1 /= 2;
    s2 /= 2;
  }
  i = s1 + s2;

  POKEYSND_Process(mixbuf, i);

  pbufs = (signed short *)mixbuf;
  pbufb = (UBYTE *)mixbuf;
  if (s1)
    {
      if (bit16)
      for (; s1; s1--)
	  *p1s++ = *pbufs++;
      else
	for (; s1; s1--)
	  *p1b++ = *pbufb++;
    }
  if (s2)
    {
      if (bit16)
      for (; s2; s2--)
	  *p2s++ = *pbufs++;
      else
	for (; s2; s2--)
	  *p2b++ = *pbufb++;
    }

  IDirectSoundBuffer_Unlock(pDSB, pMem1, dwSize1, pMem2, dwSize2);

  return;
}
#endif /* DIRECTX */

static void uninitsound_wav(void)
{
  int i;
  MMRESULT err;

  if (issound != SOUND_WAV)
    return;

l0:
  for (i = 0; i < buffers; i++)
    {
      if (!(waves[i].dwFlags & WHDR_DONE))
	{
	  WaitForSingleObject(event, 5000);
	  ResetEvent(event);
	  goto l0;
	}
    }

  waveOutReset (wout);

  for (i = 0; i < buffers; i++)
    {
      err = waveOutUnprepareHeader(wout, &waves[i], sizeof (waves[i]));
      if (err != MMSYSERR_NOERROR)
	{
	  fprintf(stderr, "warning: cannot unprepare wave header (%x)\n", err);
	}
      free(waves[i].lpData);
    }
  free(waves);

  waveOutClose(wout);
  CloseHandle(event);

  issound = SOUND_NONE;
}

static int initsound_wav(void)
{
  int i;
  WAVEFORMATEX wfx;
  MMRESULT err;

  event = CreateEvent(NULL, TRUE, FALSE, NULL);

  memset(&wfx, 0, sizeof(wfx));

  wfx.wFormatTag = WAVE_FORMAT_PCM;
#ifdef STEREO_SOUND
  wfx.nChannels = POKEYSND_stereo_enabled ? 2 : 1;
#else
  wfx.nChannels = 1;
#endif /* STEREO_SOUND */
  wfx.nSamplesPerSec = dsprate;
  wfx.nAvgBytesPerSec = dsprate * wfx.nChannels * (bit16 ? 2 : 1);
  wfx.nBlockAlign = wfx.nChannels * (bit16 ? 2 : 1);
  wfx.wBitsPerSample = bit16 ? 16 : 8;
  wfx.cbSize = 0;

  err = waveOutOpen(&wout, WAVE_MAPPER, &wfx, (int)event, 0, CALLBACK_EVENT);
  if (err == WAVERR_BADFORMAT)
    {
      Log_print("wave output parameters unsupported\n");
      exit(1);
    }
  if (err != MMSYSERR_NOERROR)
    {
      Log_print("cannot open wave output (%x)\n", err);
      exit(1);
    }

  buffers = ((wfx.nAvgBytesPerSec * snddelaywav / 1000) >> WAVSHIFT) + 1;
  waves = (WAVEHDR *)malloc(buffers * sizeof(*waves));
  for (i = 0; i < buffers; i++)
    {
      memset(&waves[i], 0, sizeof (waves[i]));
      if (!(waves[i].lpData = (LPSTR)malloc(WAVSIZE)))
	{
	  Log_print("could not get wave buffer memory\n");
	  exit(1);
	}
      waves[i].dwBufferLength = WAVSIZE;
      err = waveOutPrepareHeader(wout, &waves[i], sizeof(waves[i]));
      if (err != MMSYSERR_NOERROR)
	{
	  Log_print("cannot prepare wave header (%x)\n", err);
	  exit(1);
	}
      waves[i].dwFlags |= WHDR_DONE;
    }

  POKEYSND_Init(POKEYSND_FREQ_17_EXACT, (UWORD) dsprate, (UBYTE) wfx.nChannels, (bit16 ? POKEYSND_BIT16 : 0));

  issound = SOUND_WAV;
  return 0;
}

int Sound_Initialise(int *argc, char *argv[])
{
  int i, j;
  int help = FALSE;

  if (issound != SOUND_NONE)
    return TRUE;

  for (i = j = 1; i < *argc; i++)
    {
      int i_a = (i + 1 < *argc); /* is argument available? */
      int a_m = FALSE; /* error, argument missing! */

      if (strcmp(argv[i], "-sound") == 0)
	usesound = TRUE;
      else if (strcmp(argv[i], "-nosound") == 0)
	usesound = FALSE;
      else if (strcmp(argv[i], "-audio16") == 0)
	bit16 = TRUE;
      else if (strcmp(argv[i], "-dsprate") == 0)
      {
	if (i_a)
	  sscanf(argv[++i], "%d", &dsprate);
	else a_m = TRUE;
      }
      else if (strcmp(argv[i], "-snddelay") == 0)
      {
	if (i_a)
	{
	  sscanf(argv[++i], "%d", &snddelay);
	  snddelaywav = snddelay;
	}
	else a_m = TRUE;
      }
      else if (strcmp(argv[i], "-quality") == 0) {
	if (i_a) {
	  int quality;
	  sscanf(argv[++i], "%d", &quality);
	  if (quality > 1) {
	    POKEYSND_SetMzQuality(quality - 1);
	    POKEYSND_enable_new_pokey = 1;
	  }
	  else
	    POKEYSND_enable_new_pokey = 0;
	}
	else a_m = TRUE;
      }
#ifdef DIRECTX
      else if (strcmp(argv[i], "-wavonly") == 0)
	wavonly = TRUE;
#endif
      else
      {
	if (strcmp(argv[i], "-help") == 0)
	{
	  Log_print("\t-sound           Enable sound\n"
		 "\t-nosound         Disable sound\n"
#ifdef DIRECTX
		 "\t-wavonly         Disable direct sound\n"
#endif
		 "\t-dsprate <rate>  Set dsp rate\n"
		 "\t-snddelay <ms>   Set sound delay\n"
		 "\t-audio16         Use 16 bit mixing\n"
		 "\t-quality <level> Set sound quality"
		);
	  help = TRUE;
	}
	argv[j++] = argv[i];
      }

      if (a_m) {
	Log_print("Missing argument for '%s'", argv[i]);
	usesound = FALSE;
	return FALSE;
      }
    }
  *argc = j;

  if (help || !usesound) {
    usesound = FALSE;
    return TRUE;
  }

#ifdef DIRECTX
  if (!wavonly)
  {
    i = initsound_dx();
    if (!i)
      return TRUE;
  }
#endif
  initsound_wav();
  return TRUE;
}

void Sound_Reinit(void)
{
  if (usesound)
  {
  Sound_Exit();
#ifdef DIRECTX
  if (!wavonly)
  {
    int i = initsound_dx();
    if (!i)
      return;
  }
#endif
  initsound_wav();
  }
  return;
}

void Sound_Exit(void)
{
#ifdef DIRECTX
  if (issound == SOUND_DX)
    uninitsound_dx();
#endif
  if (issound == SOUND_WAV)
    uninitsound_wav();

  issound = SOUND_NONE;
}

static WAVEHDR *getwave(void)
{
  int i;

  for (i = 0; i < buffers; i++)
    {
      if (waves[i].dwFlags & WHDR_DONE)
	{
	  waves[i].dwFlags &= ~WHDR_DONE;
	  return &waves[i];
	}
    }

  return NULL;
}

void Sound_Update(void)
{
  MMRESULT err;
  WAVEHDR *wh;

  switch (issound)
  {
  case SOUND_WAV:
    while ((wh = getwave()))
    {
      POKEYSND_Process(wh->lpData, wh->dwBufferLength >> (bit16 ? 1 : 0));
      err = waveOutWrite(wout, wh, sizeof(*wh));
      if (err != MMSYSERR_NOERROR)
      {
	Log_print("cannot write wave output (%x)\n", err);
	return;
      }
    }
    break;
#ifdef DIRECTX
  case SOUND_DX:
    sound_update_dx();
#endif
  }
}

void Sound_Pause(void)
{
#ifdef DIRECTX
  if (issound != SOUND_DX)
    return;
  if (!pDSB)
    return;
  IDirectSoundBuffer_Stop(pDSB);
#endif
}

void Sound_Continue(void)
{
#ifdef DIRECTX
  if (issound != SOUND_DX)
    return;
  if (!pDSB)
    return;
  IDirectSoundBuffer_Restore(pDSB);
  IDirectSoundBuffer_Play(pDSB, 0, 0, DSBPLAY_LOOPING);
  IDirectSoundBuffer_GetCurrentPosition(pDSB, 0, &bufpos);
#endif
}

#endif	/* SOUND */
