#include "dspcontext.h"
#include "audioclipbackend.h"
#include "buffer.h"
#include "remotebuffer.h"
#include "rendercontext.h"
#include "timing/piecewise_linear.h"
#include "util.h"
#include <algorithm>
#include <catch2/catch.hpp>
#include <emscripten/bind.h>
#include <emscripten/webaudio.h>
#include <map>
#include <memory>
#include <queue>
#include <set>
#include <unordered_map>
#include <vector>

// Web audio glue

static uint8_t audioThreadStack[3670016];

EMSCRIPTEN_WEBAUDIO_T DSPContext::audioContext = -1;
bool DSPContext::asyncInitialized = false;
int DSPContext::audioContextSampleRate = 0;

void DSPContext::AudioThreadInitialized(EMSCRIPTEN_WEBAUDIO_T audioContext,
                                        bool success, void *userData) {
  if (!success) {
    auto continuation = static_cast<ContinueDSPContextCreate *>(userData);
    continuation->reject(emscripten::val("Failed to initialize audio context"));
    delete continuation;
    return;
  }

  asyncInitialized = true;

  CreateProcessorAsync(static_cast<ContinueDSPContextCreate *>(userData));
}

void DSPContext::CreateProcessorAsync(ContinueDSPContextCreate *continuation) {
  WebAudioWorkletProcessorCreateOptions opts = {
      .name = continuation->name.c_str(),
  };
  emscripten_create_wasm_audio_worklet_processor_async(
      audioContext, &opts, &AudioWorkletProcessorCreated, continuation);
}

void DSPContext::AudioWorkletProcessorCreated(
    EMSCRIPTEN_WEBAUDIO_T audioContext, bool success, void *userData) {
  if (!success) {
    auto continuation = static_cast<ContinueDSPContextCreate *>(userData);
    continuation->reject(emscripten::val("Failed to create processor"));
    delete continuation;
    return;
  }

  auto continuation = static_cast<ContinueDSPContextCreate *>(userData);

  int outputChannelCounts[1] = {continuation->channelCount};
  EmscriptenAudioWorkletNodeCreateOptions options = {.numberOfInputs = 0,
                                                     .numberOfOutputs = 1,
                                                     .outputChannelCounts =
                                                         outputChannelCounts};

  DSPContextRoot *root = nullptr;

  try {
    root = new DSPContextRoot{
        .instance = std::make_shared<DSPContext>(
            continuation->vuAlpha, continuation->channelCount,
            audioContextSampleRate, false, true,
            continuation->analyticsObserver),
    };
  } catch (const std::exception &e) {
    continuation->reject(emscripten::val(e.what()));
    delete continuation;
    return;
  }

  // Create node
  EMSCRIPTEN_AUDIO_WORKLET_NODE_T wasmAudioWorklet =
      emscripten_create_wasm_audio_worklet_node(audioContext,
                                                continuation->name.c_str(),
                                                &options, &RunWorklet, root);

  // Connect it to audio context destination
  EM_ASM({emscriptenGetAudioObject($0).connect(
             emscriptenGetAudioObject($1).destination)},
         wasmAudioWorklet, audioContext);

  EM_ASM({ console.log(emscriptenGetAudioObject($0)); }, audioContext);

  if (emscripten_audio_context_state(audioContext) !=
      AUDIO_CONTEXT_STATE_RUNNING) {
    emscripten_resume_audio_context_sync(audioContext);
  }

  continuation->resolve(root->instance);
  delete continuation;
}

bool DSPContext::RunWorklet(int numInputs, const AudioSampleFrame *inputs,
                            int numOutputs, AudioSampleFrame *outputs,
                            int numParams, const AudioParamFrame *params,
                            void *userData) {
  auto *root = static_cast<DSPContextRoot *>(userData);

  return root->instance->run(numInputs, inputs, numOutputs, outputs);
}

DSPContextPromise
DSPContext::create(const std::string &name, float vuAlpha,
                   const std::shared_ptr<AnalyticsObserver> &analyticsObserver,
                   std::optional<int> channelCount) {
  int channelCountVal = channelCount.value_or(2);

  // only mono or stereo are supported for now
  assert(channelCountVal == 1 || channelCountVal == 2);

  auto promObj = make_blank_promise();
  auto prom = promObj["promise"].as<DSPContextPromise>();
  auto continuation = new ContinueDSPContextCreate{
      .name = name,
      .vuAlpha = vuAlpha,
      .channelCount = channelCountVal,
      .analyticsObserver = analyticsObserver,
      .resolve = promObj["capturedResolve"],
      .reject = promObj["capturedReject"],
  };

  if (audioContext == -1) {
    EmscriptenWebAudioCreateAttributes attrs = {.latencyHint = nullptr};
    audioContext = emscripten_create_audio_context(&attrs);
    audioContextSampleRate = EM_ASM_INT(
        {
          var audioContext = emscriptenGetAudioObject($0);
          return audioContext.sampleRate;
        },
        audioContext);
    emscripten_start_wasm_audio_worklet_thread_async(
        audioContext, audioThreadStack, sizeof(audioThreadStack),
        &AudioThreadInitialized, continuation);
  } else {
    assert(asyncInitialized);
    CreateProcessorAsync(continuation);
  }

  return prom;
}

DSPContextPromise DSPContext::createOffline(const std::string &name,
                                            float vuAlpha, int channelCount,
                                            int sampleRate,
                                            bool enableLimiter) {
  auto result = std::make_shared<DSPContext>(vuAlpha, channelCount, sampleRate,
                                             true, enableLimiter, nullptr);
  return emscripten::val::global("Promise").call<DSPContextPromise>("resolve",
                                                                    result);
}

// End web audio glue

static std::shared_ptr<Limiter> make_project_limiter(int channelCount,
                                                     int sampleRate) {
  return std::make_shared<Limiter>(channelCount, sampleRate, 0.0279f, 0.0276f,
                                   0.026f, 0.f, false, 0.8f);
}

DSPContext::DSPContext(
    float vuAlpha, int channelCount, int sampleRate, bool isOffline,
    bool enableLimiter,
    const std::shared_ptr<AnalyticsObserver> &analyticsObserver)
    : sync_newPlayingTimeline(nullptr), rt_playingTimeline(nullptr),
      timelineDestroyRequests([] {
        return true; // XXX
      }),
      vuAlpha(vuAlpha), channelCount(channelCount), sampleRate(sampleRate),
      isOffline(isOffline),
      meter(std::make_shared<Meter>(channelCount, sampleRate, 10, 10, vuAlpha)),
      analyticsObserver(analyticsObserver),
      mipMapWorker(isOffline ? nullptr : std::make_shared<MipMapWorker>()),
      limiter(enableLimiter ? make_project_limiter(channelCount, sampleRate)
                            : nullptr),
      timelineSharedState(std::make_shared<TimelineSharedState>()) {}

DSPContext::~DSPContext() {
  if (rt_playingTimeline) {
    delete rt_playingTimeline;
  }

  if (sync_newPlayingTimeline) {
    delete sync_newPlayingTimeline;
  }
}

// Helper function to assign clips to conflict-free groups
std::vector<std::vector<std::shared_ptr<AudioClip>>>
DSPContext::assignClipsToGroups(
    const std::vector<std::shared_ptr<AudioClip>> &allClips) {

  struct ClipInterval {
    std::shared_ptr<AudioClip> clip;
    double startBeats;
    double endBeats;
    int groupIndex;

    ClipInterval(std::shared_ptr<AudioClip> c)
        : clip(std::move(c)), groupIndex(-1) {
      startBeats = clip->getTimelineStartBeats();
      endBeats = clip->getTimelineEndBeats();
    }
  };

  // Create interval list
  std::vector<ClipInterval> intervals;
  intervals.reserve(allClips.size());
  for (auto clip : allClips) {
    intervals.emplace_back(clip);
  }

  // Sort intervals by start time (O(N log N))
  std::sort(intervals.begin(), intervals.end(),
            [](const ClipInterval &a, const ClipInterval &b) {
              return a.startBeats < b.startBeats;
            });

  // Greedy assignment using priority queue for group end times
  // Each entry: (end_time, group_index)
  std::priority_queue<std::pair<double, int>,
                      std::vector<std::pair<double, int>>,
                      std::greater<std::pair<double, int>>>
      groupEndTimes;

  int nextGroupIndex = 0;

  // Process each interval in start time order
  for (auto &interval : intervals) {
    int assignedGroup = -1;

    // Find earliest ending group that can accommodate this interval
    if (!groupEndTimes.empty() &&
        groupEndTimes.top().first <= interval.startBeats) {
      // Reuse existing group
      assignedGroup = groupEndTimes.top().second;
      groupEndTimes.pop();
    } else {
      // Create new group
      assignedGroup = nextGroupIndex++;
    }

    // Assign interval to group and update group end time
    interval.groupIndex = assignedGroup;
    groupEndTimes.emplace(interval.endBeats, assignedGroup);
  }

  // Convert to output format: group clips by assigned group index
  std::vector<std::vector<std::shared_ptr<AudioClip>>> groups(nextGroupIndex);
  for (const auto &interval : intervals) {
    groups[interval.groupIndex].push_back(interval.clip);
  }

  return groups;
}

std::shared_ptr<Timeline> DSPContext::createTimeline(
    const std::shared_ptr<time_transform::TimelineTempoMap> &timing,
    const std::vector<std::shared_ptr<Track>> &tracks) {

  // Collect all clips from all tracks
  std::vector<std::shared_ptr<AudioClip>> allClips;
  for (const auto &track : tracks) {
    for (int i = 0; i < track->getClipCount(); ++i) {
      allClips.push_back(track->getClip(i));
    }
  }

  // Assign clips to conflict-free groups
  auto clipGroups = assignClipsToGroups(allClips);

  struct TimestretchReaderCompatKey {
    int underlyingSampleRate;
    int channelCount;

    TimestretchReaderCompatKey(int underlyingSampleRate, int channelCount)
        : underlyingSampleRate(underlyingSampleRate),
          channelCount(channelCount) {}

    bool operator<(const TimestretchReaderCompatKey &other) const {
      if (underlyingSampleRate != other.underlyingSampleRate) {
        return underlyingSampleRate < other.underlyingSampleRate;
      }
      return channelCount < other.channelCount;
    }
  };

  struct BufferAssignment {
    std::shared_ptr<RandomAccessAudioReadable> buffer;
    int usedByGroup;

    BufferAssignment() : buffer(nullptr), usedByGroup(-1) {}
  };

  struct TimestretchReaderAssignment {
    std::shared_ptr<TimestretchReader> reader;
    int usedByGroup;

    TimestretchReaderAssignment() : reader(nullptr), usedByGroup(-1) {}
  };

  std::unordered_map<std::shared_ptr<RandomAccessAudioReadable>,
                     BufferAssignment>
      bufferAssignments;
  std::map<TimestretchReaderCompatKey, TimestretchReaderAssignment>
      timestretchReaderAssignments;

  // Process each group
  for (int groupIdx = 0; groupIdx < (int)clipGroups.size(); ++groupIdx) {
    const auto &clipsInGroup = clipGroups[groupIdx];

    // Resamplers by channel count
    std::map<int, std::shared_ptr<Resampler>> resamplerAssignments;

    // Process clips in this group
    for (auto &clip : clipsInGroup) {
      const auto originalReadable = clip->getOriginalUnderlyingReadable();
      if (originalReadable == nullptr) {
        continue;
      }
      int underlyingSampleRate = originalReadable->getSampleRate();

      // Resampler
      auto resampler =
          resamplerAssignments[originalReadable->getChannelCount()];
      if (resampler == nullptr) {
        resampler = std::make_shared<Resampler>(
            originalReadable->getChannelCount(), sampleRate, sampleRate);
        resamplerAssignments[originalReadable->getChannelCount()] = resampler;
      }

      // TimestretchReader
      auto &tsa = timestretchReaderAssignments[TimestretchReaderCompatKey(
          underlyingSampleRate, originalReadable->getChannelCount())];
      if (tsa.reader == nullptr || tsa.usedByGroup != groupIdx) {
        tsa.reader = std::make_shared<TimestretchReader>(
            originalReadable->getChannelCount(),
            originalReadable->getSampleRate(), sampleRate, true);
        tsa.usedByGroup = groupIdx;
      }

      // Readable
      auto &ba = bufferAssignments[originalReadable];
      if (ba.buffer == nullptr || ba.usedByGroup != groupIdx) {
        ba.buffer = originalReadable->clone();
        ba.usedByGroup = groupIdx;
      }

      clip->setBackend(
          std::make_shared<AudioClipBackend>(tsa.reader, ba.buffer, resampler));
    }
  }

  return std::make_shared<Timeline>(
      sampleRate, timing, tracks, analyticsObserver, limiter,
      isOffline ? nullptr : meter, timelineSharedState);
}

std::shared_ptr<Track>
DSPContext::createTrack(float gain, float pan, bool muted,
                        const std::shared_ptr<Meter> &meter,
                        const std::shared_ptr<FilterChain> &filterChain,
                        const std::vector<std::shared_ptr<AudioClip>> &clips) {
  return std::make_shared<Track>(sampleRate, vuAlpha, gain, pan, muted, meter,
                                 filterChain, clips);
}

std::shared_ptr<AudioClip> DSPContext::createAudioClip(
    std::shared_ptr<RandomAccessAudioReadable> underlyingReadable,
    std::shared_ptr<time_transform::WarpMap<double>> warpMap, float gain,
    double timelineStartBeats, double timelineEndBeats, double loopStartBeats,
    double loopEndBeats, double readStartBeats, double fadeInBeats,
    double fadeInExponent, double fadeOutBeats, double fadeOutExponent,
    float transposition, double warpedContentBps, bool warpEnabled,
    const std::string &arrangementId) {
  return std::make_shared<AudioClip>(
      underlyingReadable, warpMap, sampleRate, gain, timelineStartBeats,
      timelineEndBeats, loopStartBeats, loopEndBeats, readStartBeats,
      fadeInBeats, fadeInExponent, fadeOutBeats, fadeOutExponent, transposition,
      warpedContentBps, warpEnabled, Uuid(arrangementId));
}

std::shared_ptr<Meter> DSPContext::createMeter() {
  return std::make_shared<Meter>(channelCount, sampleRate, 10, 10, vuAlpha);
}

std::shared_ptr<FilterChain> DSPContext::createFilterChain(int filterCount) {
  return std::make_shared<FilterChain>(sampleRate, channelCount, filterCount);
}

void DSPContext::swapLiveTimeline(
    const std::shared_ptr<Timeline> &newTimeline) {
  flushDestroyRequests();

  // Send new timeline, possibly getting back a pending timeline
  const auto tdr = new TimelineDestroyRequest(newTimeline);
  const auto tdrPtrVal = reinterpret_cast<uint32_t>(tdr);

  while (true) {
    const auto pendingTdrPtrVal =
        emscripten_atomic_load_u32(&sync_newPlayingTimeline);

    if (pendingTdrPtrVal != 0) {
      auto pendingTdr =
          reinterpret_cast<TimelineDestroyRequest *>(pendingTdrPtrVal);
      if (pendingTdr->timeline == newTimeline) {
        // nothing to do
        delete tdr;
        break;
      }
    }

    const auto replacedVal = emscripten_atomic_cas_u32(
        &sync_newPlayingTimeline, pendingTdrPtrVal, tdrPtrVal);
    if (replacedVal == pendingTdrPtrVal) {
      // replaced successfully
      if (replacedVal != 0) {
        const auto replacedTdr =
            reinterpret_cast<TimelineDestroyRequest *>(replacedVal);
        // must delete the one we replaced
        delete replacedTdr;
      }
      break;
    }
  }
}

void DSPContext::flushDestroyRequests() {
  auto requests = timelineDestroyRequests.removeAll();
  while (requests != nullptr) {
    auto reqCopy = requests;
    requests = requests->next;
    delete reqCopy;
  }
}

void DSPContext::resolvePendingTimelineSwap() {
  // Check for a pending timeline swap
  const auto pendingTdrPtrVal =
      emscripten_atomic_load_u32(&sync_newPlayingTimeline);
  if (pendingTdrPtrVal != 0 &&
      pendingTdrPtrVal == emscripten_atomic_cas_u32(&sync_newPlayingTimeline,
                                                    pendingTdrPtrVal, 0)) {
    // successfully swapped
    const auto pendingTdr =
        reinterpret_cast<TimelineDestroyRequest *>(pendingTdrPtrVal);
    if (rt_playingTimeline != nullptr) {
      timelineDestroyRequests.add(rt_playingTimeline);
    }
    rt_playingTimeline = pendingTdr;
  }
}

bool DSPContext::run(int numInputs, const AudioSampleFrame *inputs,
                     int numOutputs, AudioSampleFrame *outputs) {
  assert(!isOffline);

  resolvePendingTimelineSwap();

  assert(numOutputs == 1);
  auto &output = outputs[0];
  auto outAsBuffer = BufferF32::fromVLA(output.numberOfChannels,
                                        output.samplesPerChannel, output.data);

  if (rt_playingTimeline != nullptr) {
    const auto &timeline = rt_playingTimeline->timeline;
    if (timeline != nullptr) {
      timeline->read(outAsBuffer);
    }
    return true;
  }

  outAsBuffer.fill(0.0);
  return true;
}

std::shared_ptr<Bouncer> DSPContext::createBouncer(double start, double end,
                                                   int bufferSize) {
  assert(isOffline);
  resolvePendingTimelineSwap();

  if (rt_playingTimeline == nullptr) {
    return nullptr;
  }

  const auto &timeline = rt_playingTimeline->timeline;
  if (timeline == nullptr) {
    return nullptr;
  }

  return std::make_shared<Bouncer>(timeline, start, end, channelCount,
                                   bufferSize);
}

std::shared_ptr<SumOnlyBouncer>
DSPContext::createSumOnlyBouncer(int bufferSize) {
  assert(isOffline);

  return std::make_shared<SumOnlyBouncer>(bufferSize, channelCount, sampleRate);
}

void DSPContext::setLimiterParams(float lookaheadSeconds, float attackSeconds,
                                  float releaseSeconds, float preGainDb,
                                  bool bypass, float stereoLink) {
  assert(limiter != nullptr);
  limiter->setParameters(lookaheadSeconds, attackSeconds, releaseSeconds,
                         preGainDb, bypass, stereoLink);
}

Bouncer::Bouncer(const std::shared_ptr<Timeline> &timeline, double start,
                 double end, int channelCount, int bufferSize)
    : timeline(timeline), loopGuard(timeline), end(end) {
  timeline->setPlaying(true);
  timeline->setPosition(start);

  outputBuffer = std::make_shared<BufferF32>(channelCount, bufferSize);
}

std::shared_ptr<BufferF32> Bouncer::bounceNext() {
  if (timeline->getPosition() >= end) {
    return nullptr;
  }

  outputBuffer->fill(0.0);
  timeline->read(*outputBuffer);
  return outputBuffer;
}

SumOnlyBouncer::SumOnlyBouncer(int bufferSize, int channelCount, int sampleRate)
    : mixdownBuffer(std::make_shared<BufferF32>(channelCount, bufferSize)),
      outputBuffer(std::make_shared<BufferF32>(channelCount, bufferSize)),
      limiter(make_project_limiter(channelCount, sampleRate)),
      framesProduced(0), tailFramesProduced(0) {
  mixdownBuffer->fill(0.0);
}

void SumOnlyBouncer::sumIntoMixdownBuffer(
    const std::shared_ptr<BufferF32> &buffer) {
  assert(buffer->getChannelCount() == mixdownBuffer->getChannelCount());
  assert(buffer->getFrameCount() == mixdownBuffer->getFrameCount());
  mixdownBuffer->sumWith(*buffer);
}

std::shared_ptr<BufferF32> SumOnlyBouncer::bounceNext(bool inputFinished) {
  const auto delay = limiter ? limiter->getDelayFrames() : 0;

  if (limiter) {
    limiter->process(*mixdownBuffer);
  }

  framesProduced += mixdownBuffer->getFrameCount();

  std::shared_ptr<BufferF32> returnBuffer;

  if (tailFramesProduced > 0) {
    assert(inputFinished);
  }

  if (inputFinished) {
    const auto framesLeft = std::max(delay - (int)tailFramesProduced, 0);
    const auto framesToProduce =
        std::min(framesLeft, (int)mixdownBuffer->getFrameCount());
    tailFramesProduced += framesToProduce;

    returnBuffer =
        std::make_shared<BufferF32>(outputBuffer->slice(0, framesToProduce));
    returnBuffer->set(0, mixdownBuffer->slice(0, framesToProduce));
  } else {
    const auto framesToOutput =
        std::min(std::max((int)framesProduced - delay, 0),
                 (int)mixdownBuffer->getFrameCount());

    returnBuffer =
        std::make_shared<BufferF32>(outputBuffer->slice(0, framesToOutput));

    if (framesToOutput > 0) {
      returnBuffer->set(0, mixdownBuffer->slice(mixdownBuffer->getFrameCount() -
                                                framesToOutput));
    }
  }

  mixdownBuffer->fill(0.0);

  return returnBuffer;
}

// this is a little roundabout
EM_JS(emscripten::EM_VAL, get_audio_context_as_val, (int context_id),
      { return Emval.toHandle(emscriptenGetAudioObject(context_id)); });

AudioContextValType DSPContext::getAudioContext() {
  assert(!isOffline);
  return emscripten::val::take_ownership(get_audio_context_as_val(audioContext))
      .as<AudioContextValType>();
}

EMSCRIPTEN_BINDINGS(dspcontext) {
  using namespace emscripten;

  register_type<DSPContextPromise>("Promise<DSPContext>");
  register_type<AudioContextValType>("AudioContext");
  register_optional<int>();

  class_<Bouncer>("Bouncer")
      .smart_ptr<std::shared_ptr<Bouncer>>("Bouncer")
      .function("bounceNext", &Bouncer::bounceNext);

  class_<SumOnlyBouncer>("SumOnlyBouncer")
      .smart_ptr<std::shared_ptr<SumOnlyBouncer>>("SumOnlyBouncer")
      .function("sumIntoMixdownBuffer", &SumOnlyBouncer::sumIntoMixdownBuffer)
      .function("bounceNext", &SumOnlyBouncer::bounceNext);

  class_<DSPContext>("DSPContext")
      .smart_ptr<std::shared_ptr<DSPContext>>("DSPContext")
      .class_function("create(name, vuAlpha, analyticsObserver, channelCount)",
                      &DSPContext::create)
      .class_function("createOffline(name, vuAlpha, channelCount, sampleRate, "
                      "enableLimiter)",
                      &DSPContext::createOffline)
      .function("createTimeline(timing, tracks)", &DSPContext::createTimeline,
                nonnull<ret_val>())
      .function("createTrack(gain, pan, muted, meter, filterChain, clips)",
                &DSPContext::createTrack, nonnull<ret_val>())
      .function(
          "createAudioClip(underlyingReadable, warpMap, gain, "
          "timelineStartBeats, timelineEndBeats, loopStartBeats, loopEndBeats, "
          "readStartBeats, fadeInBeats, fadeInExponent, fadeOutBeats, "
          "fadeOutExponent, transposition, warpedContentBps, warpEnabled, "
          "arrangementId)",
          &DSPContext::createAudioClip, nonnull<ret_val>())
      .function("createMeter", &DSPContext::createMeter, nonnull<ret_val>())
      .function("createFilterChain(filterCount)",
                &DSPContext::createFilterChain, nonnull<ret_val>())
      .function("swapLiveTimeline(newLiveTimeline)",
                &DSPContext::swapLiveTimeline)
      .function("createBouncer(start, end, bufferSize)",
                &DSPContext::createBouncer, nonnull<ret_val>())
      .function("createSumOnlyBouncer(bufferSize)",
                &DSPContext::createSumOnlyBouncer, nonnull<ret_val>())
      .function("getAudioContext", &DSPContext::getAudioContext)
      .function("setLimiterParams(lookaheadSeconds, attackSeconds, "
                "releaseSeconds, preGainDb, bypass, stereoLink)",
                &DSPContext::setLimiterParams)
      .property("meter", &DSPContext::meter)
      .property("analyticsObserver", &DSPContext::analyticsObserver)
      .property("mipMapWorker", &DSPContext::mipMapWorker);
}

TEST_CASE("dspcontext scheduling tests", "[dspcontext]") {
  auto dsp = std::make_shared<DSPContext>(1.0, 2, 44100, true, true, nullptr);

  // Helper function to create test AudioClip with specific timeline start/end
  // beats
  auto createTestClip = [](double startBeats,
                           double endBeats) -> std::shared_ptr<AudioClip> {
    auto buf = std::make_shared<BufferF32>(2, 1024);
    buf->noise();
    auto rab = std::make_shared<BufferAsRemoteAudioBuffer>(44100, buf);
    auto warpMap = std::make_shared<time_transform::WarpMap<double>>(
        std::vector<time_transform::WarpMap<double>::WarpMarker>{});

    return std::make_shared<AudioClip>(rab, warpMap, 44100, 1.0f, startBeats,
                                       endBeats, 0.0, endBeats, 0.0, 0.0, 0.0,
                                       0.0, 0.0, 0.0f, 120.0, false, Uuid());
  };

  SECTION("Empty input") {
    std::vector<std::shared_ptr<AudioClip>> emptyClips;
    auto result = dsp->assignClipsToGroups(emptyClips);
    REQUIRE(result.empty());
  }

  SECTION("Single clip") {
    std::vector<std::shared_ptr<AudioClip>> clips;
    clips.push_back(createTestClip(0.0, 4.0));

    auto result = dsp->assignClipsToGroups(clips);
    REQUIRE(result.size() == 1);
    REQUIRE(result[0].size() == 1);
    REQUIRE(result[0][0] == clips[0]);
  }

  SECTION("Non-overlapping clips in order") {
    std::vector<std::shared_ptr<AudioClip>> clips;
    clips.push_back(createTestClip(0.0, 2.0)); // [0-2]
    clips.push_back(createTestClip(3.0, 5.0)); // [3-5]
    clips.push_back(createTestClip(6.0, 8.0)); // [6-8]

    auto result = dsp->assignClipsToGroups(clips);
    REQUIRE(result.size() == 1); // All should be in same group
    REQUIRE(result[0].size() == 3);
    // Check that all clips are present
    std::set<std::shared_ptr<AudioClip>> resultSet(result[0].begin(),
                                                   result[0].end());
    std::set<std::shared_ptr<AudioClip>> inputSet(clips.begin(), clips.end());
    REQUIRE(resultSet == inputSet);
  }

  SECTION("Non-overlapping clips out of order") {
    std::vector<std::shared_ptr<AudioClip>> clips;
    clips.push_back(createTestClip(6.0, 8.0)); // [6-8]
    clips.push_back(createTestClip(0.0, 2.0)); // [0-2]
    clips.push_back(createTestClip(3.0, 5.0)); // [3-5]

    auto result = dsp->assignClipsToGroups(clips);
    REQUIRE(result.size() == 1); // All should be in same group
    REQUIRE(result[0].size() == 3);
    // Check that all clips are present
    std::set<std::shared_ptr<AudioClip>> resultSet(result[0].begin(),
                                                   result[0].end());
    std::set<std::shared_ptr<AudioClip>> inputSet(clips.begin(), clips.end());
    REQUIRE(resultSet == inputSet);
  }

  SECTION("Overlapping clips - simple case") {
    std::vector<std::shared_ptr<AudioClip>> clips;
    auto clip1 = createTestClip(0.0, 3.0); // [0-3]
    auto clip2 = createTestClip(2.0, 5.0); // [2-5] - overlaps with clip1
    clips.push_back(clip1);
    clips.push_back(clip2);

    auto result = dsp->assignClipsToGroups(clips);
    REQUIRE(result.size() == 2); // Should be in separate groups

    // Each group should have one clip
    REQUIRE(result[0].size() == 1);
    REQUIRE(result[1].size() == 1);

    // Check that both clips are present in different groups
    std::set<std::shared_ptr<AudioClip>> allResultClips;
    for (const auto &group : result) {
      for (const auto &clip : group) {
        allResultClips.insert(clip);
      }
    }
    REQUIRE(allResultClips.size() == 2);
    REQUIRE(allResultClips.count(clip1) == 1);
    REQUIRE(allResultClips.count(clip2) == 1);
  }

  SECTION("Complex overlapping pattern") {
    std::vector<std::shared_ptr<AudioClip>> clips;
    auto clip1 = createTestClip(0.0, 2.0);  // [0-2]
    auto clip2 = createTestClip(1.0, 3.0);  // [1-3] - overlaps with clip1
    auto clip3 = createTestClip(4.0, 6.0);  // [4-6] - no overlap
    auto clip4 = createTestClip(5.0, 7.0);  // [5-7] - overlaps with clip3
    auto clip5 = createTestClip(8.0, 10.0); // [8-10] - no overlap with any

    clips.push_back(clip1);
    clips.push_back(clip2);
    clips.push_back(clip3);
    clips.push_back(clip4);
    clips.push_back(clip5);

    auto result = dsp->assignClipsToGroups(clips);

    // Expected groups:
    // Group 0: clip1, clip3 (or clip4), clip5
    // Group 1: clip2, clip4 (or clip3)
    // The algorithm should produce at most 2 groups for this pattern
    REQUIRE(result.size() <= 2);

    // Check that all clips are assigned
    std::set<std::shared_ptr<AudioClip>> allResultClips;
    for (const auto &group : result) {
      for (const auto &clip : group) {
        allResultClips.insert(clip);
      }
    }
    REQUIRE(allResultClips.size() == 5);

    // Verify no conflicts within groups
    for (const auto &group : result) {
      for (size_t i = 0; i < group.size(); i++) {
        for (size_t j = i + 1; j < group.size(); j++) {
          double start1 = group[i]->getTimelineStartBeats();
          double end1 = group[i]->getTimelineEndBeats();
          double start2 = group[j]->getTimelineStartBeats();
          double end2 = group[j]->getTimelineEndBeats();

          // Clips should not overlap: end1 <= start2 OR end2 <= start1
          REQUIRE((end1 <= start2 || end2 <= start1));
        }
      }
    }
  }

  SECTION("Adjacent clips (touching boundaries)") {
    std::vector<std::shared_ptr<AudioClip>> clips;
    clips.push_back(createTestClip(0.0, 2.0)); // [0-2]
    clips.push_back(
        createTestClip(2.0, 4.0)); // [2-4] - starts exactly where first ends
    clips.push_back(
        createTestClip(4.0, 6.0)); // [4-6] - starts exactly where second ends

    auto result = dsp->assignClipsToGroups(clips);
    REQUIRE(result.size() == 1); // Should all be in same group (no overlap)
    REQUIRE(result[0].size() == 3);
  }

  SECTION("Same start and end times") {
    std::vector<std::shared_ptr<AudioClip>> clips;
    clips.push_back(createTestClip(1.0, 3.0)); // [1-3]
    clips.push_back(createTestClip(1.0, 3.0)); // [1-3] - identical
    clips.push_back(createTestClip(1.0, 2.0)); // [1-2] - overlaps both

    auto result = dsp->assignClipsToGroups(clips);
    REQUIRE(result.size() ==
            3); // All must be in separate groups due to overlaps

    // Each group should have exactly one clip
    for (const auto &group : result) {
      REQUIRE(group.size() == 1);
    }
  }

  SECTION("Zero-length clips") {
    std::vector<std::shared_ptr<AudioClip>> clips;
    clips.push_back(createTestClip(1.0, 1.0)); // [1-1] - zero length
    clips.push_back(createTestClip(2.0, 2.0)); // [2-2] - zero length
    clips.push_back(createTestClip(1.0, 3.0)); // [1-3] - overlaps first

    auto result = dsp->assignClipsToGroups(clips);
    REQUIRE(result.size() ==
            2); // First and third should be in different groups

    // Verify no conflicts within groups
    for (const auto &group : result) {
      for (size_t i = 0; i < group.size(); i++) {
        for (size_t j = i + 1; j < group.size(); j++) {
          double start1 = group[i]->getTimelineStartBeats();
          double end1 = group[i]->getTimelineEndBeats();
          double start2 = group[j]->getTimelineStartBeats();
          double end2 = group[j]->getTimelineEndBeats();

          // Clips should not overlap: end1 <= start2 OR end2 <= start1
          REQUIRE((end1 <= start2 || end2 <= start1));
        }
      }
    }
  }
}

TEST_CASE("resource assignment tests", "[dspcontext]") {
  auto dsp = std::make_shared<DSPContext>(1.0f, 2, 44100, true, true, nullptr);

  // Create some shared audio buffers with different properties
  auto buffer44k_2ch = std::make_shared<BufferF32>(2, 1024);
  buffer44k_2ch->noise();
  auto readable44k_2ch =
      std::make_shared<BufferAsRemoteAudioBuffer>(44100, buffer44k_2ch);

  auto buffer48k_2ch = std::make_shared<BufferF32>(2, 1024);
  buffer48k_2ch->noise();
  auto readable48k_2ch =
      std::make_shared<BufferAsRemoteAudioBuffer>(48000, buffer48k_2ch);

  auto buffer44k_1ch = std::make_shared<BufferF32>(1, 1024);
  buffer44k_1ch->noise();
  auto readable44k_1ch =
      std::make_shared<BufferAsRemoteAudioBuffer>(44100, buffer44k_1ch);

  // Helper function to create AudioClip instances
  auto createClipWithReadable =
      [](std::shared_ptr<RandomAccessAudioReadable> readable, double startBeats,
         double endBeats) -> std::shared_ptr<AudioClip> {
    auto warpMap = std::make_shared<time_transform::WarpMap<double>>(
        std::vector<time_transform::WarpMap<double>::WarpMarker>{});

    return std::make_shared<AudioClip>(
        readable, warpMap, 44100, 1.0f, startBeats, endBeats, 0.0, endBeats,
        0.0, 0.0, 0.0, 0.0, 0.0, 0.0f, 120.0, false, Uuid());
  };

  SECTION("Resource assignment with shared and unique readables") {
    std::vector<std::shared_ptr<AudioClip>> clips;

    // Create clips that share the same underlying readable
    clips.push_back(createClipWithReadable(readable44k_2ch, 0.0,
                                           2.0)); // Group 0: 44.1kHz 2ch
    clips.push_back(createClipWithReadable(readable44k_2ch->clone(), 4.0,
                                           6.0)); // Group 0: shared readable

    // Create clips with different sample rates and channel counts
    clips.push_back(createClipWithReadable(readable48k_2ch, 1.0,
                                           3.0)); // Group 1: 48kHz 2ch
    clips.push_back(createClipWithReadable(readable44k_1ch, 2.5,
                                           4.5)); // Group 1: 44.1kHz 1ch

    // Create another clip sharing the first readable (different group due to
    // overlap)
    clips.push_back(
        createClipWithReadable(readable44k_2ch->clone(), 1.5, 3.5)); // Group 2

    // Create tracks with the clips
    auto meter = dsp->createMeter();
    auto filterChain = dsp->createFilterChain(1);
    std::vector<std::shared_ptr<Track>> tracks;

    // Split clips across multiple tracks to test track-level organization
    tracks.push_back(dsp->createTrack(1.0f, 0.0f, false, meter, filterChain,
                                      {clips[0], clips[1]}));
    tracks.push_back(dsp->createTrack(1.0f, 0.0f, false, meter, filterChain,
                                      {clips[2], clips[3], clips[4]}));

    // Create timing
    auto timing = std::make_shared<time_transform::TimelineTempoMap>(
        2.0, std::vector<time_transform::TimelineTempoMap::LinearSegment>{});

    // Call createTimeline - this should assign backends to all clips
    auto timeline = dsp->createTimeline(timing, tracks);

    REQUIRE(timeline != nullptr);

    // Verify that all clips have backends assigned
    for (auto &clip : clips) {
      // Check that each clip now has a backend
      // We can't directly access the backend from the public interface,
      // so we'll check by attempting to call readSegment which requires a
      // backend

      // Create a test render context
      auto testTiming = std::make_shared<time_transform::TimelineTempoMap>(
          2.0, std::vector<time_transform::TimelineTempoMap::LinearSegment>{});
      RenderContext renderContext(
          time_units::Beats<double>(0.0), // position
          2.0,                            // bps
          true,                           // isContinuous
          *testTiming,                    // globalBeatsToSeconds
          nullptr,                        // analyticsObserver
          time_units::BeatsDelta<double>(512.0 / 44100.0 * 2.0) // duration
      );

      // Try to read from the clip - this should not crash if backend is
      // properly assigned
      BufferF32 testOutput(
          clip->getOriginalUnderlyingReadable()->getChannelCount(), 512);
      clip->readSegment(&renderContext, testOutput);
      // If we get here without crashing, the backend was properly assigned
    }

    // Test that clips with the same underlying readable properties can share
    // resources This is implementation-specific and may vary, but we can at
    // least ensure no exceptions were thrown during timeline creation
    REQUIRE(true); // Timeline creation succeeded without exceptions

    // Additional verification could include checking that:
    // - Clips with same sample rate/channel count share timestretch readers
    // (within groups)
    // - Clips with same channel count share resamplers (within groups)
    // - Clips sharing the same original readable get cloned readables when in
    // different groups However, these details are internal to the
    // implementation
  }

  SECTION("Resource assignment with overlapping clips") {
    std::vector<std::shared_ptr<AudioClip>> clips;

    // Create overlapping clips that should be in different groups
    clips.push_back(
        createClipWithReadable(readable44k_2ch, 0.0, 4.0)); // Group 0
    clips.push_back(createClipWithReadable(readable44k_2ch->clone(), 2.0,
                                           6.0)); // Group 1 (overlaps)
    clips.push_back(createClipWithReadable(readable48k_2ch, 1.0,
                                           3.0)); // Group 2 (overlaps both)

    auto meter = dsp->createMeter();
    auto filterChain = dsp->createFilterChain(1);
    std::vector<std::shared_ptr<Track>> tracks;
    tracks.push_back(
        dsp->createTrack(1.0f, 0.0f, false, meter, nullptr, clips));

    auto timing = std::make_shared<time_transform::TimelineTempoMap>(
        2.0, std::vector<time_transform::TimelineTempoMap::LinearSegment>{});

    // This should succeed and properly assign resources to overlapping clips
    auto timeline = dsp->createTimeline(timing, tracks);

    REQUIRE(timeline != nullptr);

    // Verify all clips can be read from
    auto testTiming2 = std::make_shared<time_transform::TimelineTempoMap>(
        2.0, std::vector<time_transform::TimelineTempoMap::LinearSegment>{});
    RenderContext renderContext(
        time_units::Beats<double>(0.0), // position
        2.0,                            // bps
        true,                           // isContinuous
        *testTiming2,                   // globalBeatsToSeconds
        nullptr,                        // analyticsObserver
        time_units::BeatsDelta<double>(512.0 / 44100.0 * 2.0) // duration
    );

    for (auto &clip : clips) {
      BufferF32 testOutput(
          clip->getOriginalUnderlyingReadable()->getChannelCount(), 512);
      clip->readSegment(&renderContext, testOutput);
      // Successful read indicates proper backend assignment
    }
  }

  SECTION("Resource assignment with mixed sample rates and channel counts") {
    std::vector<std::shared_ptr<AudioClip>> clips;

    // Create non-overlapping clips with different audio properties
    clips.push_back(
        createClipWithReadable(readable44k_1ch, 0.0, 2.0)); // 44.1kHz 1ch
    clips.push_back(
        createClipWithReadable(readable44k_2ch, 2.0, 4.0)); // 44.1kHz 2ch
    clips.push_back(
        createClipWithReadable(readable48k_2ch, 4.0, 6.0)); // 48kHz 2ch

    // Should all be assignable to the same group since they don't overlap
    auto meter = dsp->createMeter();
    std::vector<std::shared_ptr<Track>> tracks;
    tracks.push_back(
        dsp->createTrack(1.0f, 0.0f, false, meter, nullptr, clips));

    auto timing = std::make_shared<time_transform::TimelineTempoMap>(
        2.0, std::vector<time_transform::TimelineTempoMap::LinearSegment>{});

    auto timeline = dsp->createTimeline(timing, tracks);
    REQUIRE(timeline != nullptr);

    // All clips should be readable
    auto testTiming3 = std::make_shared<time_transform::TimelineTempoMap>(
        2.0, std::vector<time_transform::TimelineTempoMap::LinearSegment>{});
    RenderContext renderContext(
        time_units::Beats<double>(0.0), // position
        2.0,                            // bps
        true,                           // isContinuous
        *testTiming3,                   // globalBeatsToSeconds
        nullptr,                        // analyticsObserver
        time_units::BeatsDelta<double>(512.0 / 44100.0 * 2.0) // duration
    );

    for (auto &clip : clips) {
      BufferF32 testOutput(
          clip->getOriginalUnderlyingReadable()->getChannelCount(), 512);
      clip->readSegment(&renderContext, testOutput);
      // Successful read indicates proper backend assignment
    }
  }
}

TEST_CASE("sum only bouncer tests", "[dspcontext]") {
  auto dsp = std::make_shared<DSPContext>(1.0f, 2, 44100, true, true, nullptr);
  const auto limiterDelay = make_project_limiter(2, 44100)->getDelayFrames();

  // check limiter is working
  {
    auto bigBuffer = std::make_shared<BufferF32>(2, 44100);
    for (int channel = 0; channel < 2; channel++) {
      auto channelData = bigBuffer->getChannelData(channel);
      for (int i = 0; i < 44100; i++) {
        channelData[i] = std::sin(i * 2.0f * M_PI / 44100.0f * 440.0f) * 10.0f;
      }
    }

    const auto bufferSize = 1024;

    auto sumOnlyBouncer = dsp->createSumOnlyBouncer(bufferSize);

    auto workBuffer = std::make_shared<BufferF32>(2, bufferSize);

    int readPos = 0;
    int checkedFrames = 0;
    while (readPos + bufferSize < bigBuffer->getFrameCount()) {
      auto thisChunk = bigBuffer->slice(readPos, readPos + bufferSize);
      readPos += bufferSize;
      sumOnlyBouncer->sumIntoMixdownBuffer(
          std::make_shared<BufferF32>(thisChunk));
      auto result = sumOnlyBouncer->bounceNext(false);
      REQUIRE(result != nullptr);
      REQUIRE(result->getChannelCount() == 2);
      checkedFrames += result->getFrameCount();
      if (result->getFrameCount() == 0) {
        continue;
      }
      bool allUnderLimit = true;
      for (int channel = 0; channel < 2 && allUnderLimit; channel++) {
        auto channelData = result->getChannelData(channel);
        for (int i = 0; i < bufferSize; i++) {
          if (std::abs(channelData[i]) > 1.0f) {
            allUnderLimit = false;
            break;
          }
        }
      }
      REQUIRE(allUnderLimit);
    }
    REQUIRE(checkedFrames > 10000);
  }

  for (auto bufferSize : {123, 456, 17, 1024}) {
    auto sumOnlyBouncer = dsp->createSumOnlyBouncer(bufferSize);

    int overallFrames =
        2 * limiterDelay + bufferSize - (2 * limiterDelay) % bufferSize;
    BufferF32 overallInput(2, overallFrames), overallOutput(2, overallFrames);
    for (int channel = 0; channel < 2; channel++) {
      auto channelData = overallInput.getChannelData(channel);
      for (int i = 0; i < overallFrames; i++) {
        channelData[i] = std::sin(i * 2.0f * M_PI / 44100.0f * 440.0f) * 0.7f;
      }
    }
    overallOutput.fill(0.0f);
    int inputPosition = 0;
    int outputPosition = 0;

    REQUIRE(sumOnlyBouncer != nullptr);

    for (int i = 0; i < limiterDelay / bufferSize; i++) {
      sumOnlyBouncer->sumIntoMixdownBuffer(std::make_shared<BufferF32>(
          overallInput.slice(inputPosition, inputPosition + bufferSize)));
      inputPosition += bufferSize;
      auto result = sumOnlyBouncer->bounceNext(false);
      REQUIRE(result != nullptr);
      REQUIRE(result->getFrameCount() == 0);
      REQUIRE(result->getChannelCount() == 2);
    }

    sumOnlyBouncer->sumIntoMixdownBuffer(std::make_shared<BufferF32>(
        overallInput.slice(inputPosition, inputPosition + bufferSize)));
    inputPosition += bufferSize;
    auto result = sumOnlyBouncer->bounceNext(false);
    REQUIRE(result != nullptr);
    REQUIRE(result->getFrameCount() == bufferSize - limiterDelay % bufferSize);

    overallOutput
        .slice(outputPosition, outputPosition + result->getFrameCount())
        .set(0, *result);
    outputPosition += result->getFrameCount();

    while (inputPosition < overallFrames) {
      sumOnlyBouncer->sumIntoMixdownBuffer(std::make_shared<BufferF32>(
          overallInput.slice(inputPosition, inputPosition + bufferSize)));
      inputPosition += bufferSize;
      auto result = sumOnlyBouncer->bounceNext(false);
      REQUIRE(result != nullptr);
      REQUIRE(result->getFrameCount() == bufferSize);
      overallOutput
          .slice(outputPosition, outputPosition + result->getFrameCount())
          .set(0, *result);
      outputPosition += result->getFrameCount();
    }

    for (;;) {
      auto result = sumOnlyBouncer->bounceNext(true);
      REQUIRE(result != nullptr);
      if (result->getFrameCount() == 0) {
        break;
      }
      overallOutput
          .slice(outputPosition, outputPosition + result->getFrameCount())
          .set(0, *result);
      outputPosition += result->getFrameCount();
    }

    REQUIRE(outputPosition == overallFrames);

    for (int channel = 0; channel < 2; channel++) {
      auto inChannelData = overallInput.getChannelData(channel);
      auto outChannelData = overallOutput.getChannelData(channel);
      for (int i = 0; i < overallFrames; i++) {
        // TODO the limiter shouldn't be changing the signal *this* much...
        REQUIRE(std::abs(inChannelData[i] - outChannelData[i]) < 0.1f);
      }
    }
  }
}

TEST_CASE("offline context creation tests", "[dspcontext]") {
  SECTION("Mono offline context creation") {
    auto monoContext =
        std::make_shared<DSPContext>(1.0f, 1, 44100, true, false, nullptr);

    REQUIRE(monoContext != nullptr);
  }

  SECTION("Stereo offline context creation") {
    auto stereoContext =
        std::make_shared<DSPContext>(1.0f, 2, 48000, true, true, nullptr);

    REQUIRE(stereoContext != nullptr);
  }
}

TEST_CASE("bouncer tests", "[dspcontext]") {
  for (auto channelCount : {1, 2}) {
    DYNAMIC_SECTION("Bounce a single buffer, channel count: " << channelCount) {
      auto dsp = std::make_shared<DSPContext>(1.0f, channelCount, 44100, true,
                                              false, nullptr);

      auto buffer = std::make_shared<BufferF32>(channelCount, 4096);
      for (int channel = 0; channel < channelCount; channel++) {
        auto channelData = buffer->getChannelData(channel);
        for (int i = 0; i < 4096; i++) {
          channelData[i] = std::sin(i * 2.0f * M_PI / 44100.0f * 440.0f) * 0.5f;
        }
      }

      auto readable =
          std::make_shared<BufferAsRemoteAudioBuffer>(44100, buffer);

      auto warpMap = std::make_shared<time_transform::WarpMap<double>>(
          std::vector<time_transform::WarpMap<double>::WarpMarker>{});

      auto clip = std::make_shared<AudioClip>(
          readable, warpMap, 44100, 1.0f, 0.0, 4.0, 0.0, 4.0, 0.0, 0.0, 0.0,
          0.0, 0.0, 0.0f, 120.0, false, Uuid());

      auto meter = dsp->createMeter();
      std::vector<std::shared_ptr<AudioClip>> clips = {clip};
      auto track = dsp->createTrack(1.0f, 0.0f, false, meter, nullptr, clips);

      auto timing = std::make_shared<time_transform::TimelineTempoMap>(
          2.0, // bps (beats per second)
          std::vector<time_transform::TimelineTempoMap::LinearSegment>{});

      std::vector<std::shared_ptr<Track>> tracks = {track};
      auto timeline = dsp->createTimeline(timing, tracks);

      dsp->swapLiveTimeline(timeline);

      auto bouncer = dsp->createBouncer(0.0, 4.0, 1024);

      REQUIRE(bouncer != nullptr);

      int totalFramesBounced = 0;
      int iterationCount = 0;
      while (true) {
        auto result = bouncer->bounceNext();
        if (result == nullptr) {
          break;
        }

        REQUIRE(result->getChannelCount() == channelCount);
        REQUIRE(result->getFrameCount() <= 1024);

        bool hasNonZeroData = false;
        for (int channel = 0; channel < channelCount; channel++) {
          auto channelData = result->getChannelData(channel);
          for (int i = 0; i < result->getFrameCount(); i++) {
            if (std::abs(channelData[i]) > 0.001f) {
              hasNonZeroData = true;
              break;
            }
          }
          if (hasNonZeroData)
            break;
        }

        totalFramesBounced += result->getFrameCount();
        iterationCount++;
      }

      REQUIRE(totalFramesBounced > 80000);
      REQUIRE(totalFramesBounced < 100000);
      REQUIRE(iterationCount > 0);
    }
  }
}