#include "Bgra8ReadbackPipeline.h"

#include "../frames/SystemFrameTypes.h"

#include <chrono>
#include <cstring>

namespace
{
double MillisecondsSince(std::chrono::steady_clock::time_point start)
{
	return std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(
		std::chrono::steady_clock::now() - start).count();
}
}

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

bool Bgra8ReadbackPipeline::Initialize(unsigned width, unsigned height, std::size_t pboDepth)
{
	Shutdown();

	mWidth = width;
	mHeight = height;
	mRowBytes = VideoIORowBytes(VideoIOPixelFormat::Bgra8, width);
	if (mWidth == 0 || mHeight == 0 || mRowBytes == 0)
		return false;

	if (!CreateRenderTarget())
	{
		Shutdown();
		return false;
	}

	const std::size_t byteCount = static_cast<std::size_t>(mRowBytes) * static_cast<std::size_t>(mHeight);
	if (!mPboRing.Initialize(pboDepth, byteCount))
	{
		Shutdown();
		return false;
	}

	return true;
}

void Bgra8ReadbackPipeline::Shutdown()
{
	mPboRing.Shutdown();
	DestroyRenderTarget();
	mWidth = 0;
	mHeight = 0;
	mRowBytes = 0;
}

bool Bgra8ReadbackPipeline::RenderAndQueue(uint64_t frameIndex, const RenderCallback& renderFrame)
{
	if (mFramebuffer == 0 || !renderFrame)
		return false;

	glBindFramebuffer(GL_FRAMEBUFFER, mFramebuffer);
	const auto renderStart = std::chrono::steady_clock::now();
	renderFrame(frameIndex);
	mLastRenderFrameMilliseconds = MillisecondsSince(renderStart);
	glBindFramebuffer(GL_FRAMEBUFFER, 0);

	const auto queueStart = std::chrono::steady_clock::now();
	const bool queued = mPboRing.QueueReadback(mFramebuffer, mWidth, mHeight, frameIndex);
	mLastReadbackQueueMilliseconds = MillisecondsSince(queueStart);
	return queued;
}

void Bgra8ReadbackPipeline::ConsumeCompleted(
	const AcquireFrameCallback& acquireFrame,
	const PublishFrameCallback& publishFrame,
	const CounterCallback& onAcquireMiss,
	const CounterCallback& onCompleted)
{
	if (!acquireFrame || !publishFrame)
		return;

	PboReadbackRing::CompletedReadback readback;
	while (mPboRing.TryAcquireCompleted(readback))
	{
		const auto copyStart = std::chrono::steady_clock::now();
		glBindBuffer(GL_PIXEL_PACK_BUFFER, readback.pbo);
		void* mapped = glMapBuffer(GL_PIXEL_PACK_BUFFER, GL_READ_ONLY);
		if (!mapped)
		{
			glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
			mPboRing.ReleaseCompleted(readback);
			mLastCompletedReadbackCopyMilliseconds = MillisecondsSince(copyStart);
			continue;
		}

		SystemFrame frame;
		if (acquireFrame(frame))
		{
			const std::size_t byteCount = static_cast<std::size_t>(frame.rowBytes) * static_cast<std::size_t>(frame.height);
			if (frame.bytes != nullptr && byteCount <= readback.byteCount)
			{
				std::memcpy(frame.bytes, mapped, byteCount);
				frame.frameIndex = readback.frameIndex;
				frame.pixelFormat = VideoIOPixelFormat::Bgra8;
				publishFrame(frame);
				if (onCompleted)
					onCompleted();
			}
		}
		else if (onAcquireMiss)
		{
			onAcquireMiss();
		}

		glUnmapBuffer(GL_PIXEL_PACK_BUFFER);
		glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
		mPboRing.ReleaseCompleted(readback);
		mLastCompletedReadbackCopyMilliseconds = MillisecondsSince(copyStart);
	}
}

bool Bgra8ReadbackPipeline::CreateRenderTarget()
{
	glGenFramebuffers(1, &mFramebuffer);
	glBindFramebuffer(GL_FRAMEBUFFER, mFramebuffer);

	glGenTextures(1, &mTexture);
	glBindTexture(GL_TEXTURE_2D, mTexture);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
	glTexImage2D(
		GL_TEXTURE_2D,
		0,
		GL_RGBA8,
		static_cast<GLsizei>(mWidth),
		static_cast<GLsizei>(mHeight),
		0,
		GL_BGRA,
		GL_UNSIGNED_INT_8_8_8_8_REV,
		nullptr);
	glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, mTexture, 0);

	const bool complete = glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE;
	glBindTexture(GL_TEXTURE_2D, 0);
	glBindFramebuffer(GL_FRAMEBUFFER, 0);
	return complete;
}

void Bgra8ReadbackPipeline::DestroyRenderTarget()
{
	if (mFramebuffer != 0)
		glDeleteFramebuffers(1, &mFramebuffer);
	if (mTexture != 0)
		glDeleteTextures(1, &mTexture);
	mFramebuffer = 0;
	mTexture = 0;
}
