#include "ringbuffercache.h"
#include "remotebuffer.h"
#include <catch2/catch.hpp>

RingBufferCache::RingBufferCache(
    std::shared_ptr<RandomAccessAudioReadable> underlyingReadable, int pageSize,
    int pageCount)
    : RingBufferCache(underlyingReadable->getChannelCount(), pageSize,
                      pageCount) {
  assert(underlyingReadable != nullptr);
  assert(pageSize > 0);
  assert(pageCount > 0);

  this->underlyingReadable = std::move(underlyingReadable);
}

RingBufferCache::RingBufferCache(int channelCount, int pageSize, int pageCount)
    : pageSize(pageSize), pageCount(pageCount) {
  for (int i = 0; i < pageCount; i++) {
    pages.emplace_back(channelCount, pageSize);
  }
}

RingBufferCache::~RingBufferCache() {}

void RingBufferCache::setUnderlyingReadable(
    std::shared_ptr<RandomAccessAudioReadable> underlyingReadable) {
  assert(underlyingReadable != nullptr);
  this->underlyingReadable = std::move(underlyingReadable);
  for (auto &page : pages) {
    page.startFrame = -1;
  }
}

void RingBufferCache::read(int frameOffset, BufferF32 output) {
  if (underlyingReadable == nullptr) {
    output.fill(0);
    return;
  }

  requestSerial += 1;
  if (requestSerial == 0) {
    for (auto &page : pages) {
      page.requestedSerial = 0;
    }
    requestSerial = 1;
  }

  const int outputFrameCount = output.getFrameCount();
  assert(frameOffset >= 0 &&
         frameOffset + outputFrameCount <= underlyingReadable->getFrameCount());
  assert(output.getChannelCount() == underlyingReadable->getChannelCount());

  int framesRead = 0;
  while (framesRead < outputFrameCount) {
    auto &page = getPageForFrame(frameOffset + framesRead);
    page.requestedSerial = requestSerial;
    int offsetInPage = frameOffset + framesRead - page.startFrame;
    int framesToReadFromPage =
        std::min(outputFrameCount - framesRead, pageSize - offsetInPage);
    auto outBuf = output.slice(framesRead, framesRead + framesToReadFromPage);
    outBuf.set(0, page.buffer.slice(offsetInPage,
                                    offsetInPage + framesToReadFromPage));
    framesRead += framesToReadFromPage;
  }
}

RingBufferCache::Page &RingBufferCache::getPageForFrame(int frame) {
  for (int i = 0; i < pageCount; i++) {
    auto &page = pages[i];
    if (page.startFrame >= 0 && frame >= page.startFrame &&
        frame < page.startFrame + pageSize) {
      return page;
    }
  }

  auto &newPage = pages[findEmptyPageIndex()];
  newPage.startFrame = frame - (frame % pageSize);

  int framesToRead = std::min(pageSize, underlyingReadable->getFrameCount() -
                                            newPage.startFrame);
  underlyingReadable->read(newPage.startFrame,
                           newPage.buffer.slice(0, framesToRead));
  if (framesToRead < pageSize) {
    newPage.buffer.slice(framesToRead).fill(0);
  }
  return newPage;
}

int RingBufferCache::findEmptyPageIndex() {
  // find an empty page (startFrame == -1) or the page with the oldest serial
  int oldestPageIndex = 0;
  for (int i = 1; i < pageCount; i++) {
    if (pages[i].startFrame == -1) {
      return i;
    }
    if (pages[i].requestedSerial < pages[oldestPageIndex].requestedSerial) {
      oldestPageIndex = i;
    }
  }
  return oldestPageIndex;
}

TEST_CASE("ring buffer cache tests", "[ringbuffercache]") {
  const int totalFrames = 44100;
  auto buf = std::make_shared<BufferF32>(1, totalFrames);
  buf->noise();
  auto rab = std::make_shared<BufferAsRemoteAudioBuffer>(44100, buf);

  for (auto &sizeCombo : std::vector<std::pair<int, int>>{{128, 1024},
                                                          {1024, 1024},
                                                          {2048, 1024},
                                                          {1234, 1024},
                                                          {1024, 128},
                                                          {1025, 127}}) {
    auto [outBufferSize, pageSize] = sizeCombo;
    auto cache = std::make_shared<RingBufferCache>(rab, pageSize, 10);
    BufferF32 out(1, outBufferSize);

    // sequential
    for (int i = 0; i < totalFrames / outBufferSize; i++) {
      cache->read(i * outBufferSize, out);
      bool match = true;
      auto refData = buf->getChannelData(0) + i * outBufferSize;
      auto outData = out.getChannelData(0);
      for (int j = 0; j < out.getFrameCount(); j++) {
        if (refData[j] != outData[j]) {
          match = false;
          break;
        }
      }
      REQUIRE(match);
    }

    // random
    for (int i = 0; i < 100; i++) {
      int offset = (286473 + i * 123471) % (totalFrames - outBufferSize);
      cache->read(offset, out);
      bool match = true;
      auto refData = buf->getChannelData(0) + offset;
      auto outData = out.getChannelData(0);
      for (int j = 0; j < out.getFrameCount(); j++) {
        if (refData[j] != outData[j]) {
          match = false;
          break;
        }
      }
      REQUIRE(match);
    }
  }
}