/*
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
 */

#include "ResourceManager.hpp"
#include "ManagedBuffer.hpp"
#include "MemoryAllocation.hpp"
#include "Scene/Vertex.hpp"
#include "Scene/Geometry.hpp"
#include "Scene/Material.hpp"
#include "Scene/UniformBuffer.hpp"
#include "Scene/Camera.hpp"
#include "Math/ByteSize.hpp"
#include "Vulkan/Context.hpp"
#include "Vulkan/Image.hpp"
#include "Vulkan/Scene/VulkanShader.hpp"
#include "Vulkan/Scene/VulkanGeometry.hpp"
#include "Vulkan/Scene/VulkanNode.hpp"
#include "Vulkan/Scene/VulkanTexture.hpp"
#include "Vulkan/Scene/VulkanCamera.hpp"
#include "Vulkan/Scene/VulkanUniformBuffer.hpp"

namespace OpenVulkano::Vulkan
{
	ResourceManager* ResourceManager::INSTANCE;

	struct ResourceManager::FrameResources
	{
		vk::Device device;
		vk::CommandPool cmdPool;
		vk::CommandBuffer cmdBuffer;
		vk::Semaphore semaphore;
		
		FrameResources(Device* device)
		{
			this->device = device->device;
			cmdPool = this->device.createCommandPool({ {}, device->queueIndices.transfer });
			cmdBuffer = this->device.allocateCommandBuffers({ cmdPool, vk::CommandBufferLevel::ePrimary, 1 })[0];
			semaphore = this->device.createSemaphore({});
		}
		
		~FrameResources()
		{
			device.freeCommandBuffers(cmdPool, 1, &cmdBuffer);
			device.destroyCommandPool(cmdPool);
			device.destroy(semaphore);
		}
	};
	
	
	ResourceManager::ResourceManager()
	{
		static_assert(sizeof(DescriptorSetLayoutBinding) == sizeof(vk::DescriptorSetLayoutBinding));
		static_assert(sizeof(Scene::SamplerConfig) == sizeof(vk::SamplerCreateInfo));
	}

	ResourceManager::~ResourceManager() noexcept
	{
		if (device) ResourceManager::Close();
	}

	void ResourceManager::Init(Context* context, int buffers)
	{
		this->context = context;
		device = context->device->device;

		uniformBufferAlignment = context->device->properties.limits.minUniformBufferOffsetAlignment;
		
		frameResources = Array<FrameResources>(static_cast<size_t>(buffers), context->device.get());

		memPool.Init(context->device.get(), buffers);

		transferQueue = this->device.getQueue(context->device->queueIndices.transfer, 0);

		// Setup descriptor pool
		constexpr vk::DescriptorPoolSize sizeInfo[] = {
				{ vk::DescriptorType::eSampler, 100000 },
				{ vk::DescriptorType::eCombinedImageSampler, 100000 },
				{ vk::DescriptorType::eSampledImage, 100000 },
				{ vk::DescriptorType::eStorageImage, 100000 },
				{ vk::DescriptorType::eUniformTexelBuffer, 100000 },
				{ vk::DescriptorType::eStorageTexelBuffer, 100000 },
				{ vk::DescriptorType::eUniformBuffer, 100000 },
				{ vk::DescriptorType::eStorageBuffer, 100000 },
				{ vk::DescriptorType::eUniformBufferDynamic, 100000 },
				{ vk::DescriptorType::eStorageBufferDynamic, 100000 },
				{ vk::DescriptorType::eInputAttachment, 100000 }
		};
		const vk::DescriptorPoolCreateInfo poolCreateInfo(vk::DescriptorPoolCreateFlagBits::eFreeDescriptorSet, 100000, std::size(sizeInfo), sizeInfo);
		descriptorPool = device.createDescriptorPool(poolCreateInfo);

		INSTANCE = this;
	}

	void ResourceManager::Close()
	{
		transferQueue.waitIdle();
		transferQueue = nullptr;
		OnShutdown(this); // Notify all custom resources that it's time to die
		geometries.clear();
		nodes.clear();
		textures.clear();
		cameras.clear();
		uniforms.clear();
		device.destroyDescriptorPool(descriptorPool);
		for (auto& sampler : samplerCache)
		{
			device.destroy(sampler.second);
		}
		samplerCache.clear();
		for(auto& layout : descriptorSetLayoutCache)
		{
			device.destroy(layout.second);
		}
		descriptorSetLayoutCache.clear();
		shaders.clear();
		frameResources = {};
		memPool.Close();
		device = nullptr;
		INSTANCE = nullptr;
	}

	vk::CommandBuffer& ResourceManager::GetCmdBuffer() { return frameResources[currentBuffer].cmdBuffer; }
	
	void ResourceManager::StartFrame(uint64_t frameId)
	{
		currentBuffer = frameId;
		memPool.StartFrame(frameId);
		device.resetCommandPool(frameResources[currentBuffer].cmdPool, {});
		GetCmdBuffer().begin({ vk::CommandBufferUsageFlagBits::eOneTimeSubmit });
	}

	vk::Semaphore ResourceManager::EndFrame()
	{
		GetCmdBuffer().end();
		vk::SubmitInfo si = { 0, nullptr, nullptr, 1, &frameResources[currentBuffer].cmdBuffer, 1, &frameResources[currentBuffer].semaphore };
		vk::Result result = transferQueue.submit(1, &si, vk::Fence());
		if (result != vk::Result::eSuccess) [[unlikely]]
		{
			Logger::RENDER->error("Failed to submit resource manager transfer queue");
		}
		return frameResources[currentBuffer].semaphore;
	}

	void ResourceManager::Resize()
	{
		for (auto& shader : shaders)
		{
			shader->Resize();
		}
	}

	VulkanGeometry* ResourceManager::PrepareGeometry(Scene::Geometry* geometry)
	{
		const std::unique_lock lock(mutex);
		if(!geometry->HasRenderResource())
		{
			ManagedBuffer::Ptr vertexBuffer =
					CreateDeviceOnlyBufferWithData(sizeof(Vertex) * geometry->GetVertexCount(), vk::BufferUsageFlagBits::eVertexBuffer, geometry->GetVertices());
			ManagedBuffer::Ptr indexBuffer;
			if (geometry->GetIndexCount())
				indexBuffer = CreateDeviceOnlyBufferWithData(Utils::EnumAsInt(geometry->indexType) * geometry->GetIndexCount(), vk::BufferUsageFlagBits::eIndexBuffer, geometry->GetIndices());
			VulkanGeometry* vkGeo = new VulkanGeometry(geometry, vertexBuffer, indexBuffer);
			geometries.emplace_back(vkGeo);
			if (geometry->ownsMemory && geometry->freeAfterUpload)
			{
				geometry->Free();
			}
			return vkGeo;
		}
		return geometry->GetRenderResource();
	}

	void ResourceManager::PrepareMaterial(Scene::Material* material)
	{
		if (material->texture && !material->texture->HasRenderResource())
		{
			PrepareTexture(material->texture);
		}
	}

	VulkanNode* ResourceManager::PrepareNode(Scene::Node* node)
	{
		const std::unique_lock lock(mutex);
		if (!node->HasRenderResource())
		{
			UniformBuffer* uBuffer = new UniformBuffer();
			ManagedBuffer::Ptr buffer;
			VulkanNode* vkNode;
			const vk::DeviceSize allocSize = Utils::Align(sizeof(Math::Matrix4f), uniformBufferAlignment);
			vk::DeviceSize frameSize = 0;
			if (node->GetUpdateFrequency() != Scene::UpdateFrequency::Never)
			{
				frameSize = allocSize;
				vkNode = new VulkanNodeDynamic(node, uBuffer);
				const uint32_t imgs = context->swapChain.GetImageCount();
				buffer = memPool.CreateBuffer(imgs * allocSize, vk::BufferUsageFlagBits::eUniformBuffer, vk::MemoryPropertyFlagBits::eHostCoherent | vk::MemoryPropertyFlagBits::eHostVisible);
				buffer->Map();
			}
			else
			{
				buffer = CreateDeviceOnlyBufferWithData(Scene::Node::SIZE, vk::BufferUsageFlagBits::eUniformBuffer, &node->worldMat);
				vkNode = new VulkanNode(node, uBuffer);
			}

			uBuffer->Init(std::move(buffer), frameSize, allocSize, GetDescriptorLayoutSet(Scene::Node::DESCRIPTOR_SET_LAYOUT_BINDING), Scene::Node::DESCRIPTOR_SET_LAYOUT_BINDING, 0);
			nodes.emplace_back(vkNode);
			return vkNode;
		}
		return node->GetRenderResource();
	}

	VulkanCamera* ResourceManager::PrepareCamera(Scene::Camera* camera)
	{
		const std::unique_lock lock(mutex);
		if (!camera->HasRenderResource())
		{
			const vk::DeviceSize allocSize = Utils::Align(Scene::Camera::SIZE, uniformBufferAlignment);
			const uint32_t imgs = context->swapChain.GetImageCount();
			ManagedBuffer::Ptr buffer = memPool.CreateBuffer(imgs * allocSize, vk::BufferUsageFlagBits::eUniformBuffer, vk::MemoryPropertyFlagBits::eHostCoherent | vk::MemoryPropertyFlagBits::eHostVisible);
			buffer->Map();
			UniformBuffer* uBuffer = new UniformBuffer();
			uBuffer->Init(std::move(buffer), allocSize, allocSize, GetDescriptorLayoutSet(Scene::Camera::DESCRIPTOR_SET_LAYOUT_BINDING), Scene::Camera::DESCRIPTOR_SET_LAYOUT_BINDING, 1);
			VulkanCamera* vkCam = new VulkanCamera(camera, uBuffer);
			cameras.emplace_back(vkCam);
		}
		return static_cast<VulkanCamera*>(camera->GetRenderResource());
	}

	UniformBuffer* ResourceManager::CreateUniformBuffer(const DescriptorSetLayoutBinding& binding, size_t size, void* data, uint32_t setId, bool hostVis)
	{
		const std::unique_lock lock(mutex);
		const vk::DeviceSize allocSize = Utils::Align(size, uniformBufferAlignment);

		MemoryPool::ManagedBufferPtr buffer;
		if (hostVis)
		{
			buffer = memPool.CreateBuffer(allocSize, vk::BufferUsageFlagBits::eUniformBuffer, vk::MemoryPropertyFlagBits::eHostCoherent | vk::MemoryPropertyFlagBits::eHostVisible);
			buffer->Map();
			buffer->Copy(data, size, 0);
		}
		else
		{
			buffer = CreateDeviceOnlyBufferWithData(allocSize, vk::BufferUsageFlagBits::eUniformBuffer, data);
		}

		UniformBuffer* uBuffer = new UniformBuffer();
		uBuffer->Init(std::move(buffer), 0, allocSize, GetDescriptorLayoutSet(binding), binding, setId);
		return uBuffer;
	}

	vk::DescriptorSetLayout* ResourceManager::GetDescriptorLayoutSet(const DescriptorSetLayoutBinding& descriptorSetLayoutBinding)
	{
		auto& layout = descriptorSetLayoutCache[descriptorSetLayoutBinding];
		if (!layout)
		{
			vk::DescriptorSetLayoutCreateInfo createInfo({}, 1, &reinterpret_cast<const vk::DescriptorSetLayoutBinding&>(descriptorSetLayoutBinding));
			layout = device.createDescriptorSetLayout(createInfo);
		}
		return &layout;
	}

	MemoryPool::ManagedBufferPtr ResourceManager::CreateSharedMemoryBuffer(const size_t size)
	{
		const std::unique_lock lock(mutex);
		return memPool.CreateSharedMemoryBuffer(size);
	}

	void ResourceManager::RemoveShader(VulkanShader* shader)
	{
		if (!transferQueue) return;
		const std::unique_lock lock(mutex);
		std::vector<std::unique_ptr<VulkanShader>>::iterator object =
				find_if(shaders.begin(), shaders.end(),
						[&](auto& obj){ return obj.get() == shader; }
				);
		object->get()->owner = nullptr;
		shaders.erase(object);
	}

	void ResourceManager::CopyDataToImage(vk::DeviceSize size, void* data, Image* image)
	{
		ManagedBuffer::Ptr uploadBuffer = memPool.CreateBuffer(size, vk::BufferUsageFlagBits::eTransferSrc, vk::MemoryPropertyFlagBits::eHostCoherent | vk::MemoryPropertyFlagBits::eHostVisible);
		uploadBuffer->Copy(data, size, 0);

		image->SetLayout(GetCmdBuffer(), vk::ImageAspectFlagBits::eColor, vk::ImageLayout::eTransferDstOptimal);

		vk::BufferImageCopy region(0, 0, 0, { vk::ImageAspectFlagBits::eColor, 0, 0, 1 }, { 0, 0, 0 }, image->extent);
		GetCmdBuffer().copyBufferToImage(uploadBuffer->buffer, image->image, vk::ImageLayout::eTransferDstOptimal, 1, &region);

		image->SetLayout(GetCmdBuffer(), vk::ImageAspectFlagBits::eColor, vk::ImageLayout::eShaderReadOnlyOptimal, vk::ImageLayout::eTransferDstOptimal);

		// TODO set access masks for mip and array layers
		//GetCmdBuffer().pipelineBarrier(vk::PipelineStageFlagBits::eTopOfPipe, vk::PipelineStageFlagBits::eTransfer, {}, 0, nullptr, 0, nullptr, 1, &barrier );
	}

	ManagedBuffer::Ptr ResourceManager::CreateDeviceOnlyBufferWithData(vk::DeviceSize size, vk::BufferUsageFlagBits usage, const void* data)
	{
		ManagedBuffer::Ptr target = memPool.CreateBuffer(size, usage | vk::BufferUsageFlagBits::eTransferDst, vk::MemoryPropertyFlagBits::eDeviceLocal);
		ManagedBuffer::Ptr uploadBuffer = memPool.CreateBuffer(size, vk::BufferUsageFlagBits::eTransferSrc, vk::MemoryPropertyFlagBits::eHostCoherent | vk::MemoryPropertyFlagBits::eHostVisible);
		uploadBuffer->Copy(data, size, 0);
		RecordCopy(uploadBuffer->buffer, target->buffer, size);
		return target;
	}

	VulkanShader* ResourceManager::CreateShader(Scene::Shader* shader)
	{
		const std::unique_lock lock(mutex);
		if (shader->HasRenderResource()) return static_cast<VulkanShader*>(shader->GetRenderResource());
		VulkanShader* vkShader = new VulkanShader(context, shader, this);
		shaders.emplace_back(vkShader);
		return vkShader;
	}

	VulkanTexture* ResourceManager::PrepareTexture(Scene::Texture* texture)
	{
		const std::unique_lock lock(mutex);
		if (texture->HasRenderResource()) return static_cast<VulkanTexture*>(texture->GetRenderResource());
		VulkanTexture* vkTexture;
		if (texture->updateFrequency == Scene::UpdateFrequency::Never)
			vkTexture = new VulkanTexture();
		else
			vkTexture = new VulkanTextureDynamic();

		vkTexture->Init(this, texture, GetDescriptorLayoutSet(Scene::Texture::DESCRIPTOR_SET_LAYOUT_BINDING), Scene::Texture::DESCRIPTOR_SET_LAYOUT_BINDING);

		textures.emplace_back(vkTexture);

		return vkTexture;
	}

	VulkanUniformBuffer* ResourceManager::PrepareUniformBuffer(Scene::UniformBuffer* buffer)
	{
		const std::unique_lock lock(mutex);
		if (buffer->HasRenderResource()) return static_cast<VulkanUniformBuffer*>(buffer->GetRenderResource());

		VulkanUniformBuffer* vkBuffer;
		ManagedBuffer::Ptr mBuffer;
		const vk::DeviceSize allocSize = Utils::Align(buffer->size, uniformBufferAlignment);
		UniformBuffer* uBuffer = new UniformBuffer();
		if (buffer->GetUpdateFrequency() != Scene::UpdateFrequency::Never)
		{
			const uint32_t imgs = context->swapChain.GetImageCount();
			mBuffer = memPool.CreateBuffer(imgs * allocSize, vk::BufferUsageFlagBits::eUniformBuffer, vk::MemoryPropertyFlagBits::eHostCoherent | vk::MemoryPropertyFlagBits::eHostVisible);
			mBuffer->Map();
			vkBuffer = new VulkanUniformBufferDynamic(buffer, uBuffer);
		}
		else
		{
			mBuffer = CreateDeviceOnlyBufferWithData(buffer->size, vk::BufferUsageFlagBits::eUniformBuffer, buffer->data);
			buffer->updated = false;
			vkBuffer = new VulkanUniformBuffer(buffer, uBuffer);
		}

		const uint64_t s = mBuffer->size;
		uBuffer->Init(std::move(mBuffer), 0, s, GetDescriptorLayoutSet(buffer->binding), buffer->binding, buffer->setId);

		uniforms.emplace_back(vkBuffer);
		return vkBuffer;
	}

	vk::Sampler ResourceManager::CreateSampler(const vk::SamplerCreateInfo& samplerConfig)
	{
		auto& sampler = samplerCache[samplerConfig];
		if (!sampler) sampler = device.createSampler(samplerConfig);
		return sampler;
	}

	Unique<Scene::Texture> ResourceManager::CreateSharedMemoryTexture(const Math::Vector3ui& resolution, DataFormat format)
	{
		const std::unique_lock lock(mutex);
		Unique<Scene::Texture> texture = std::make_unique<Scene::Texture>();
		texture->resolution = resolution;
		texture->format = format;
		texture->size = resolution.x * resolution.y * resolution.z * format.GetBytesPerPixel();

		VulkanTexture* vkTexture = new VulkanTexture();
		vkTexture->InitSharedMem(this, texture.get(), GetDescriptorLayoutSet(Scene::Texture::DESCRIPTOR_SET_LAYOUT_BINDING), Scene::Texture::DESCRIPTOR_SET_LAYOUT_BINDING);
		textures.emplace_back(vkTexture);

		return texture;
	}
}
