#include "IUI/ImGuiFilamentBridge.h"

#include <utils/EntityManager.h>
#include <tl/vector.h>
#include "IUI/ImGui/imgui.h"
#include <filamat/MaterialBuilder.h>
#include <filament/IndexBuffer.h>
#include <filament/Material.h>
#include <filament/MaterialInstance.h>
#include <filament/RenderableManager.h>
#include <filament/Scene.h>
#include <filament/Texture.h>
#include <filament/TextureSampler.h>
#include <filament/VertexBuffer.h>
#include <Kit/EngineManager.h>

#include "tl/narrow_cast.h"

using namespace filament::math;
using namespace filament;
using namespace utils;

namespace kit
{
static constexpr uint8_t IMGUI_PACKAGE[] =
{
// ReSharper disable All
#include "Materials/ImGuiMaterial.h"
// ReSharper restore All
};

ImGuiFilamentBridge::ImGuiFilamentBridge(tl::lent_ref<EngineManager> engineManager, tl::lent_ref<filament::Scene> flmScene)
	: m_engineManager(std::move(engineManager))
    , m_scene(std::move(flmScene))
{
	// Create a simple alpha-blended 2D blitting material.
	auto builder = Material::Builder()
		.package(IMGUI_PACKAGE, sizeof(IMGUI_PACKAGE));

	m_material = m_engineManager->adopt(builder.build(*m_engineManager->getEngine()));

	EntityManager& em = utils::EntityManager::get();
	m_renderable = em.create();
	m_scene->addEntity(m_renderable);
}

ImTextureID ImGuiFilamentBridge::createAtlasTexture()
{
	const ImGuiIO& io = ImGui::GetIO();
	// Create the grayscale texture that ImGui uses for its glyph atlas.
	unsigned char* pixels;
	int width, height;
	io.Fonts->GetTexDataAsRGBA32(&pixels, &width, &height);
	const size_t size = tl::narrow<size_t>(width * height * 4);
	uint8_t* buffer = m_engineManager->acquireUploadMemory(size);
	memcpy(buffer, pixels, size);

	auto builder = Texture::Builder()
	               .width(tl::narrow<uint32_t>(width))
	               .height(tl::narrow<uint32_t>(height))
	               .levels(tl::narrow<uint8_t>(1))
	               .format(Texture::InternalFormat::RGBA8)
	               .sampler(Texture::Sampler::SAMPLER_2D);

	m_texture = m_engineManager->adopt(builder.build(*m_engineManager->getEngine()));
	m_texture->setImage(*m_engineManager->getEngine(), 0, Texture::PixelBufferDescriptor(buffer, size, Texture::Format::RGBA, Texture::Type::UBYTE));

	const TextureSampler sampler(TextureSampler::MinFilter::LINEAR_MIPMAP_LINEAR, TextureSampler::MagFilter::LINEAR);
	m_material->setDefaultParameter("albedo", m_texture.get(), sampler);
	for (const auto& mi : m_materialInstances)
		mi->setParameter("albedo", m_texture.get(), sampler);

	return m_texture.get();
}

ImGuiFilamentBridge::~ImGuiFilamentBridge()
{
	m_engineManager->destroy(m_renderable);
}

//void ImGuiFilamentBridge::setDisplaySize(int width, int height, float scaleX, float scaleY)
//{
//	setContext();
//	ImGuiIO& io = ImGui::GetIO();
//	io.DisplaySize = ImVec2((float)width, (float)height);
//	io.DisplayFramebufferScale.x = scaleX;
//	io.DisplayFramebufferScale.y = scaleY;
//}

void ImGuiFilamentBridge::processImGuiCommands(ImDrawData& commands, const ImGuiIO& io)
{
	auto& rcm = m_engineManager->getEngine()->getRenderableManager();

	// Avoid rendering when minimized and scale coordinates for retina displays.
	const int fbwidth = static_cast<int>(commands.DisplaySize.x * commands.FramebufferScale.x);
	const int fbheight = static_cast<int>(commands.DisplaySize.y * commands.FramebufferScale.y);
	if (fbwidth == 0 || fbheight == 0)
		return;
	//commands.ScaleClipRects(commands.FramebufferScale);

	// Ensure that we have enough vertex buffers and index buffers.
	createBuffers(commands.CmdListsCount);

	// Count how many primitives we'll need, then create a Renderable builder.
	size_t primitiveCount = 0;
	//tl::unordered_map<uint64_t, filament::MaterialInstance*> scissorRects;
	for (int cmdListIndex = 0; cmdListIndex < commands.CmdListsCount; cmdListIndex++)
	{
		const ImDrawList* cmds = commands.CmdLists[cmdListIndex];
		primitiveCount += cmds->CmdBuffer.size();
	}
	auto rbuilder = RenderableManager::Builder(primitiveCount);
	rbuilder.boundingBox({{0, 0, 0}, {10000, 10000, 10000}}).culling(false);

	// Ensure that we have a material instance for each primitive.
	const size_t previousSize = m_materialInstances.size();
	if (primitiveCount > m_materialInstances.size())
	{
		//m_materialInstances.resize(primitiveCount);
		for (size_t i = previousSize; i < primitiveCount; i++)
			m_materialInstances.push_back(m_engineManager->adopt(m_material->createInstance()));
	}

	const ImVec2 clipOff = commands.DisplayPos; // (0,0) unless using multi-viewports
	const ImVec2 clipScale = commands.FramebufferScale; // (1,1) unless using retina display which are often (2,2)

	// Recreate the Renderable component and point it to the vertex buffers.
	rcm.destroy(m_renderable);
	size_t bufferIndex = 0;
	size_t primIndex = 0;
	for (int cmdListIndex = 0; cmdListIndex < commands.CmdListsCount; cmdListIndex++)
	{
		const ImDrawList* cmds = commands.CmdLists[cmdListIndex];
		size_t indexOffset = 0;
		populateVertexData(bufferIndex,
		                   cmds->VtxBuffer.Size * sizeof(ImDrawVert),
		                   cmds->VtxBuffer.Data,
		                   cmds->IdxBuffer.Size * sizeof(ImDrawIdx),
		                   cmds->IdxBuffer.Data);
		for (const auto& pcmd : cmds->CmdBuffer)
		{
			if (pcmd.UserCallback)
				pcmd.UserCallback(cmds, &pcmd);
			else
			{
				MaterialInstance& materialInstance = *m_materialInstances[primIndex];

				// Project scissor/clipping rectangles into framebuffer space
				const ImVec2 clipMin((pcmd.ClipRect.x - clipOff.x) * clipScale.x, (pcmd.ClipRect.y - clipOff.y) * clipScale.y);
				const ImVec2 clipMax((pcmd.ClipRect.z - clipOff.x) * clipScale.x, (pcmd.ClipRect.w - clipOff.y) * clipScale.y);

				materialInstance.setScissor(uint32_t(clipMin.x),
				                             uint32_t(fbheight - clipMax.y),
				                             (uint16_t)(clipMax.x - clipMin.x),
				                             (uint16_t)(clipMax.y - clipMin.y));
				TextureSampler sampler(TextureSampler::MinFilter::LINEAR_MIPMAP_LINEAR, TextureSampler::MagFilter::LINEAR);
				if (pcmd.TextureId)
					materialInstance.setParameter("albedo", (Texture const*)pcmd.TextureId, sampler);
				else
					materialInstance.setParameter("albedo", m_texture.get(), sampler);

				rbuilder
					.geometry(primIndex,
					          RenderableManager::PrimitiveType::TRIANGLES,
					          m_vertexBuffers[bufferIndex].get(),
					          m_indexBuffers[bufferIndex].get(),
					          indexOffset,
					          pcmd.ElemCount)
					.blendOrder(primIndex, tl::narrow<uint16_t>(primIndex))
					.material(primIndex, &materialInstance);
				primIndex++;
			}
			indexOffset += pcmd.ElemCount;
		}
		bufferIndex++;
	}
	if (commands.CmdListsCount > 0)
		rbuilder.build(*m_engineManager->getEngine(), m_renderable);
}

tl::unique_ref<filament::VertexBuffer> ImGuiFilamentBridge::createVertexBuffer(size_t capacity) const
{
	auto builder = VertexBuffer::Builder()
	               .vertexCount(uint32_t(capacity))
	               .bufferCount(1)
	               .attribute(VertexAttribute::POSITION,
	                          0,
	                          VertexBuffer::AttributeType::FLOAT2,
	                          0,
	                          sizeof(ImDrawVert))
	               .attribute(VertexAttribute::UV0,
	                          0,
	                          VertexBuffer::AttributeType::FLOAT2,
	                          sizeof(filament::math::float2),
	                          sizeof(ImDrawVert))
	               .attribute(VertexAttribute::COLOR,
	                          0,
	                          VertexBuffer::AttributeType::UBYTE4,
	                          2 * sizeof(filament::math::float2),
	                          sizeof(ImDrawVert))
	               .normalized(COLOR);

	return m_engineManager->adopt(builder.build(*m_engineManager->getEngine()));
}

tl::unique_ref<filament::IndexBuffer> ImGuiFilamentBridge::createIndexBuffer(size_t capacity) const
{
	auto builder = IndexBuffer::Builder()
	               .indexCount(uint32_t(capacity))
	               .bufferType(IndexBuffer::IndexType::UINT);
	return m_engineManager->adopt(builder.build(*m_engineManager->getEngine()));
}

void ImGuiFilamentBridge::createBuffers(int numRequiredBuffers)
{
	if (numRequiredBuffers > m_vertexBuffers.size())
	{
		const size_t previousSize = m_vertexBuffers.size();
		for (size_t i = previousSize; i < numRequiredBuffers; i++)
		{
			// Pick a reasonable starting capacity; it will grow if needed.
			m_vertexBuffers.push_back(createVertexBuffer(1000));
		}
	}
	if (numRequiredBuffers > m_indexBuffers.size())
	{
		const size_t previousSize = m_indexBuffers.size();
		for (size_t i = previousSize; i < numRequiredBuffers; i++)
		{
			// Pick a reasonable starting capacity; it will grow if needed.
			m_indexBuffers.push_back(createIndexBuffer(1000));
		}
	}
}

void ImGuiFilamentBridge::populateVertexData(size_t bufferIndex, size_t vbSizeInBytes, void* vbImguiData, size_t ibSizeInBytes, void* ibImguiData)
{
	// Create a new vertex buffer if the size isn't large enough, then copy the ImGui data into
	// a staging area since Filament's render thread might consume the data at any time.
	const size_t requiredVertCount = vbSizeInBytes / sizeof(ImDrawVert);
	const size_t capacityVertCount = m_vertexBuffers[bufferIndex]->getVertexCount();
	if (requiredVertCount > capacityVertCount)
		m_vertexBuffers[bufferIndex] = createVertexBuffer(requiredVertCount);

	{
		const size_t nVbBytes = requiredVertCount * sizeof(ImDrawVert);
		uint8_t* vbFilamentData = m_engineManager->acquireUploadMemory(nVbBytes);
		memcpy(vbFilamentData, vbImguiData, nVbBytes);
		m_vertexBuffers[bufferIndex]->setBufferAt(*m_engineManager->getEngine(), 0, VertexBuffer::BufferDescriptor(vbFilamentData, nVbBytes));
	}

	// Create a new index buffer if the size isn't large enough, then copy the ImGui data into
	// a staging area since Filament's render thread might consume the data at any time.
	const size_t requiredIndexCount = ibSizeInBytes / sizeof(ImDrawIdx);
	const size_t capacityIndexCount = m_indexBuffers[bufferIndex]->getIndexCount();
	if (requiredIndexCount > capacityIndexCount)
		m_indexBuffers[bufferIndex] = createIndexBuffer(requiredIndexCount);

	{
		const size_t nIbBytes = requiredIndexCount * sizeof(ImDrawIdx);
		uint8_t* ibFilamentData = m_engineManager->acquireUploadMemory(nIbBytes);
		memcpy(ibFilamentData, ibImguiData, nIbBytes);
		m_indexBuffers[bufferIndex]->setBuffer(*m_engineManager->getEngine(), IndexBuffer::BufferDescriptor(ibFilamentData, nIbBytes));
	}
}
} // namespace filagui
