#include "biquad.h"
#include "catch2/catch.hpp"
#include "constants.h"
#include "simd.h"
#include <algorithm>
#include <cassert>
#include <catch2/catch.hpp>
#include <cmath>
#include <emscripten/atomic.h>
#include <emscripten/bind.h>

static inline void lowpassCoefs(const int bufferSize,
                                const float *__restrict alpha,
                                const float *__restrict cosW0,
                                float *__restrict b0, float *__restrict b1,
                                float *__restrict b2, float *__restrict a1,
                                float *__restrict a2) {
#pragma clang loop vectorize(enable)
  for (int i = 0; i < bufferSize; i++) {
    const auto a0 = 1.f + alpha[i];
    b0[i] = (1.f - cosW0[i]) / (2.f * a0);
    b1[i] = (1.f - cosW0[i]) / a0;
    b2[i] = (1.f - cosW0[i]) / (2.f * a0);
    a1[i] = -2.f * cosW0[i] / a0;
    a2[i] = (1.f - alpha[i]) / a0;
  }
}

static inline void highpassCoefs(const int bufferSize,
                                 const float *__restrict alpha,
                                 const float *__restrict cosW0,
                                 float *__restrict b0, float *__restrict b1,
                                 float *__restrict b2, float *__restrict a1,
                                 float *__restrict a2) {
#pragma clang loop vectorize(enable)
  for (int i = 0; i < bufferSize; i++) {
    const auto a0 = 1.f + alpha[i];
    b0[i] = (1.f + cosW0[i]) / (2.f * a0);
    b1[i] = -(1.f + cosW0[i]) / a0;
    b2[i] = (1.f + cosW0[i]) / (2.f * a0);
    a1[i] = -2.f * cosW0[i] / a0;
    a2[i] = (1.f - alpha[i]) / a0;
  }
}

static inline void
bandpassCoefs(const int bufferSize, const float *__restrict q,
              const float *__restrict alpha, const float *__restrict cosW0,
              float *__restrict b0, float *__restrict b1, float *__restrict b2,
              float *__restrict a1, float *__restrict a2) {
#pragma clang loop vectorize(enable)
  for (int i = 0; i < bufferSize; i++) {
    const auto a0 = 1.f + alpha[i];
    b0[i] = q[i] * alpha[i] / a0;
    b1[i] = 0.f;
    b2[i] = -q[i] * alpha[i] / a0;
    a1[i] = -2.f * cosW0[i] / a0;
    a2[i] = (1.f - alpha[i]) / a0;
  }
}

static inline void notchCoefs(const int bufferSize,
                              const float *__restrict alpha,
                              const float *__restrict cosW0,
                              float *__restrict b0, float *__restrict b1,
                              float *__restrict b2, float *__restrict a1,
                              float *__restrict a2) {
#pragma clang loop vectorize(enable)
  for (int i = 0; i < bufferSize; i++) {
    const auto a0 = 1.f + alpha[i];
    b0[i] = 1.f / a0;
    b1[i] = -2.f * cosW0[i] / a0;
    b2[i] = 1.f / a0;
    a1[i] = -2.f * cosW0[i] / a0;
    a2[i] = (1.f - alpha[i]) / a0;
  }
}

static inline void allpassCoefs(const int bufferSize,
                                const float *__restrict alpha,
                                const float *__restrict cosW0,
                                float *__restrict b0, float *__restrict b1,
                                float *__restrict b2, float *__restrict a1,
                                float *__restrict a2) {
#pragma clang loop vectorize(enable)
  for (int i = 0; i < bufferSize; i++) {
    const auto a0 = 1.f + alpha[i];
    b0[i] = (1.f - alpha[i]) / a0;
    b1[i] = -2.f * cosW0[i] / a0;
    b2[i] = (1.f + alpha[i]) / a0;
    a1[i] = -2.f * cosW0[i] / a0;
    a2[i] = (1.f - alpha[i]) / a0;
  }
}

static inline void
eqBandCoefs(const int bufferSize, const float *__restrict dbGain,
            const float *__restrict alpha, const float *__restrict cosW0,
            float *__restrict b0, float *__restrict b1, float *__restrict b2,
            float *__restrict a1, float *__restrict a2) {
  // A = 10^(dbGain / 40)
  float A[bufferSize];
#pragma clang loop vectorize(enable)
  for (int i = 0; i < bufferSize; i++) {
    A[i] = dbGain[i] / 40.f;
  }

  vfastpow10_array(A, A, bufferSize);

#pragma clang loop vectorize(enable)
  for (int i = 0; i < bufferSize; i++) {
    const auto a0 = 1.f + alpha[i] / A[i];
    b0[i] = (1.f + alpha[i] * A[i]) / a0;
    b1[i] = -2.f * cosW0[i] / a0;
    b2[i] = (1.f - alpha[i] * A[i]) / a0;
    a1[i] = -2.f * cosW0[i] / a0;
    a2[i] = (1.f - alpha[i] / A[i]) / a0;
  }
}

static inline void
lowShelfCoefs(const int bufferSize, const float *__restrict dbGain,
              const float *__restrict alpha, const float *__restrict cosW0,
              float *__restrict b0, float *__restrict b1, float *__restrict b2,
              float *__restrict a1, float *__restrict a2) {
  // A = 10^(dbGain / 40)
  float A[bufferSize];
  float sqrtA[bufferSize];

#pragma clang loop vectorize(enable)
  for (int i = 0; i < bufferSize; i++) {
    A[i] = dbGain[i] / 40.f;
  }

  vfastpow10_array(A, A, bufferSize);
  vfastnanzero_array(A, A, bufferSize);
  vfastclamp_array(A, 1e-6f, 1e6f, A, bufferSize);
  vfastsqrt_array(A, sqrtA, bufferSize);

#pragma clang loop vectorize(enable)
  for (int i = 0; i < bufferSize; i++) {
    const auto a0 =
        A[i] + 1.f + (A[i] - 1.f) * cosW0[i] + 2.f * sqrtA[i] * alpha[i];
    b0[i] = A[i] *
            (A[i] + 1.f - (A[i] - 1.f) * cosW0[i] + 2.f * sqrtA[i] * alpha[i]) /
            a0;
    b1[i] = 2.f * A[i] * (A[i] - 1.f - (A[i] + 1.f) * cosW0[i]) / a0;
    b2[i] = A[i] *
            (A[i] + 1.f - (A[i] - 1.f) * cosW0[i] - 2.f * sqrtA[i] * alpha[i]) /
            a0;
    a1[i] = -2.f * (A[i] - 1.f + (A[i] + 1.f) * cosW0[i]) / a0;
    a2[i] =
        (A[i] + 1.f + (A[i] - 1.f) * cosW0[i] - 2.f * sqrtA[i] * alpha[i]) / a0;
  }
}

static inline void
highShelfCoefs(const int bufferSize, const float *__restrict dbGain,
               const float *__restrict alpha, const float *__restrict cosW0,
               float *__restrict b0, float *__restrict b1, float *__restrict b2,
               float *__restrict a1, float *__restrict a2) {
  // A = 10^(dbGain / 40)
  float A[bufferSize];
  float sqrtA[bufferSize];

#pragma clang loop vectorize(enable)
  for (int i = 0; i < bufferSize; i++) {
    A[i] = dbGain[i] / 40.f;
  }

  vfastpow10_array(A, A, bufferSize);
  vfastnanzero_array(A, A, bufferSize);
  vfastclamp_array(A, 1e-6f, 1e6f, A, bufferSize);
  vfastsqrt_array(A, sqrtA, bufferSize);

#pragma clang loop vectorize(enable)
  for (int i = 0; i < bufferSize; i++) {
    const auto a0 =
        A[i] + 1.f - (A[i] - 1.f) * cosW0[i] + 2.f * sqrtA[i] * alpha[i];
    b0[i] = A[i] *
            (A[i] + 1.f + (A[i] - 1.f) * cosW0[i] + 2.f * sqrtA[i] * alpha[i]) /
            a0;
    b1[i] = -2.f * A[i] * (A[i] - 1.f + (A[i] + 1.f) * cosW0[i]) / a0;
    b2[i] = A[i] *
            (A[i] + 1.f + (A[i] - 1.f) * cosW0[i] - 2.f * sqrtA[i] * alpha[i]) /
            a0;
    a1[i] = 2.f * (A[i] - 1.f - (A[i] + 1.f) * cosW0[i]) / a0;
    a2[i] =
        (A[i] + 1.f - (A[i] - 1.f) * cosW0[i] - 2.f * sqrtA[i] * alpha[i]) / a0;
  }
}

BiQuadFilter::BiQuadFilter(int sampleRate)
    : sampleRate(sampleRate), nyquistLimit((float)sampleRate / 2.f),
      w0Coef(2.f * M_PI / (float)sampleRate), xn2(0.f), xn1(0.f), yn2(0.f),
      yn1(0.f) {}

void BiQuadFilter::processBuffer(BufferF32 frequencyInput,
                                 BufferF32 dbGainInput, BufferF32 qInput,
                                 BufferF32 input, BufferF32 output, Mode mode) {
  if (mode == Mode::BYPASS) {
    if (!input.isSameStorage(output)) {
      output.set(0, input);
    }
    reset();
    return;
  }

  const auto bufferSize = input.getFrameCount();
  assert(frequencyInput.getFrameCount() == bufferSize);
  assert(dbGainInput.getFrameCount() == bufferSize);
  assert(qInput.getFrameCount() == bufferSize);
  assert(output.getFrameCount() == bufferSize);
  assert(frequencyInput.getChannelCount() == 1);
  assert(dbGainInput.getChannelCount() == 1);
  assert(qInput.getChannelCount() == 1);
  assert(input.getChannelCount() == 1);
  assert(output.getChannelCount() == 1);

  const auto frequencyData = frequencyInput.getChannelData(0);
  const auto dbGainData = dbGainInput.getChannelData(0);
  const auto qData = qInput.getChannelData(0);
  const auto inputData = input.getChannelData(0);
  auto outputData = output.getChannelData(0);

  float w0[MAX_BUFFER_SIZE];
#pragma clang loop vectorize(enable)
  for (int i = 0; i < bufferSize; i++) {
    // 0 <= w0 <= pi
    w0[i] = w0Coef * std::clamp(frequencyData[i], 20.f, nyquistLimit);
  }

  float alpha[MAX_BUFFER_SIZE];
  vfastsin_0topi_array(w0, alpha, bufferSize);
#pragma clang loop vectorize(enable)
  for (int i = 0; i < bufferSize; i++) {
    alpha[i] /= (2.f * qData[i]);
  }
  vfastnanzero_array(alpha, alpha, bufferSize);

  float cosW0[MAX_BUFFER_SIZE];
  vfastcos_0topi_array(w0, cosW0, bufferSize);

  float b0[MAX_BUFFER_SIZE];
  float b1[MAX_BUFFER_SIZE];
  float b2[MAX_BUFFER_SIZE];
  float a1[MAX_BUFFER_SIZE];
  float a2[MAX_BUFFER_SIZE];

  switch (mode) {
  case Mode::LOWPASS:
    lowpassCoefs(bufferSize, alpha, cosW0, b0, b1, b2, a1, a2);
    break;
  case Mode::HIGHPASS:
    highpassCoefs(bufferSize, alpha, cosW0, b0, b1, b2, a1, a2);
    break;
  case Mode::BANDPASS:
    bandpassCoefs(bufferSize, qData, alpha, cosW0, b0, b1, b2, a1, a2);
    break;
  case Mode::NOTCH:
    notchCoefs(bufferSize, alpha, cosW0, b0, b1, b2, a1, a2);
    break;
  case Mode::ALLPASS:
    allpassCoefs(bufferSize, alpha, cosW0, b0, b1, b2, a1, a2);
    break;
  case Mode::EQ_BAND:
    eqBandCoefs(bufferSize, dbGainData, alpha, cosW0, b0, b1, b2, a1, a2);
    break;
  case Mode::LOW_SHELF:
    lowShelfCoefs(bufferSize, dbGainData, alpha, cosW0, b0, b1, b2, a1, a2);
    break;
  case Mode::HIGH_SHELF:
    highShelfCoefs(bufferSize, dbGainData, alpha, cosW0, b0, b1, b2, a1, a2);
    break;
  default:
    assert(false);
    break;
  }

  auto xn2temp = xn2;
  auto xn1temp = xn1;
  auto yn2temp = yn2;
  auto yn1temp = yn1;

  for (int i = 0; i < bufferSize; i++) {
    const auto x = inputData[i];
    const auto y = (b0[i] * x + b1[i] * xn1temp + b2[i] * xn2temp -
                    a1[i] * yn1temp - a2[i] * yn2temp);
    outputData[i] = y;
    xn2temp = xn1temp;
    xn1temp = x;
    yn2temp = yn1temp;
    yn1temp = y;
  }

  vfastclamp_nanzero_array(outputData, -10.f, 10.f, outputData, bufferSize);

  xn2 = xn2temp;
  xn1 = xn1temp;
  yn2 = yn2temp;
  yn1 = yn1temp;

  // recover from bad situations
  if (!std::isfinite(xn2) || !std::isfinite(xn1) || !std::isfinite(yn2) ||
      !std::isfinite(yn1)) {
    xn2 = xn1 = yn2 = yn1 = 0.f;
  }
}

void BiQuadFilter::processBufferConstant(float frequency, float dbGain, float q,
                                         BufferF32 input, BufferF32 output,
                                         Mode mode) {
  assert(!std::isnan(frequency));
  assert(!std::isnan(dbGain));
  assert(std::isfinite(q));

  if (mode == Mode::BYPASS) {
    if (!input.isSameStorage(output)) {
      output.set(0, input);
    }
    reset();
    return;
  }

  const auto bufferSize = input.getFrameCount();
  assert(bufferSize == output.getFrameCount());
  assert(input.getChannelCount() == 1);
  assert(output.getChannelCount() == 1);

  const auto inputData = input.getChannelData(0);
  auto outputData = output.getChannelData(0);

  float w0 = w0Coef * std::clamp(frequency, 20.f, nyquistLimit);
  float alpha = std::sin(w0) / (2.f * std::max(1e-6f, q));
  float cosW0 = std::cos(w0);

  float b0, b1, b2, a1, a2;

  switch (mode) {
  case Mode::LOWPASS:
    lowpassCoefs(1, &alpha, &cosW0, &b0, &b1, &b2, &a1, &a2);
    break;
  case Mode::HIGHPASS:
    highpassCoefs(1, &alpha, &cosW0, &b0, &b1, &b2, &a1, &a2);
    break;
  case Mode::BANDPASS:
    bandpassCoefs(1, &q, &alpha, &cosW0, &b0, &b1, &b2, &a1, &a2);
    break;
  case Mode::NOTCH:
    notchCoefs(1, &alpha, &cosW0, &b0, &b1, &b2, &a1, &a2);
    break;
  case Mode::ALLPASS:
    allpassCoefs(1, &alpha, &cosW0, &b0, &b1, &b2, &a1, &a2);
    break;
  case Mode::EQ_BAND:
    eqBandCoefs(1, &dbGain, &alpha, &cosW0, &b0, &b1, &b2, &a1, &a2);
    break;
  case Mode::LOW_SHELF:
    lowShelfCoefs(1, &dbGain, &alpha, &cosW0, &b0, &b1, &b2, &a1, &a2);
    break;
  case Mode::HIGH_SHELF:
    highShelfCoefs(1, &dbGain, &alpha, &cosW0, &b0, &b1, &b2, &a1, &a2);
    break;
  default:
    assert(false);
    break;
  }

  auto xn2temp = xn2;
  auto xn1temp = xn1;
  auto yn2temp = yn2;
  auto yn1temp = yn1;

  for (int i = 0; i < bufferSize; i++) {
    const auto x = inputData[i];
    const auto y =
        (b0 * x + b1 * xn1temp + b2 * xn2temp - a1 * yn1temp - a2 * yn2temp);
    outputData[i] = y;
    xn2temp = xn1temp;
    xn1temp = x;
    yn2temp = yn1temp;
    yn1temp = y;
  }

  vfastclamp_nanzero_array(outputData, -10.f, 10.f, outputData, bufferSize);

  xn2 = xn2temp;
  xn1 = xn1temp;
  yn2 = yn2temp;
  yn1 = yn1temp;
}

void BiQuadFilter::reset() { xn2 = xn1 = yn2 = yn1 = 0.f; }

FilterChain::FilterChain(int sampleRate, int channelCount, int filterCount)
    : channelCount(channelCount) {
  filterConfigs.resize(filterCount);
  for (int i = 0; i < filterCount; i++) {
    filterConfigs[i].filters.resize(channelCount, BiQuadFilter(sampleRate));
    filterConfigs[i].frequency = sampleRate / 2.f - 20.f;
    filterConfigs[i].dbGain = 0.f;
    filterConfigs[i].q = 0.707f;
    filterConfigs[i].mode = static_cast<uint32_t>(BiQuadFilter::Mode::BYPASS);
  }
}

void FilterChain::processBuffer(BufferF32 buf) {
  for (int i = 0; i < filterConfigs.size(); i++) {
    const auto mode = static_cast<BiQuadFilter::Mode>(
        emscripten_atomic_load_u32(&filterConfigs[i].mode));
    if (mode == BiQuadFilter::Mode::BYPASS) {
      for (int channel = 0; channel < channelCount; channel++) {
        filterConfigs[i].filters[channel].reset();
      }
      continue;
    }
    for (int channel = 0; channel < channelCount; channel++) {
      auto channelBuf = buf.sliceChannel(channel);
      filterConfigs[i].filters[channel].processBufferConstant(
          emscripten_atomic_load_f32(&filterConfigs[i].frequency),
          emscripten_atomic_load_f32(&filterConfigs[i].dbGain),
          emscripten_atomic_load_f32(&filterConfigs[i].q), channelBuf,
          channelBuf, mode);
    }
  }
}

int FilterChain::getFilterCount() const { return filterConfigs.size(); }

BiQuadFilter::Mode FilterChain::getMode(int idx) const {
  return static_cast<BiQuadFilter::Mode>(
      emscripten_atomic_load_u32(&filterConfigs[idx].mode));
}

void FilterChain::setMode(int idx, BiQuadFilter::Mode mode) {
  emscripten_atomic_store_u32(&filterConfigs[idx].mode,
                              static_cast<uint32_t>(mode));
}

float FilterChain::getFrequency(int idx) const {
  return emscripten_atomic_load_f32(&filterConfigs[idx].frequency);
}

void FilterChain::setFrequency(int idx, float frequency) {
  emscripten_atomic_store_f32(&filterConfigs[idx].frequency, frequency);
}

float FilterChain::getGain(int idx) const {
  return emscripten_atomic_load_f32(&filterConfigs[idx].dbGain);
}

void FilterChain::setGain(int idx, float dbGain) {
  emscripten_atomic_store_f32(&filterConfigs[idx].dbGain, dbGain);
}

float FilterChain::getQ(int idx) const {
  return emscripten_atomic_load_f32(&filterConfigs[idx].q);
}

void FilterChain::setQ(int idx, float q) {
  emscripten_atomic_store_f32(&filterConfigs[idx].q, q);
}

EMSCRIPTEN_BINDINGS(biquad) {
  using namespace emscripten;

  enum_<BiQuadFilter::Mode>("BiQuadFilterMode")
      .value("LOWPASS", BiQuadFilter::Mode::LOWPASS)
      .value("HIGHPASS", BiQuadFilter::Mode::HIGHPASS)
      .value("BANDPASS", BiQuadFilter::Mode::BANDPASS)
      .value("NOTCH", BiQuadFilter::Mode::NOTCH)
      .value("ALLPASS", BiQuadFilter::Mode::ALLPASS)
      .value("EQ_BAND", BiQuadFilter::Mode::EQ_BAND)
      .value("LOW_SHELF", BiQuadFilter::Mode::LOW_SHELF)
      .value("HIGH_SHELF", BiQuadFilter::Mode::HIGH_SHELF)
      .value("BYPASS", BiQuadFilter::Mode::BYPASS);

  class_<FilterChain>("FilterChain")
      .smart_ptr<std::shared_ptr<FilterChain>>("FilterChain")
      .property("filterCount", &FilterChain::getFilterCount)
      .function("getMode(index)", &FilterChain::getMode)
      .function("setMode(index, mode)", &FilterChain::setMode)
      .function("getFrequency(index)", &FilterChain::getFrequency)
      .function("setFrequency(index, frequency)", &FilterChain::setFrequency)
      .function("getGain(index)", &FilterChain::getGain)
      .function("setGain(index, dbGain)", &FilterChain::setGain)
      .function("getQ(index)", &FilterChain::getQ)
      .function("setQ(index, q)", &FilterChain::setQ);
};

TEST_CASE("Biquad filter", "[biquad]") {
  constexpr int BUFFER_SIZE = 1024;

  BufferF32 frequencyBuffer(1, BUFFER_SIZE);
  BufferF32 gainBuffer(1, BUFFER_SIZE);
  BufferF32 qBuffer(1, BUFFER_SIZE);
  BufferF32 inputBuffer(1, BUFFER_SIZE);
  BufferF32 outputBuffer(1, BUFFER_SIZE);

  frequencyBuffer.fill(440.f);
  gainBuffer.fill(0.f);
  qBuffer.fill(1.f);
  inputBuffer.noise();
  outputBuffer.fill(0.f);

  for (int mode = 0; mode < 8; mode++) {
    auto biQuadFilter = std::make_shared<BiQuadFilter>(44100);

    biQuadFilter->processBuffer(frequencyBuffer, gainBuffer, qBuffer,
                                inputBuffer, outputBuffer,
                                static_cast<BiQuadFilter::Mode>(mode));

    REQUIRE(!outputBuffer.hasNaN());
  }

  for (int mode = 0; mode < 8; mode++) {
    auto biQuadFilter = std::make_shared<BiQuadFilter>(44100);

    biQuadFilter->processBufferConstant(440.f, 1.f, 1.f, inputBuffer,
                                        outputBuffer,
                                        static_cast<BiQuadFilter::Mode>(mode));

    REQUIRE(!outputBuffer.hasNaN());
  }
}

TEST_CASE("FilterChain basic properties", "[filterchain]") {
  constexpr int SAMPLE_RATE = 44100;
  constexpr int CHANNEL_COUNT = 2;
  constexpr int FILTER_COUNT = 4;

  auto filterChain =
      std::make_shared<FilterChain>(SAMPLE_RATE, CHANNEL_COUNT, FILTER_COUNT);

  SECTION("Filter count is correct") {
    REQUIRE(filterChain->getFilterCount() == FILTER_COUNT);
  }

  SECTION("Default values are set correctly") {
    for (int i = 0; i < FILTER_COUNT; i++) {
      REQUIRE(filterChain->getMode(i) == BiQuadFilter::Mode::BYPASS);
      REQUIRE(filterChain->getFrequency(i) == Approx(SAMPLE_RATE / 2.f - 20.f));
      REQUIRE(filterChain->getGain(i) == Approx(0.f));
      REQUIRE(filterChain->getQ(i) == Approx(0.707f));
    }
  }
}

TEST_CASE("FilterChain setters and getters", "[filterchain]") {
  constexpr int SAMPLE_RATE = 44100;
  constexpr int CHANNEL_COUNT = 2;
  constexpr int FILTER_COUNT = 3;

  auto filterChain =
      std::make_shared<FilterChain>(SAMPLE_RATE, CHANNEL_COUNT, FILTER_COUNT);

  SECTION("Set and get mode") {
    filterChain->setMode(0, BiQuadFilter::Mode::HIGHPASS);
    filterChain->setMode(1, BiQuadFilter::Mode::BANDPASS);
    filterChain->setMode(2, BiQuadFilter::Mode::EQ_BAND);

    REQUIRE(filterChain->getMode(0) == BiQuadFilter::Mode::HIGHPASS);
    REQUIRE(filterChain->getMode(1) == BiQuadFilter::Mode::BANDPASS);
    REQUIRE(filterChain->getMode(2) == BiQuadFilter::Mode::EQ_BAND);
  }

  SECTION("Set and get frequency") {
    filterChain->setFrequency(0, 440.f);
    filterChain->setFrequency(1, 1000.f);
    filterChain->setFrequency(2, 5000.f);

    REQUIRE(filterChain->getFrequency(0) == Approx(440.f));
    REQUIRE(filterChain->getFrequency(1) == Approx(1000.f));
    REQUIRE(filterChain->getFrequency(2) == Approx(5000.f));
  }

  SECTION("Set and get gain") {
    filterChain->setGain(0, 0.5f);
    filterChain->setGain(1, 2.0f);
    filterChain->setGain(2, 0.75f);

    REQUIRE(filterChain->getGain(0) == Approx(0.5f));
    REQUIRE(filterChain->getGain(1) == Approx(2.0f));
    REQUIRE(filterChain->getGain(2) == Approx(0.75f));
  }

  SECTION("Set and get Q") {
    filterChain->setQ(0, 0.707f);
    filterChain->setQ(1, 2.0f);
    filterChain->setQ(2, 5.0f);

    REQUIRE(filterChain->getQ(0) == Approx(0.707f));
    REQUIRE(filterChain->getQ(1) == Approx(2.0f));
    REQUIRE(filterChain->getQ(2) == Approx(5.0f));
  }
}

TEST_CASE("FilterChain buffer processing", "[filterchain]") {
  constexpr int SAMPLE_RATE = 44100;
  constexpr int BUFFER_SIZE = 1024;

  SECTION("Single channel, single filter") {
    auto filterChain = std::make_shared<FilterChain>(SAMPLE_RATE, 1, 1);
    BufferF32 buffer(1, BUFFER_SIZE);
    buffer.noise();

    filterChain->setMode(0, BiQuadFilter::Mode::LOWPASS);
    filterChain->setFrequency(0, 1000.f);
    filterChain->setQ(0, 0.707f);

    filterChain->processBuffer(buffer);

    REQUIRE(!buffer.hasNaN());
  }

  SECTION("Stereo, multiple filters") {
    auto filterChain = std::make_shared<FilterChain>(SAMPLE_RATE, 2, 3);
    BufferF32 buffer(2, BUFFER_SIZE);
    buffer.noise();

    // Configure a typical EQ chain
    filterChain->setMode(0, BiQuadFilter::Mode::LOW_SHELF);
    filterChain->setFrequency(0, 100.f);
    filterChain->setGain(0, 3.f);
    filterChain->setQ(0, 0.707f);

    filterChain->setMode(1, BiQuadFilter::Mode::EQ_BAND);
    filterChain->setFrequency(1, 1000.f);
    filterChain->setGain(1, -2.f);
    filterChain->setQ(1, 1.5f);

    filterChain->setMode(2, BiQuadFilter::Mode::HIGH_SHELF);
    filterChain->setFrequency(2, 8000.f);
    filterChain->setGain(2, -20.f);
    filterChain->setQ(2, 0.707f);

    filterChain->processBuffer(buffer);

    REQUIRE(!buffer.hasNaN());
  }

  SECTION("All filter modes") {
    for (int mode = 0; mode < 8; mode++) {
      auto filterChain = std::make_shared<FilterChain>(SAMPLE_RATE, 1, 1);
      BufferF32 buffer(1, BUFFER_SIZE);
      buffer.noise();

      filterChain->setMode(0, static_cast<BiQuadFilter::Mode>(mode));
      filterChain->setFrequency(0, 440.f);
      filterChain->setGain(0, 1.5f);
      filterChain->setQ(0, 1.0f);

      filterChain->processBuffer(buffer);

      REQUIRE(!buffer.hasNaN());
    }
  }

  SECTION("Multiple passes through chain") {
    auto filterChain = std::make_shared<FilterChain>(SAMPLE_RATE, 1, 2);
    BufferF32 buffer(1, BUFFER_SIZE);
    buffer.noise();

    filterChain->setMode(0, BiQuadFilter::Mode::HIGHPASS);
    filterChain->setFrequency(0, 100.f);

    filterChain->setMode(1, BiQuadFilter::Mode::LOWPASS);
    filterChain->setFrequency(1, 5000.f);

    // Process multiple times to test state persistence
    for (int i = 0; i < 5; i++) {
      filterChain->processBuffer(buffer);
      REQUIRE(!buffer.hasNaN());
    }
  }

  SECTION("Many channels") {
    constexpr int MANY_CHANNELS = 8;
    auto filterChain =
        std::make_shared<FilterChain>(SAMPLE_RATE, MANY_CHANNELS, 2);
    BufferF32 buffer(MANY_CHANNELS, BUFFER_SIZE);
    buffer.noise();

    filterChain->setMode(0, BiQuadFilter::Mode::BANDPASS);
    filterChain->setFrequency(0, 1000.f);
    filterChain->setQ(0, 5.0f);

    filterChain->setMode(1, BiQuadFilter::Mode::NOTCH);
    filterChain->setFrequency(1, 2000.f);
    filterChain->setQ(1, 10.0f);

    filterChain->processBuffer(buffer);

    REQUIRE(!buffer.hasNaN());
  }

  SECTION("Bypass mode") {
    auto filterChain = std::make_shared<FilterChain>(SAMPLE_RATE, 1, 1);
    BufferF32 buffer(1, BUFFER_SIZE);
    BufferF32 checkBuffer(1, BUFFER_SIZE);
    buffer.noise();
    checkBuffer.set(0, buffer);

    filterChain->setMode(0, BiQuadFilter::Mode::BYPASS);
    filterChain->processBuffer(buffer);

    bool allSame = true;
    for (int i = 0; i < BUFFER_SIZE; i++) {
      if (buffer[0][i] != checkBuffer[0][i]) {
        allSame = false;
        break;
      }
    }
    REQUIRE(allSame);
  }
}