#include "buffer.h"
#include <catch2/catch.hpp>
#include <emscripten/bind.h>
#include <memory>

using namespace emscripten;

template <typename T> void register_buffer_bindings(const char *name) {
  class_<T>(name)
      .template smart_ptr_constructor<std::shared_ptr<T>, int &&, size_t &&>(
          name, &std::make_shared<T>)
      .class_function("fromArray(inputTypedArrays)", &T::fromArray,
                      nonnull<ret_val>())
      .function("fill(value)",
                select_overload<void(typename T::sampleType)>(&T::fill))
      .function(
          "fill(offset, count, value)",
          select_overload<void(int, size_t, typename T::sampleType)>(&T::fill))
      .function("noise", &T::noise)
      .function(
          "set(channel, inputTypedArray)",
          select_overload<void(int, const typename T::SampleArrayValType &)>(
              &T::js_set))
      .function(
          "set(channel, offset, inputTypedArray)",
          select_overload<void(
              int, int, const typename T::SampleArrayValType &)>(&T::js_set))
      .function("set(channels)",
                select_overload<void(const typename T::ChannelArrayValType &)>(
                    &T::js_set))
      .function(
          "view",
          select_overload<typename T::ChannelArrayValType() const>(&T::js_view))
      .function("view(channel)",
                select_overload<typename T::SampleArrayValType(int) const>(
                    &T::js_view))
      .function("setInto(outputTypedArrays)",
                select_overload<void(typename T::ChannelArrayValType) const>(
                    &T::js_setInto))
      .function(
          "setInto(channel, outputTypedArray)",
          select_overload<void(int, typename T::SampleArrayValType) const>(
              &T::js_setInto))
      .property("channelCount", &T::getChannelCount)
      .property("frameCount", &T::getFrameCount)
      .function("peak", &T::peak)
      .function("interleaveTo(destination)",
                select_overload<void(const std::shared_ptr<T> &) const>(
                    &T::interleaveTo))
      .function("deinterleaveFrom(source)",
                select_overload<void(const std::shared_ptr<T> &)>(
                    &T::deinterleaveFrom));
}

EMSCRIPTEN_BINDINGS(buffer) {
  register_buffer_bindings<BufferF32>("BufferF32");
  register_buffer_bindings<BufferF64>("BufferF64");

  register_type<Float32Array>("Float32Array");
  register_type<Float64Array>("Float64Array");
  register_type<ChannelArrayF32>("Float32Array[]");
  register_type<ChannelArrayF64>("Float64Array[]");
};

TEST_CASE("buffer c++ tests", "[buffer]") {
  {
    float xs[4] = {1, 2, 3, 4};
    BufferF32 buf = BufferF32::fromVLA(2, 2, xs);
    REQUIRE(buf.getFrameCount() == 2);
    REQUIRE(buf.getChannelCount() == 2);
    REQUIRE(buf[0][0] == 1.f);
    REQUIRE(buf[0][1] == 2.f);
    REQUIRE(buf[1][0] == 3.f);
    REQUIRE(buf[1][1] == 4.f);
  }
  {
    BufferF32 buf(1, 10);
    for (int i = 0; i < 5; i++) {
      buf[0][i] = i;
    }
    for (int i = 0; i < 5; i++) {
      buf[0][i + 5] = 6 - i;
    }

    REQUIRE(buf.peak() == 6.f);
  }
  {
    BufferF32 buf(1, 100);
    buf.fill(0.f);
    REQUIRE(buf.getFrameCount() == 100);
    REQUIRE(buf.getChannelCount() == 1);
    auto sub = buf.slice(1, 10);
    REQUIRE(sub.getFrameCount() == 9);
    REQUIRE(sub.getChannelCount() == 1);
    sub.fill(1.f);
    REQUIRE(std::all_of(sub.getChannelData(0), sub.getChannelData(0) + 9,
                        [](float v) { return v == 1.f; }));
    REQUIRE(std::all_of(buf.getChannelData(0) + 10, buf.getChannelData(0) + 100,
                        [](float v) { return v == 0.f; }));
    REQUIRE(buf[0][0] == 0.f);
    REQUIRE(buf[0][1] == 1.f);
  }

  {
    BufferF32 buf(2, 123);
    buf.fill(0.f);
    REQUIRE(buf.getFrameCount() == 123);
    REQUIRE(buf.getChannelCount() == 2);
    auto right = buf.sliceChannel(1);
    right[0][0] = 123.f;
    REQUIRE(right[0][0] == 123.f);
    REQUIRE(buf[1][0] == 123.f);
    REQUIRE(buf[1][1] == 0.f);

    buf.cloneChannel(0, 1);
    REQUIRE(buf[1][0] == 0.f);

    right.fill(2.f);
    buf.mixDownInPlace();
    REQUIRE(std::all_of(buf.getChannelData(0),
                        buf.getChannelData(0) + buf.getFrameCount(),
                        [](float v) { return v == 1.f; }));

    BufferF32 buf2(2, 10);
    buf2.noise();

    buf.set(10, buf2);
    for (int channel = 0; channel < buf.getChannelCount(); channel++) {
      REQUIRE(std::equal(buf[channel] + 10,
                         buf[channel] + 10 + buf2.getFrameCount(),
                         buf2[channel]));
    }

    buf.set(1, 20, 10, buf2.getChannelData(0));
    REQUIRE(std::all_of(buf[0] + 20, buf[0] + 30,
                        [](float v) { return v == 1.f; }));
    REQUIRE(std::equal(buf[1] + 20, buf[1] + 30, buf2[0]));

    buf.fill(10, 1, 123.f);
    for (int channel = 0; channel < buf.getChannelCount(); channel++) {
      REQUIRE(buf[channel][10] == 123.f);
      REQUIRE(buf[channel][11] == buf2[channel][1]);
    }

    REQUIRE(!buf.hasNaN());
    buf[0][0] = 0.f / 0.f;
    REQUIRE(buf.hasNaN());
  }

  {
    BufferF32 buf(2, 123);
    buf.fill(0.f);
    buf = buf; // should have no effect
    REQUIRE(buf[0][0] == 0.f);

    auto buf2 = buf;
    buf2.fill(1.f);
    REQUIRE(buf[0][0] == 1.f);

    auto buf3(buf);
    buf3.fill(2.f);
    REQUIRE(buf[0][0] == 2.f);

    BufferF32 buf4(2, 10);
    buf4 = buf;
    buf4.fill(3.f);
    REQUIRE(buf[0][0] == 3.f);

    BufferF32 buf5(std::move(buf));
    buf5.fill(4.f);
    REQUIRE(buf2[0][0] == 4.f);
  }

  // Test interleaveTo
  {
    BufferF32 stereo(2, 4);
    stereo[0][0] = 1.f;
    stereo[0][1] = 2.f;
    stereo[0][2] = 3.f;
    stereo[0][3] = 4.f;
    stereo[1][0] = 5.f;
    stereo[1][1] = 6.f;
    stereo[1][2] = 7.f;
    stereo[1][3] = 8.f;

    BufferF32 interleaved(1, 8);
    stereo.interleaveTo(interleaved);

    REQUIRE(interleaved[0][0] == 1.f);
    REQUIRE(interleaved[0][1] == 5.f);
    REQUIRE(interleaved[0][2] == 2.f);
    REQUIRE(interleaved[0][3] == 6.f);
    REQUIRE(interleaved[0][4] == 3.f);
    REQUIRE(interleaved[0][5] == 7.f);
    REQUIRE(interleaved[0][6] == 4.f);
    REQUIRE(interleaved[0][7] == 8.f);
  }

  // Test deinterleaveFrom
  {
    BufferF32 interleaved(1, 8);
    interleaved[0][0] = 1.f;
    interleaved[0][1] = 5.f;
    interleaved[0][2] = 2.f;
    interleaved[0][3] = 6.f;
    interleaved[0][4] = 3.f;
    interleaved[0][5] = 7.f;
    interleaved[0][6] = 4.f;
    interleaved[0][7] = 8.f;

    BufferF32 stereo(2, 4);
    stereo.deinterleaveFrom(interleaved);

    REQUIRE(stereo[0][0] == 1.f);
    REQUIRE(stereo[0][1] == 2.f);
    REQUIRE(stereo[0][2] == 3.f);
    REQUIRE(stereo[0][3] == 4.f);
    REQUIRE(stereo[1][0] == 5.f);
    REQUIRE(stereo[1][1] == 6.f);
    REQUIRE(stereo[1][2] == 7.f);
    REQUIRE(stereo[1][3] == 8.f);
  }

  // Test round-trip: interleaveTo then deinterleaveFrom
  {
    BufferF32 original(2, 10);
    original.noise();

    BufferF32 interleaved(1, 20);
    original.interleaveTo(interleaved);

    BufferF32 restored(2, 10);
    restored.deinterleaveFrom(interleaved);

    for (int channel = 0; channel < 2; channel++) {
      for (int frame = 0; frame < 10; frame++) {
        REQUIRE(restored[channel][frame] == original[channel][frame]);
      }
    }
  }
}

TEST_CASE("buffer interleave mono", "[buffer]") {
  BufferF32 monoIn(1, 4);
  BufferF32 monoOut(1, 4);
  monoIn[0][0] = 1.f;
  monoIn[0][1] = 2.f;
  monoIn[0][2] = 3.f;
  monoIn[0][3] = 4.f;

  monoOut.fill(0.f);

  // SECTION lets us take every possible branch combination
  SECTION("interleaveTo") { monoIn.interleaveTo(monoOut); }

  SECTION("deinterleaveFrom") { monoOut.deinterleaveFrom(monoIn); }

  REQUIRE(monoOut[0][0] == 1.f);
  REQUIRE(monoOut[0][1] == 2.f);
  REQUIRE(monoOut[0][2] == 3.f);
  REQUIRE(monoOut[0][3] == 4.f);
}

template <typename T>
concept CanConstructOwning = requires {
  { Buffer<T>(2, 1024) } -> std::same_as<Buffer<T>>;
};

template <typename T>
concept CanModifyContents =
    requires(Buffer<T> buffer, typename Buffer<T>::sampleType value) {
      {
        buffer[0][0] = value
      } -> std::convertible_to<typename Buffer<T>::sampleType &>;
    };

template <typename T>
concept SupportsOwnershipTransfer = requires {
  requires std::is_move_constructible_v<Buffer<T>>;
  requires std::is_move_assignable_v<Buffer<T>>;
};

template <typename T>
concept CanConvertToConstBuffer = requires(Buffer<T> buffer) {
  {
    static_cast<Buffer<const typename std::remove_const_t<T>>>(buffer)
  } -> std::same_as<Buffer<const typename std::remove_const_t<T>>>;
};

template <typename T>
concept CanPerformDataOperations = requires(Buffer<T> buffer) {
  { buffer.fill(0.0f) } -> std::same_as<void>;
  { buffer.slice(0, 10) } -> std::same_as<Buffer<T>>;
};

template <typename T>
concept ConstMethodsReturnConstBuffers = requires(const Buffer<T> buffer) {
  {
    buffer.slice(0, 10)
  } -> std::same_as<Buffer<const typename std::remove_const_t<T>>>;
  {
    buffer.sliceChannel(0)
  } -> std::same_as<Buffer<const typename std::remove_const_t<T>>>;
};

static_assert(CanConstructOwning<float>,
              "Buffer<float> should support memory-owning construction");
static_assert(CanModifyContents<float>,
              "Buffer<float> should allow modifying its contents");
static_assert(SupportsOwnershipTransfer<float>,
              "Buffer<float> should support move operations");
static_assert(CanConvertToConstBuffer<float>,
              "Buffer<float> should be convertible to Buffer<const float>");
static_assert(CanPerformDataOperations<float>,
              "Buffer<float> should support data manipulation operations");
static_assert(ConstMethodsReturnConstBuffers<float>,
              "Const methods should return const buffers for Buffer<float>");

// Const buffer should have restricted capabilities
static_assert(
    !CanConstructOwning<const float>,
    "Buffer<const float> should not support memory-owning construction");
static_assert(!CanModifyContents<const float>,
              "Buffer<const float> should not allow modifying its contents");
static_assert(!SupportsOwnershipTransfer<const float>,
              "Buffer<const float> should not support ownership transfer in "
              "move operations");
static_assert(
    ConstMethodsReturnConstBuffers<const float>,
    "Const methods should return const buffers for Buffer<const float>");