/*         ______   ___    ___ 
 *        /\  _  \ /\_ \  /\_ \ 
 *        \ \ \L\ \\//\ \ \//\ \      __     __   _ __   ___ 
 *         \ \  __ \ \ \ \  \ \ \   /'__`\ /'_ `\/\`'__\/ __`\
 *          \ \ \/\ \ \_\ \_ \_\ \_/\  __//\ \L\ \ \ \//\ \L\ \
 *           \ \_\ \_\/\____\/\____\ \____\ \____ \ \_\\ \____/
 *            \/_/\/_/\/____/\/____/\/____/\/___L\ \/_/ \/___/
 *                                           /\____/
 *                                           \_/__/
 *
 *      DirectSound input driver.
 *
 *      By Nick Kochakian.
 *
 *      API compliance improvements, enhanced format detection
 *      and bugfixes by Javier Gonzalez.
 *
 *      See readme.txt for copyright information.
 */


#define DIRECTSOUND_VERSION 0x0500

#include "allegro.h"
#include "allegro/internal/aintern.h"
#include "allegro/platform/aintwin.h"

#ifndef SCAN_DEPEND
   #ifdef ALLEGRO_MINGW32
      #undef MAKEFOURCC
   #endif

   #include <mmsystem.h>
   #include <dsound.h>
   #include <math.h>

   #ifdef ALLEGRO_MSVC
      #include <mmreg.h>
   #endif
#endif

#ifndef ALLEGRO_WINDOWS
#error something is wrong with the makefile
#endif


/* sound driver globals */
static LPDIRECTSOUNDCAPTURE ds_capture = NULL;
static LPDIRECTSOUNDCAPTUREBUFFER ds_capture_buf = NULL;
static DSCBUFFERDESC dscBufDesc;
static WAVEFORMATEX dsc_buf_wfx;
static unsigned long int ds_capture_buffer_size;
static unsigned long int last_capture_pos, input_wave_bytes_read;
static unsigned char *input_wave_data = NULL;



/* ds_err:
 *  Returns a DirectSound error string.
 */
#ifdef DEBUGMODE
static char *ds_err(long err)
{
   static char err_str[32];

   switch (err) {

      case DS_OK:
         strcpy(err_str, "DS_OK");
         break;

      case DSERR_ALLOCATED:
         strcpy(err_str, "DSERR_ALLOCATED");
         break;

      case DSERR_BADFORMAT:
         strcpy(err_str, "DSERR_BADFORMAT");
         break;

      case DSERR_INVALIDPARAM:
         strcpy(err_str, "DSERR_INVALIDPARAM");
         break;

      case DSERR_NOAGGREGATION:
         strcpy(err_str, "DSERR_NOAGGREGATION");
         break;

      case DSERR_OUTOFMEMORY:
         strcpy(err_str, "DSERR_OUTOFMEMORY");
         break;

      case DSERR_UNINITIALIZED:
         strcpy(err_str, "DSERR_UNINITIALIZED");
         break;

      case DSERR_UNSUPPORTED:
         strcpy(err_str, "DSERR_UNSUPPORTED");
         break;

      default:
         strcpy(err_str, "DSERR_UNKNOWN");
         break;
   }

   return err_str;
}
#else
#define ds_err(hr) "\0"
#endif



/* create_test_capture_buffer:
 *  Helper function that tries to create a capture buffer with
 *  the specified format and deletes it immediatly.
 */
static int create_test_capture_buffer(WAVEFORMATEX *wfx)
{
   LPDIRECTSOUNDCAPTUREBUFFER dsc_trybuf;
   DSCBUFFERDESC dsc_trybuf_desc;
   HRESULT hr;

   /* create the capture buffer */
   dsc_trybuf_desc.dwSize  = sizeof(DSCBUFFERDESC);
   dsc_trybuf_desc.dwFlags = 0;
   dsc_trybuf_desc.dwBufferBytes = 1024;
   dsc_trybuf_desc.dwReserved  = 0;
   dsc_trybuf_desc.lpwfxFormat = wfx;

   hr = IDirectSoundCapture_CreateCaptureBuffer(ds_capture, &dsc_trybuf_desc, &dsc_trybuf, NULL);
   
   if (FAILED(hr))
      return -1;

   IDirectSoundCaptureBuffer_Release(dsc_trybuf);
   return 0;
}



/* get_capture_format_support:
 *  Helper function to see if the specified input device
 *  can support a combination of capture settings.
 */
static int get_capture_format_support(int bits, int stereo, int rate,
                                      int autodetect, WAVEFORMATEX *wfx)
{
   int i;
   DSCCAPS dsCaps;
   HRESULT hr;
   WAVEFORMATEX *test_wfx;

   struct {
      unsigned long int type;
      unsigned long int freq;
      unsigned char bits;
      unsigned char channels;
      BOOL stereo;
   } ds_formats[] = {
      { WAVE_FORMAT_4S16,   44100, 16, 2, TRUE  },
      { WAVE_FORMAT_2S16,   22050, 16, 2, TRUE  },
      { WAVE_FORMAT_1S16,   11025, 16, 2, TRUE  },

      { WAVE_FORMAT_4M16,   44100, 16, 1, FALSE },
      { WAVE_FORMAT_2M16,   22050, 16, 1, FALSE },
      { WAVE_FORMAT_1M16,   11025, 16, 1, FALSE },

      { WAVE_FORMAT_4S08,   44100, 8,  2, TRUE  },
      { WAVE_FORMAT_2S08,   22050, 8,  2, TRUE  },
      { WAVE_FORMAT_1S08,   11025, 8,  2, TRUE  },

      { WAVE_FORMAT_4M08,   44100, 8,  1, FALSE },
      { WAVE_FORMAT_2M08,   22050, 8,  1, FALSE },
      { WAVE_FORMAT_1M08,   11025, 8,  1, FALSE },

      { WAVE_INVALIDFORMAT,     0, 0,  0, FALSE }
   };

   if (!ds_capture)
     return -1;

   /* if we have already a capture buffer working
    * we return its format as the only valid one
    */
   if (ds_capture_buf) {
      if (!autodetect) {
         /* we must check that everything that cares is
          * the same as in the capture buffer settings
          */
         if (((bits > 0) && (dsc_buf_wfx.wBitsPerSample != bits)) ||
             ( stereo && (dsc_buf_wfx.nChannels != 2)) ||
             (!stereo && (dsc_buf_wfx.nChannels != 1)) ||
             ((rate > 0) && (dsc_buf_wfx.nSamplesPerSec != (unsigned int) rate)))
            return -1;
      }

      /* return the actual capture buffer settings */
      if (wfx)
         memcpy(&dsc_buf_wfx, wfx, sizeof(WAVEFORMATEX));

      return 0;
   }

   /* we use a two-level checking process:
    *  - the normal check of exposed capabilities,
    *  - the actual creation of the capture buffer,
    * because of the single frequency limitation on some
    * sound cards (e.g SB16 ISA) in full duplex mode.
    */
   dsCaps.dwSize = sizeof(DSCCAPS);
   hr = IDirectSoundCapture_GetCaps(ds_capture, &dsCaps);
   if (FAILED(hr)) {
      _TRACE("Can't get input device caps (%s).\n", ds_err(hr));
      return -1;
   }

   if (wfx)
      test_wfx = wfx;
   else
      test_wfx = malloc(sizeof(WAVEFORMATEX));

   for (i=0; ds_formats[i].type != WAVE_INVALIDFORMAT; i++)
      /* if the current format is supported */
      if (dsCaps.dwFormats & ds_formats[i].type) {
         if (!autodetect) {
            /* we must check that everything that cares is
             * the same as in the capture buffer settings
             */
            if (((bits > 0) && (ds_formats[i].bits != bits)) ||
                (stereo && !ds_formats[i].stereo) ||
                (!stereo && ds_formats[i].stereo) ||
                ((rate > 0) && (ds_formats[i].freq != (unsigned int) rate)))
               continue;  /* go to next format */
         }

         test_wfx->wFormatTag = WAVE_FORMAT_PCM;
         test_wfx->nChannels = ds_formats[i].channels;
         test_wfx->nSamplesPerSec = ds_formats[i].freq;
         test_wfx->wBitsPerSample = ds_formats[i].bits;
         test_wfx->nBlockAlign = test_wfx->nChannels * (test_wfx->wBitsPerSample / 8);
         test_wfx->nAvgBytesPerSec = test_wfx->nSamplesPerSec * test_wfx->nBlockAlign;
         test_wfx->cbSize = 0;

         if (create_test_capture_buffer(test_wfx) == 0) {
            if (!wfx)
               free(test_wfx);
            return 0;
         }
      }

   if (!wfx)
      free(test_wfx);

   _TRACE("No valid recording formats found.\n");
   return -1;
}



/* digi_directsound_capture_init:
 */
int digi_directsound_capture_init(LPGUID guid)
{
   DSCCAPS dsCaps;
   WAVEFORMATEX wfx;
   HRESULT hr;

   /* the DirectSoundCapture interface is not part of DirectX 3 */
   if (_dx_ver < 0x0500)
      return -1;  

   /* create the device:
    *  we use CoCreateInstance() instead of DirectSoundCaptureCreate() to avoid
    *  the dll loader blocking the start of Allegro under DirectX 3.
    */
   hr = CoCreateInstance(&CLSID_DirectSoundCapture, NULL, CLSCTX_INPROC_SERVER,
                         &IID_IDirectSoundCapture, (LPVOID *)&ds_capture);

   if (FAILED(hr)) {
      _TRACE("Can't create DirectSoundCapture interface (%s).\n", ds_err(hr));
      goto Error;
   }

   /* initialize the device */
   hr = IDirectSoundCapture_Initialize(ds_capture, guid);

   if (FAILED(hr)) {
      hr = IDirectSoundCapture_Initialize(ds_capture, NULL);
      if (FAILED(hr)) {
         _TRACE("Can't initialize DirectSoundCapture interface (%s).\n", ds_err(hr));
         goto Error;
      }
   }

   /* get the device caps */
   dsCaps.dwSize = sizeof(DSCCAPS);
   hr = IDirectSoundCapture_GetCaps(ds_capture, &dsCaps);
   if (FAILED(hr)) {
      _TRACE("Can't get input device caps (%s).\n", ds_err(hr));
      goto Error;
   }

   /* cool little 'autodetection' process :) */
   if (get_capture_format_support(0, FALSE, 0, TRUE, &wfx) != 0) {
      _TRACE("The DirectSound hardware doesn't support any capture types.\n");
      goto Error;
   }

   /* set capabilities */
   digi_input_driver->rec_cap_bits = wfx.wBitsPerSample;
   digi_input_driver->rec_cap_stereo = (wfx.nChannels == 2) ? 1 : 0;

   return 0;

 Error:
   /* shutdown DirectSoundCapture */
   digi_directsound_capture_exit();

   return -1;
}



/* digi_directsound_capture_exit:
 */
void digi_directsound_capture_exit(void)
{
   /* destroy capture buffer */
   digi_directsound_rec_stop();

   /* shutdown DirectSoundCapture */
   if (ds_capture) {
      IDirectSoundCapture_Release(ds_capture);
      ds_capture = NULL;
   }
}



/* digi_directsound_capture_detect:
 */
int digi_directsound_capture_detect(LPGUID guid)
{
   HRESULT hr;

   /* the DirectSoundCapture interface is not part of DirectX 3 */
   if (_dx_ver < 0x500)
      return 0;  

   if (!ds_capture) {
      /* create the device:
       *  we use CoCreateInstance() instead of DirectSoundCaptureCreate() to avoid
       *  the dll loader blocking the start of Allegro under DirectX 3.
       */
      hr = CoCreateInstance(&CLSID_DirectSoundCapture, NULL, CLSCTX_INPROC_SERVER,
                            &IID_IDirectSoundCapture, (LPVOID *)&ds_capture);

      if (FAILED(hr)) {
         _TRACE("DirectSoundCapture interface creation failed during detect (%s).\n", ds_err(hr));
         return 0;
      }

      /* initialize the device */
      hr = IDirectSoundCapture_Initialize(ds_capture, guid);

      if (FAILED(hr)) {
         hr = IDirectSoundCapture_Initialize(ds_capture, NULL);
         if (FAILED(hr)) {
            _TRACE("DirectSoundCapture interface initialization failed during detect (%s).\n", ds_err(hr));
            return 0;
         }
      }

      _TRACE("DirectSoundCapture interface successfully created.\n");

      /* release DirectSoundCapture interface */
      IDirectSoundCapture_Release(ds_capture);
      ds_capture = NULL;
   }

   return 1;
}



/* digi_directsound_rec_cap_rate:
 *  Gets the maximum input frequency for the specified parameters.
 */
int digi_directsound_rec_cap_rate(int bits, int stereo)
{
   WAVEFORMATEX wfx;

   if (get_capture_format_support(bits, stereo, 0, FALSE, &wfx) != 0)
      return 0;

   return wfx.nSamplesPerSec;
}



/* digi_directsound_rec_cap_param:
 *  Determines if the combination of provided parameters can be
 *  used for recording.
 */
int digi_directsound_rec_cap_param(int rate, int bits, int stereo)
{
   if (get_capture_format_support(bits, stereo, rate, FALSE, NULL) == 0)
      return 2;

   if (get_capture_format_support(bits, stereo, 44100, FALSE, NULL) == 0)
      return -44100;

   if (get_capture_format_support(bits, stereo, 22050, FALSE, NULL) == 0)
      return -22050;

   if (get_capture_format_support(bits, stereo, 11025, FALSE, NULL) == 0)
      return -11025;

   return 0;
}



/* digi_directsound_rec_source:
 *  Sets the source for the audio recording.
 */
int digi_directsound_rec_source(int source)
{
   /* since DirectSoundCapture doesn't allow us to
    * select a input source manually, we return -1
    */
   return -1;
}



/* digi_directsound_rec_start:
 *  Start recording with the specified parameters.
 */
int digi_directsound_rec_start(int rate, int bits, int stereo)
{
   DSCBUFFERDESC dscBufDesc;
   HRESULT hr;

   if (!ds_capture || ds_capture_buf)
      return 0;

   /* check if we support the desired format */
   if ((bits <= 0) || (rate <= 0))
      return 0;

   if (get_capture_format_support(bits, stereo, rate, FALSE, &dsc_buf_wfx) != 0)
      return 0;

   digi_driver->rec_cap_bits = dsc_buf_wfx.wBitsPerSample;
   digi_driver->rec_cap_stereo = (dsc_buf_wfx.nChannels == 2) ? 1 : 0;

   /* create the capture buffer */
   dscBufDesc.dwSize  = sizeof(DSCBUFFERDESC);
   dscBufDesc.dwFlags = 0;
   dscBufDesc.dwBufferBytes = dsc_buf_wfx.nAvgBytesPerSec;
   dscBufDesc.dwReserved  = 0;
   dscBufDesc.lpwfxFormat = &dsc_buf_wfx;
   ds_capture_buffer_size = dscBufDesc.dwBufferBytes;

   hr = IDirectSoundCapture_CreateCaptureBuffer(ds_capture, &dscBufDesc, &ds_capture_buf, NULL);
   if (FAILED(hr)) {
      _TRACE("Can't create the DirectSound capture buffer (%s).\n", ds_err(hr));
      return 0;
   }

   hr = IDirectSoundCaptureBuffer_Start(ds_capture_buf, DSCBSTART_LOOPING);
   if (FAILED(hr)) {
      IDirectSoundCaptureBuffer_Release(ds_capture_buf);
      ds_capture_buf = NULL;

      _TRACE("Can't start the DirectSound capture buffer (%s).\n", ds_err(hr));
      return 0;
   }

   last_capture_pos = 0;
   input_wave_data = malloc(ds_capture_buffer_size);
   input_wave_bytes_read = 0;

   return ds_capture_buffer_size;
}



/* digi_directsound_rec_stop:
 *  Stops recording.
 */
void digi_directsound_rec_stop(void)
{
   if (ds_capture_buf) {
      IDirectSoundCaptureBuffer_Stop(ds_capture_buf);
      IDirectSoundCaptureBuffer_Release(ds_capture_buf);
      ds_capture_buf = NULL;
   }

   if (input_wave_data) {
      free(input_wave_data);
      input_wave_data = NULL;
   }
}



/* digi_directsound_rec_read:
 *  Reads the input buffer.
 */
int digi_directsound_rec_read(void *buf)
{
   unsigned char *input_ptr1, *input_ptr2, *linear_input_ptr;
   unsigned long int input_bytes1, input_bytes2, bytes_to_lock;
   unsigned long int capture_pos;
   HRESULT hr;
   BOOL buffer_filled = FALSE;

   if (!ds_capture || !ds_capture_buf || !input_wave_data)
      return 0;

   IDirectSoundCaptureBuffer_GetCurrentPosition(ds_capture_buf, &capture_pos, NULL);

   /* check if we are not still in the same capture position */
   if (last_capture_pos == capture_pos)
      return 0;

   /* check how many bytes we need to lock since last check */
   if (capture_pos > last_capture_pos) {
      bytes_to_lock = capture_pos - last_capture_pos;
   }
   else {
      bytes_to_lock = ds_capture_buffer_size - last_capture_pos;
      bytes_to_lock += capture_pos;
   }

   hr = IDirectSoundCaptureBuffer_Lock(ds_capture_buf, last_capture_pos,
                                       bytes_to_lock, (LPVOID *)&input_ptr1,
                                       &input_bytes1, (LPVOID *)&input_ptr2,
                                       &input_bytes2, 0);
   if (FAILED(hr))
      return 0;

   /* let's get the data aligned linearly */
   linear_input_ptr = malloc(bytes_to_lock);
   memcpy(linear_input_ptr, input_ptr1, input_bytes1);

   if (input_ptr2)
      memcpy(linear_input_ptr + input_bytes1, input_ptr2, input_bytes2);

   IDirectSoundCaptureBuffer_Unlock(ds_capture_buf, input_ptr1,
                                    input_bytes1, input_ptr2, input_bytes2);

   if ((input_wave_bytes_read + bytes_to_lock) >= ds_capture_buffer_size) {
      /* we fill the buffer */
      long int bytes_left_to_fill = ds_capture_buffer_size - input_wave_bytes_read;
      long int bytes_to_internal = bytes_to_lock - bytes_left_to_fill;

      /* copy old buffer to user buffer */
      memcpy((char*)buf, input_wave_data, input_wave_bytes_read);

      /* and the rest of bytes we would need to fill in the buffer */
      memcpy((char*)buf + input_wave_bytes_read, linear_input_ptr, bytes_left_to_fill);

      /* and the rest of the data to the internal buffer */
      input_wave_bytes_read = bytes_to_internal;
      memcpy(input_wave_data, linear_input_ptr + bytes_left_to_fill, bytes_to_internal);

      buffer_filled = TRUE;
  
      /* if we are using 16-bit data, we need to convert it to unsigned format */
      if (digi_driver->rec_cap_bits == 16) {
         unsigned int i;
         unsigned short *buf16 = (unsigned short *)buf;

         for (i = 0; i < ds_capture_buffer_size/2; i++)
            buf16[i] ^= 0x8000;
      }
   }
   else {
      /* we won't fill the buffer */
      memcpy(input_wave_data + input_wave_bytes_read, linear_input_ptr, bytes_to_lock);
      input_wave_bytes_read += bytes_to_lock;
   }

   free(linear_input_ptr);

   last_capture_pos = capture_pos;

   if (buffer_filled)
      return 1;
   else
      return 0;
}
