// Copyright (c) 2019 Giorgos Vougioukas
//
// The license can be found in the LICENSE file.

#include "euterpe_ds/audio_streamer_pa_ds.h"
#include "euterpe_ds/res/resource.h"

#include <math.h>
#include <string.h>

#include "WDL/fpcmp.h"
#include "WDL/db2val.h"
#include "WDL/pcmfmtcvt.h"
#include "WDL/ptrlist.h"

RSE_PaDirectSound::RSE_PaDirectSound()
  : m_error(paNoError)
  , m_stream(NULL)
  , m_running(false)
  , m_callback(NULL)
  , m_sample_fmt(16)
  , m_samplerate(44100)
  , m_output_nch(2)
  , m_output_dev(0)
  , m_ini_file(NULL)
{}

RSE_PaDirectSound::~RSE_PaDirectSound()
{}

bool RSE_PaDirectSound::Open()
{
  m_error = Pa_Initialize();
  if (m_error != paNoError) { return false; }

  WDL_FastString inipath(GetSettingsPath());
  inipath.Append("euterpe_ds.ini");

  m_ini_file = new RSE_IniFile(inipath.Get());

  m_sample_fmt = m_ini_file->read_int("ds_sample_fmt", 16, "euterpe_ds");
  m_samplerate = m_ini_file->read_int("ds_samplerate", 44100, "euterpe_ds");
  m_output_nch = m_ini_file->read_int("ds_output_nch", 2, "euterpe_ds");
  m_output_dev = m_ini_file->read_int("ds_output_dev",
    Pa_GetHostApiInfo(Pa_HostApiTypeIdToHostApiIndex(paDirectSound))->defaultOutputDevice,
    "euterpe_ds");

  // Make sure that the old values are valid
  // with the new approach.
  if (m_sample_fmt == 1616) m_sample_fmt = 16;
  if (m_sample_fmt == 2424) m_sample_fmt = 24;
  if (m_sample_fmt == 3224) m_sample_fmt = 24;
  if (m_sample_fmt == 3232) m_sample_fmt = 32;

  PaSampleFormat sf;

  if (m_sample_fmt == 16) sf = paInt16;
  else if (m_sample_fmt == 24) sf = paInt24;
  else if (m_sample_fmt == 32) sf = paInt32;

  PaStreamParameters output_param;
  output_param.channelCount = m_output_nch;
  output_param.device = m_output_dev;
  output_param.sampleFormat = sf;
  output_param.suggestedLatency = Pa_GetDeviceInfo(output_param.device)->defaultLowOutputLatency;
  output_param.hostApiSpecificStreamInfo = NULL;

  PaWinDirectSoundStreamInfo ds_stream_info;

  ds_stream_info.hostApiType = paDirectSound;
  ds_stream_info.size = sizeof(PaWinDirectSoundStreamInfo);
  ds_stream_info.version = 2;
  ds_stream_info.flags = 0;
  //ds_stream_info.flags = paWinDirectSoundUseLowLevelLatencyParameters;
  //ds_stream_info.framesPerBuffer = 1024;
  ds_stream_info.channelMask = PAWIN_SPEAKER_STEREO;
  output_param.hostApiSpecificStreamInfo = &ds_stream_info;

  m_buffer.Resize(16384); // 8192, 16384, 32768
  m_pcm.Resize(16384 * sizeof(int));

  m_error = Pa_OpenStream(&m_stream, 0, &output_param, m_samplerate,
    paFramesPerBufferUnspecified, paDitherOff, pa_callback, this);
    //1024, paNoFlag, pa_callback, this);

  if (m_error != paNoError)
  {
    wdl_log("cannot open stream | %s\n", Pa_GetErrorText(m_error));
    return false;
  }

  return true;
}

void RSE_PaDirectSound::Start(AudioCallback callback)
{
  m_error = Pa_StartStream(m_stream);

  if (m_error != paNoError)
  {
    wdl_log("cannot start waveout output stream\n");
    return;
  }

  m_callback = callback;

  m_running = true;
}

void RSE_PaDirectSound::Stop()
{
  m_error = Pa_StopStream(m_stream);

  if (m_error != paNoError)
  {
    wdl_log("cannot stop waveout output stream\n");
    return;
  }

  m_callback = NULL;

  m_running = false;
}

void RSE_PaDirectSound::Close()
{
  m_error = Pa_CloseStream(m_stream);

  if (m_error != paNoError)
  {
    wdl_log("cannot close waveout output stream\n");
  }

  m_error = Pa_Terminate();

  if (m_error != paNoError)
  {
    wdl_log("cannot terminate waveout output\n");
  }

  delete m_ini_file;
  m_ini_file = NULL;
}

bool RSE_PaDirectSound::IsRunning() const
{
  return m_running;
}

double RSE_PaDirectSound::GetSampleRate() const
{
  return (double)m_samplerate;
}

int RSE_PaDirectSound::pa_callback(const void *input, void *output,
  unsigned long frame_count, const PaStreamCallbackTimeInfo *time_info,
  PaStreamCallbackFlags status_flags, void *user_data)
{
  RSE_PaDirectSound *ud = (RSE_PaDirectSound *)user_data;

  if (!ud) { return 0; }
  if (!ud->m_callback) { return 0; }

  //if (status_flags & (paOutputOverflow | paOutputUnderflow))
  //{
  //  Sleep(1);
  //}

  //if (status_flags & (paOutputOverflow | paOutputUnderflow))
  //{
  //  WDL_ASSERT(1);
  //}

  short *out_16_bit = (short *)output;
  unsigned char *out_24_bit = (unsigned char *)output;
  float *out_32_bit_fp = (float *)output;
  int *out_32_bit = (int *)output;

  int fc = (int)frame_count * ud->m_output_nch;

  while (ud->m_buffer.GetSize() < fc)
  {
    ud->m_buffer.Resize(ud->m_buffer.GetSize() * 2);
    ud->m_pcm.Resize(ud->m_pcm.GetSize() * 2);
  }

  SAM *sam = ud->m_buffer.Get();

  //wdl_log("portaudio fc: %d\n", fc);

  ud->m_callback(&ud->m_buffer, fc, ud->m_output_nch);

  if (ud->m_sample_fmt == 16)
  {
    //ud->m_pcm.Resize(fc * sizeof(short));
#if (EUTERPE_SAMPLE_PRECISION == 8)
    doublesToPcm(sam, 1, fc, ud->m_pcm.Get(), 16, 1);
#else
    floatsToPcm(sam, 1, fc, ud->m_pcm.Get(), 16, 1);
#endif
    short *pcm = (short *)ud->m_pcm.Get();

    for (int i = 0; i < fc; i++)
    {
      *out_16_bit++ = pcm[i];
    }
  }
  else if (ud->m_sample_fmt == 24)
  {
    //ud->m_pcm.Resize(fc * sizeof(char) * 3);
#if (EUTERPE_SAMPLE_PRECISION == 8)
    doublesToPcm(sam, 1, fc, ud->m_pcm.Get(), 24, 1);
#else
    floatsToPcm(sam, 1, fc, ud->m_pcm.Get(), 24, 1);
#endif
    unsigned char *pcm = (unsigned char *)ud->m_pcm.Get();

    for (int i = 0; i < fc * 3; i += 3)
    {
      *out_24_bit++ = pcm[i];
      *out_24_bit++ = pcm[i + 1];
      *out_24_bit++ = pcm[i + 2];
    }
  }
  else if (ud->m_sample_fmt == 32)
  {
    //ud->m_pcm.Resize(fc * sizeof(int));
#if (EUTERPE_SAMPLE_PRECISION == 8)
    doublesToPcm(sam, 1, fc, ud->m_pcm.Get(), 32, 1);
#else
    floatsToPcm(sam, 1, fc, ud->m_pcm.Get(), 32, 1);
#endif
    int *pcm = (int *)ud->m_pcm.Get();

    for (int i = 0; i < fc; i++)
    {
      *out_32_bit++ = pcm[i];
    }
  }

  //Sleep(1);

  return paContinue;
}
