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

#include "thalia_wasapi/audio_streamer_wasapi.h"
#include "thalia_wasapi/wasapi_entry_point.h"

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

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

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

THA_Wasapi::~THA_Wasapi()
{}

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

  m_sample_fmt = THA_GetAudioDeviceBitDepth();
  m_samplerate = (int)THA_GetAudioDeviceSamplerate();
  m_output_nch = THA_GetAudioDeviceOutputChannels();
  m_exclusive = THA_GetWASAPIExclusiveMode();
  m_output_dev = Pa_GetHostApiInfo(Pa_HostApiTypeIdToHostApiIndex(paWASAPI))->defaultOutputDevice;

  if (!m_exclusive)
  {
    WAVEFORMATEX wfe;
    PaWasapi_GetDeviceDefaultFormat(&wfe, sizeof(WAVEFORMATEX), m_output_dev);
    m_samplerate = wfe.nSamplesPerSec;
    m_output_nch = wfe.nChannels;
    int bps = wfe.wBitsPerSample;
    if (bps == 16) m_sample_fmt = 0;
    else if (bps == 24) m_sample_fmt = 1;
    else if (bps == 32) m_sample_fmt = 2;
  }

  PaSampleFormat sf;
  if (m_sample_fmt == 0) sf = paInt16;
  else if (m_sample_fmt == 1) sf = paInt24;
  else if (m_sample_fmt == 2) sf = paInt32;
  else if (m_sample_fmt == 3) sf = paFloat32;

  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;

  PaWasapiStreamInfo wasapi_stream_info;

  wasapi_stream_info.hostApiType = paWASAPI;
  wasapi_stream_info.size = sizeof(PaWasapiStreamInfo);
  wasapi_stream_info.version = 1;
  if (m_exclusive)
  {
    wasapi_stream_info.flags = paWinWasapiExclusive | paWinWasapiThreadPriority;
    wasapi_stream_info.threadPriority = eThreadPriorityProAudio;
  }
  else
  {
    wasapi_stream_info.flags = 0; //paWinWasapiAutoConvert;
  }
  wasapi_stream_info.channelMask = PAWIN_SPEAKER_STEREO;
  output_param.hostApiSpecificStreamInfo = &wasapi_stream_info;

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

  memset(m_buffer.Get(), 0, 16384 * sizeof(SAM));
  memset(m_pcm.Get(), 0, 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 (WDL_NOT_NORMALLY(m_error != paNoError))
  {
    wdl_log("cannot open stream | %s\n", Pa_GetErrorText(m_error));
    return false;
  }

  return true;
}

void THA_Wasapi::Start(THA_AudioCallback callback)
{
  m_error = Pa_StartStream(m_stream);

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

  m_callback = callback;

  m_running = true;
}

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

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

  m_callback = NULL;

  m_running = false;
}

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

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

  m_error = Pa_Terminate();

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

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

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

int THA_Wasapi::pa_callback(const void *input, void *output,
  unsigned long frame_count, const PaStreamCallbackTimeInfo *time_info,
  PaStreamCallbackFlags status_flags, void *user_data)
{
  THA_Wasapi *ud = (THA_Wasapi *)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 == 0)
  {
    //ud->m_pcm.Resize(fc * sizeof(short));
#if (THA_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 == 1)
  {
    //ud->m_pcm.Resize(fc * sizeof(char) * 3);
#if (THA_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 == 2)
  {
    //ud->m_pcm.Resize(fc * sizeof(int));
#if (THA_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];
    }
  }
  else if (ud->m_sample_fmt == 3)
  {
    float *pcm = (float *)ud->m_pcm.Get();

    for (int i = 0; i < fc; i++)
    {
      *out_32_bit_fp++ = (float)sam[i];
    }
  }

  //Sleep(1);

  return paContinue;
}
