// SPDX-License-Identifier: Unlicense

#include "stretcher.h"

#ifdef SHR3D_MUSIC_STRETCHER

#define NO_EXCEPTIONS
#define _HAS_STD_BYTE 0
#define NOMINMAX
#undef HAVE_FFTW3

#ifdef PLATFORM_LINUX
#define USE_PTHREADS
#endif // PLATFORM_LINUX

#include "../deps/rubberband-4.0.0/single/RubberBandSingle.cpp"

#include "global.h"
#include <thread>

static RubberBandStretcher stretcher(sampleRate(), 2, RubberBandStretcher::OptionProcessRealTime | RubberBandStretcher::OptionEngineFaster);
static constexpr i32 maxProcessSize = 128;

static void strecher_worker()
{
  SongIndex lastSelectedSongIndex = -1;
  f32 lastPlaybackStretchRatio = 1.0f;
  i64 allocatedBufferMonoLength = 0;

  for (;;)
  {
  start:
    if (Global::selectedSongIndex >= 0)
    {
      if (Global::musicStretchRatio == 1.0f && Global::musicStretchRatio != lastPlaybackStretchRatio)
      {
        lastPlaybackStretchRatio = Global::musicStretchRatio;
        const u8 activeIndex = to_underlying_(Global::musicDoubleBufferStatus) % 2;
        Global::musicPlaybackBufferLR[0] = &Global::musicDoubleBuffer[activeIndex][0];
        Global::musicPlaybackBufferLR[1] = &Global::musicDoubleBuffer[activeIndex][Global::musicBufferLength];
        Global::musicPlaybackLength = Global::musicBufferLength;
        Global::musicPlaybackPosition = musicTimeElapsedToPlaybackPositionNonStretched(Global::musicTimeElapsedNS, f32(sampleRate()));
      }
      else if (Global::musicStretchRatio != 1.0f)
      {
        if (lastSelectedSongIndex != Global::selectedSongIndex
          || lastPlaybackStretchRatio != Global::musicStretchRatio
          || Global::musicTimeElapsedNS < Global::musicStretcherInputBeginTimeNS
          || Global::musicPlaybackPosition > Global::musicPlaybackLength + sampleRate())
        {
          const i64 upperMonoLength = i64(f32(Global::musicBufferLength) * Const::musicStretcherMaxStretchRatio * 1.01f); // better to malloc too much than too less

          if (upperMonoLength > allocatedBufferMonoLength)
          {
            ASSERT(Global::musicStretcherDoubleBuffer[0] == nullptr);
            Global::musicStretcherDoubleBuffer[0] = reinterpret_cast<f32*>(malloc(upperMonoLength * 2 * sizeof(f32)));
            allocatedBufferMonoLength = upperMonoLength;
          }

          Global::musicPlaybackBufferLR[0] = &Global::musicStretcherDoubleBuffer[0][0];
          Global::musicPlaybackBufferLR[1] = &Global::musicStretcherDoubleBuffer[0][allocatedBufferMonoLength];

          if (Global::musicTimeElapsedNS < 1_s) // when song loops make sure to not redo the whole song just because the first milliseconds are missing.
          {
            Global::musicStretcherInputBeginTimeNS = 0;
            Global::musicStretcherInputBegin = 0;
          }
          else
          {
            Global::musicStretcherInputBeginTimeNS = Global::musicTimeElapsedNS;
            Global::musicStretcherInputBegin = musicTimeElapsedToPlaybackPositionNonStretched(max_(Global::musicTimeElapsedNS, TimeNS(0)), f32(sampleRate()));
          }
          Global::musicStretcherInputCurrent = Global::musicStretcherInputBegin;
          Global::musicPlaybackLength = Global::musicStretcherInputBegin * Global::musicStretchRatio;
          Global::musicPlaybackPosition = musicTimeElapsedToPlaybackPositionNonStretched(Global::musicTimeElapsedNS, f32(sampleRate())) * Global::musicStretchRatio;

          lastSelectedSongIndex = Global::selectedSongIndex;
          lastPlaybackStretchRatio = Global::musicStretchRatio;

          stretcher.reset();

          stretcher.setMaxProcessSize(maxProcessSize);
          stretcher.setTimeRatio(Global::musicStretchRatio);

          while (Global::musicStretcherInputCurrent < Global::musicBufferLength)
          {
            if (Global::musicTimeElapsedNS < Global::musicStretcherInputBeginTimeNS)
              goto start;

            if (Global::musicPlaybackPosition > Global::musicPlaybackLength + sampleRate()) // start from scratch when the playback position is 1 second ahead.
              goto start;

            if (lastSelectedSongIndex != Global::selectedSongIndex || lastPlaybackStretchRatio != Global::musicStretchRatio)
              goto start;

            ASSERT(0 == stretcher.available()); // sanity check. we always retrieve all stretched audio

            const i32 remainingLength = i32(Global::musicBufferLength - Global::musicStretcherInputCurrent);
            ASSERT(remainingLength > 0);
            const i32 fillSize = min_(maxProcessSize, remainingLength);

            const u8 activeIndex = to_underlying_(Global::musicDoubleBufferStatus) % 2;
            const f32* const input_ptr[2]{
              &Global::musicDoubleBuffer[activeIndex][Global::musicStretcherInputCurrent],
              &Global::musicDoubleBuffer[activeIndex][Global::musicStretcherInputCurrent + Global::musicBufferLength]
            };

            const bool isFinal = (remainingLength <= maxProcessSize);
            stretcher.process(input_ptr, fillSize, isFinal);
            Global::musicStretcherInputCurrent += fillSize;
            ASSERT(isFinal == (Global::musicBufferLength == Global::musicStretcherInputCurrent));

            const i32 retrieveable = stretcher.available();
            if (retrieveable > 0)
            {
              f32* const output_ptr[2]{
                 &Global::musicStretcherDoubleBuffer[0][Global::musicPlaybackLength],
                 &Global::musicStretcherDoubleBuffer[0][Global::musicPlaybackLength + allocatedBufferMonoLength]
              };

              stretcher.retrieve(output_ptr, retrieveable);

              Global::musicPlaybackLength += retrieveable;
            }
          }
          goto start;
        }
      }
    }

    std::this_thread::sleep_for(std::chrono::microseconds(100));
  }
}


void Stretcher::init()
{
  static std::thread wokerThread(strecher_worker);
}

#endif // SHR3D_MUSIC_STRETCHER
