/*
 * 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 "MemoryPool.hpp"
#include "ManagedBuffer.hpp"
#include "Vulkan/Device.hpp"
#include "Base/Logger.hpp"
#include "Math/ByteSize.hpp"

namespace OpenVulkano::Vulkan
{
	namespace
	{
		std::vector<MemoryPool*> MEM_POOLS;
	}

	void MemoryPool::ReleaseBuffer(OpenVulkano::Vulkan::ManagedBuffer* buffer)
	{
		for (MemoryPool* memPool : MEM_POOLS)
		{
			if (memPool->FreeBuffer(buffer)) return;
		}
		Logger::RENDER->error("Attempted to released buffer to pool, but owning pool no longer exists!");
		buffer->allocation = nullptr; // Allocation is no longer valid since owning pool is gone already
		delete buffer;
	}

	MemoryPool::~MemoryPool()
	{
		if (!allocations.empty()) Close();
	}

	void MemoryPool::Close()
	{
		Utils::Remove(MEM_POOLS, this);
		for(ManagedBuffer* buffer : recycleBuffers)
		{
			delete buffer;
		}
		recycleBuffers.clear();
		for(auto& fF : toFree)
		{
			for(ManagedBuffer* buffer : fF)
			{
				delete buffer;
			}
			fF.clear();
		}
		allocations.clear();
		Logger::RENDER->info("Destroyed gpu memory pool");
	}

	void MemoryPool::Init(Device* dev, int bufferCount)
	{
		device = dev;
		toFree = decltype(toFree)(bufferCount);
		MEM_POOLS.push_back(this);
	}

	void MemoryPool::StartFrame(uint64_t bufferId)
	{
		currentBuffer = bufferId;
		for (ManagedBuffer* i : toFree[currentBuffer])
		{
			DoFree(i);
		}
		toFree[currentBuffer].clear();
	}

	void MemoryPool::DoFree(ManagedBuffer* buffer)
	{
		if (buffer->IsLast())
		{
			device->device.destroyBuffer(buffer->buffer);
			buffer->allocation->used -= buffer->size;
		}
		else
		{
			recycleBuffers.push_back(buffer);
		}
	}

	bool MemoryPool::FreeBuffer(ManagedBuffer* buffer)
	{
		if (buffer) toFree[currentBuffer].push_back(buffer);

		return true;
	}

	MemoryAllocation* MemoryPool::CreateMemoryAllocation(size_t size, uint32_t type, bool addToCache)
	{
		MemoryAllocation* alloc = new MemoryAllocation(size, type, device->device);
		const vk::MemoryAllocateInfo allocInfo = { size, type };
		alloc->memory = device->device.allocateMemory(allocInfo);
		if (addToCache) allocations.emplace_back(alloc);
		return alloc;
	}

	MemoryAllocation* MemoryPool::GetFreeMemoryAllocation(size_t size, vk::DeviceSize alignment, uint32_t type, bool createIfAllFull)
	{
		constexpr size_t ALLOC_SIZE = 128_MiB;
		MemoryAllocation* alloc = nullptr;
		for (auto& allocation : allocations)
		{
			if (allocation->type == type && allocation->FreeSpace(alignment) >= size)
			{
				alloc = allocation.get();
				break;
			}
		}
		if(!alloc && createIfAllFull) alloc = CreateMemoryAllocation(std::max(ALLOC_SIZE, size), type, true);
		return alloc;
	}

	MemoryPool::ManagedBufferPtr MemoryPool::CreateBuffer(vk::DeviceSize size, const vk::BufferUsageFlags& usage, const vk::MemoryPropertyFlags& properties)
	{
		size = Utils::Align(size, 16);
		vk::BufferCreateInfo bufferCreateInfo = { {}, size, usage, vk::SharingMode::eExclusive };
		vk::Buffer buffer = device->device.createBuffer(bufferCreateInfo);
		const vk::MemoryRequirements memoryRequirements = device->device.getBufferMemoryRequirements(buffer);
		uint32_t memtype = device->GetMemoryType(memoryRequirements.memoryTypeBits, properties);
		if (memoryRequirements.size != size)
		{
			Logger::DATA->debug("Memory Requirement Size ({0}) != Size ({1})", memoryRequirements.size, size);
			size = memoryRequirements.size;
			device->device.destroy(buffer);
			bufferCreateInfo.size = size;
			buffer = device->device.createBuffer(bufferCreateInfo);
		}
		MemoryAllocation* allocation = GetFreeMemoryAllocation(size, memoryRequirements.alignment, memtype);
		uint32_t offset = Utils::Align(allocation->used, memoryRequirements.alignment);
		device->device.bindBufferMemory(buffer, allocation->memory, offset);
		allocation->used += size + (offset - allocation->used);
		return ManagedBufferPtr{ new ManagedBuffer(allocation, offset, size, buffer, usage, properties) };
	}

	MemoryPool::ManagedBufferPtr MemoryPool::CreateSharedMemoryBuffer(size_t size)
	{
		if (!recycleBuffers.empty())
		{
			for(auto buff : recycleBuffers)
			{
				if (buff->size == size)
				{
					Logger::DATA->info("Recycle Buffer");
					Utils::Remove(recycleBuffers, buff);
					return ManagedBufferPtr{ buff };
				}
			}
		}
		return CreateBuffer(size, vk::BufferUsageFlagBits::eVertexBuffer, vk::MemoryPropertyFlagBits::eHostCoherent/* | vk::MemoryPropertyFlagBits::eDeviceLocal*/);
	}

	void ManagedBufferDeleter::operator()(ManagedBuffer* buffer)
	{
		MemoryPool::ReleaseBuffer(buffer);
	}
}
