#include "PboReadbackRing.h"

#include <algorithm>

PboReadbackRing::~PboReadbackRing()
{
	Shutdown();
}

bool PboReadbackRing::Initialize(std::size_t depth, std::size_t byteCount)
{
	Shutdown();
	if (depth == 0 || byteCount == 0)
		return false;

	mSlots.resize(depth);
	mByteCount = byteCount;
	for (Slot& slot : mSlots)
	{
		glGenBuffers(1, &slot.pbo);
		if (slot.pbo == 0)
		{
			Shutdown();
			return false;
		}
		glBindBuffer(GL_PIXEL_PACK_BUFFER, slot.pbo);
		glBufferData(GL_PIXEL_PACK_BUFFER, static_cast<GLsizeiptr>(mByteCount), nullptr, GL_STREAM_READ);
	}
	glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
	return true;
}

void PboReadbackRing::Shutdown()
{
	for (Slot& slot : mSlots)
	{
		if (slot.fence)
			glDeleteSync(slot.fence);
		if (slot.pbo != 0)
			glDeleteBuffers(1, &slot.pbo);
		slot = {};
	}
	mSlots.clear();
	mWriteIndex = 0;
	mReadIndex = 0;
	mByteCount = 0;
}

bool PboReadbackRing::QueueReadback(GLuint framebuffer, unsigned width, unsigned height, uint64_t frameIndex)
{
	if (mSlots.empty())
		return false;

	Slot& slot = mSlots[mWriteIndex];
	if (slot.inFlight || slot.acquired)
	{
		++mQueueMisses;
		return false;
	}

	glBindFramebuffer(GL_READ_FRAMEBUFFER, framebuffer);
	glPixelStorei(GL_PACK_ALIGNMENT, 4);
	glPixelStorei(GL_PACK_ROW_LENGTH, 0);
	glBindBuffer(GL_PIXEL_PACK_BUFFER, slot.pbo);
	glBufferData(GL_PIXEL_PACK_BUFFER, static_cast<GLsizeiptr>(mByteCount), nullptr, GL_STREAM_READ);
	glReadPixels(0, 0, static_cast<GLsizei>(width), static_cast<GLsizei>(height), GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, nullptr);
	slot.fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
	slot.inFlight = slot.fence != nullptr;
	slot.frameIndex = frameIndex;
	glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);

	if (!slot.inFlight)
		return false;

	mWriteIndex = (mWriteIndex + 1) % mSlots.size();
	return true;
}

bool PboReadbackRing::TryAcquireCompleted(CompletedReadback& readback)
{
	if (mSlots.empty())
		return false;

	for (std::size_t checked = 0; checked < mSlots.size(); ++checked)
	{
		Slot& slot = mSlots[mReadIndex];
		if (!slot.inFlight || slot.acquired || slot.fence == nullptr)
		{
			mReadIndex = (mReadIndex + 1) % mSlots.size();
			continue;
		}

		const GLenum waitResult = glClientWaitSync(slot.fence, 0, 0);
		if (waitResult != GL_ALREADY_SIGNALED && waitResult != GL_CONDITION_SATISFIED)
			return false;

		slot.acquired = true;
		readback.pbo = slot.pbo;
		readback.frameIndex = slot.frameIndex;
		readback.byteCount = mByteCount;
		return true;
	}
	return false;
}

void PboReadbackRing::ReleaseCompleted(const CompletedReadback& readback)
{
	for (std::size_t index = 0; index < mSlots.size(); ++index)
	{
		Slot& slot = mSlots[index];
		if (!slot.acquired || slot.pbo != readback.pbo)
			continue;
		ResetSlot(slot);
		mReadIndex = (index + 1) % mSlots.size();
		return;
	}
}

void PboReadbackRing::DrainCompleted()
{
	for (std::size_t pass = 0; pass < mSlots.size() * 2; ++pass)
	{
		CompletedReadback readback;
		if (!TryAcquireCompleted(readback))
			break;
		ReleaseCompleted(readback);
	}
}

void PboReadbackRing::ResetSlot(Slot& slot)
{
	if (slot.fence)
		glDeleteSync(slot.fence);
	slot.fence = nullptr;
	slot.inFlight = false;
	slot.acquired = false;
	slot.frameIndex = 0;
}
