#ifdef _WIN32
#include <windows.h>
#include <stdio.h>
#else
#include <memory.h>
#include <stdlib.h>
#endif
#include <math.h>

#if defined(_WIN32) && defined(_DEBUG)
// Optional
#include <vld.h>
#endif

#include "RSI/rsi_plugin_public.h"
#include "WDL/heapbuf.h"
#include "WDL/queue.h"

#ifndef _WINDEF_
#define _WINDEF_
#endif

#include <SoundTouch.h>
#define DEFAULT_SEQUENCE_MS     82
#define DEFAULT_SEEKWINDOW_MS   14
#define DEFAULT_OVERLAP_MS      12

#include "WDL/mutex.h"

static HINSTANCE st_instance;
static HWND st_main_hwnd;

static WDL_Mutex s_createmutex;
class ST_PitchShift : public RSI_IPitchShift
{
  double m_last_sr WDL_FIXALIGN;
  double m_last_shift, m_last_tempo;
  double m_latent_outsamples;
  WDL_TypedQueue<SAM> m_outq;

  int m_last_nch;
  soundtouch::SoundTouch *m_st;
  WDL_TypedBuf<SAM> m_inputbuf;

  bool m_has_procspl;

public:
  int m_qual;

  ST_PitchShift()
  {
    m_latent_outsamples=0;
    m_has_procspl=false;
    m_qual=0;
    m_st=0;
    m_last_sr=44100.0;
    m_last_tempo=1.0;
    m_last_shift=1.0;
    m_last_nch=2;
    ST_PitchShift::Reset();
  }
  ~ST_PitchShift()
  {
    if (m_st)
    {
      s_createmutex.Enter();
      delete m_st;
      m_st=0;
      s_createmutex.Leave();
    }
  }

  void set_srate(double srate)
  {
    if (fabs(m_last_sr - srate) > 0.1)
      m_st->setSampleRate((int)((m_last_sr=srate)+0.5));
  }

  void set_nch(int nch)
  {
    if (m_last_nch != nch)
    {
      m_st->setChannels(m_last_nch=nch);
      m_st->clear();
      m_has_procspl=false;
    }
  }
  void set_formant_shift(double shift)
  {}

  void set_shift(double shift)
  {
    if (shift > 32.0) shift=32.0; // 5 octaves up
    else if (shift < 0.03125) shift=0.03125; // 5 octaves down
    if (fabs(m_last_shift - shift) > 0.0000001)
    {
      m_st->setPitch((float) (m_last_shift=shift));
    }
  }

  void set_tempo(double tempo)
  {
    if (tempo > 32.0) tempo = 32.0;
    else if (tempo < 0.001) tempo =0.001;

    if (fabs(m_last_tempo - tempo) > 0.0000001)
    {
      m_st->setTempo((float) (m_last_tempo=tempo));
    }
  }

  SAM *GetBuffer(int size)
  {
    m_has_procspl=true;
    return m_inputbuf.Resize(size*m_last_nch);
  }

  void BufferDone(int input_filled)
  {
    if (input_filled)
    {
      int i=input_filled*m_last_nch;
#if RSI_SAMPLE_PRECISION > 4
      SAM *buf=m_inputbuf.Get();
      float *bout=(float *)m_inputbuf.Get();
      while (i-->0)
      {
        float f =(float) *buf++;
        *bout++ = f;
      }
#endif
      m_st->putSamples((float *)m_inputbuf.Get(),input_filled);
      m_latent_outsamples +=  input_filled / m_last_tempo;
    }
  }

  void Reset()
  {
    m_latent_outsamples=0.0;
    //m_st->clear();
    s_createmutex.Enter();
    delete m_st;
    m_st = new soundtouch::SoundTouch;
    s_createmutex.Leave();
    m_st->setSampleRate((int) (m_last_sr+0.5));
    m_st->setTempo((float) (m_last_tempo));
    m_st->setPitch((float) (m_last_shift));
    m_st->setChannels(m_last_nch);
    UpdateQuality();
    m_has_procspl=false;
    m_outq.Clear();
  }

  void FlushSamples()
  {
    if (!m_st || !m_last_nch) return;
    float tmp[256]={0,};

    int lo=(int)floor(m_latent_outsamples+0.9999999);
    while (m_st->numSamples() < lo)
    {
      m_st->putSamples(tmp,256/m_last_nch);
    }
    float *in=m_st->ptrBegin();
    SAM *op=m_outq.Add(NULL,lo*m_last_nch);
    if (op)
    {
      int x=m_last_nch*lo;
      while (x--) *op++ = *in++;
    }
    m_st->receiveSamples(lo);
    m_latent_outsamples=0;
    m_st->clear();
  }

  bool IsReset() { return !m_has_procspl; }

  int GetSamples(int requested_output, SAM *buffer) // returns number of samplepairs returned
  {
    int offs=0;
    if (m_outq.GetSize()&&m_last_nch)
    {
      offs=m_outq.GetSize()/m_last_nch;
      if (offs > requested_output) offs=requested_output;
      memcpy(buffer,m_outq.Get(),offs*sizeof(SAM)*m_last_nch);
      m_outq.Advance(offs*m_last_nch);
      m_outq.Compact();
      requested_output -= offs;
      buffer+=offs*m_last_nch;
    }

    if (requested_output>0)
    {
      if (requested_output > (int)m_st->numSamples()) requested_output = (int)m_st->numSamples();

      if (requested_output<0)requested_output=0;
      if (requested_output > 0)
      {
        float *in=m_st->ptrBegin();
        int x=requested_output * m_last_nch;
        while (x-->0) *buffer++ = (SAM) *in++;

        m_latent_outsamples -= requested_output;
        m_st->receiveSamples(requested_output);
      }
    }
    return offs+requested_output;
  }

  int GetLatency() // current latency in samples
  {
    return m_st->numUnprocessedSamples();
  }

  void SetQualityParameter(int parm) // 0 Default settings, 1 High Quality, 2 Fast
  {
    m_qual=parm;
    UpdateQuality();
  }

  void UpdateQuality()
  {
    if (!m_st) return;

    if (m_qual<0 || m_qual > 2) m_qual=0;

    m_st->setSetting(SETTING_USE_AA_FILTER,m_qual!=2); // always enable AA
    m_st->setSetting(SETTING_AA_FILTER_LENGTH,m_qual==1?128:32);
    m_st->setSetting(SETTING_USE_QUICKSEEK,m_qual==2?1:0);
    m_st->setSetting(SETTING_SEQUENCE_MS,m_qual==0?DEFAULT_SEQUENCE_MS:m_qual==2?DEFAULT_SEQUENCE_MS*2:DEFAULT_SEQUENCE_MS/2);
    m_st->setSetting(SETTING_SEEKWINDOW_MS,m_qual==0?DEFAULT_SEEKWINDOW_MS:m_qual==2?DEFAULT_SEEKWINDOW_MS/2:DEFAULT_SEEKWINDOW_MS*2);
    m_st->setSetting(SETTING_OVERLAP_MS,m_qual==0?DEFAULT_OVERLAP_MS:m_qual==2?DEFAULT_OVERLAP_MS/2:DEFAULT_OVERLAP_MS);

  }

};

RSI_IPitchShift *CreatePitchShift()
{
  ST_PitchShift *p = new ST_PitchShift;

  if (p)
  {
    return p;
  }
  else
  {
    delete p;
  }

  return NULL;
}

RSI_PitchShiftRegister st_pitch_shift_reg =
{
  &CreatePitchShift
};

extern "C"
{
  RSI_PLUGIN_EXPORT int RSI_PLUGIN_ENTRYPOINT(
    RSI_PLUGIN_HINSTANCE instance, RSI_PluginInfo *rec)
  {
    st_instance = instance;

    if (rec)
    {
      if (rec->caller_version != RSI_PLUGIN_VERSION || !rec->GetFunc)
      {
        return 0;
      }

      st_main_hwnd = rec->hwnd_main;

      if (!rec->Register)
      {
        return 0;
      }

      rec->Register("pitch_shift:soundtouch", &st_pitch_shift_reg);

      return 1;
    }
    else
    {
      return 0;
    }
  }
}
