/*
 * 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/.
 */

#pragma once

#define CRASH_ON_MULTIPLE_MAPPINGS_TO_SAME_ALLOCATION

#include "MemoryAllocation.hpp"
#include "MemoryPool.hpp"
#include <memory>
#include <functional>

namespace OpenVulkano::Vulkan
{
	class ManagedBuffer
	{
	public:
		using Ptr = std::unique_ptr<ManagedBuffer, ManagedBufferDeleter>;

		MemoryAllocation* allocation;
		vk::DeviceSize offset, size;
		vk::Buffer buffer;
		vk::BufferUsageFlags usage;
		vk::MemoryPropertyFlags properties;
		void* mapped = nullptr;

		ManagedBuffer(MemoryAllocation* alloc, vk::DeviceSize offset, vk::DeviceSize size, vk::Buffer buffer, vk::BufferUsageFlags usageFlags, vk::MemoryPropertyFlags  memProperties)
				: allocation(alloc), offset(offset), size(size), buffer(buffer), usage(usageFlags), properties(memProperties), mapped(nullptr)
		{}

		~ManagedBuffer()
		{
			if (allocation) [[likely]] allocation->device.destroy(buffer);
		}

		[[nodiscard]] bool IsLast() const
		{
			return (offset + size == allocation->used);
		}

		[[nodiscard]] bool IsMapped() const
		{
			return mapped || allocation->mapped;
		}

		[[nodiscard]] void* GetMappedMemory() const { return mapped; }

		template <typename T = char>
		[[nodiscard]] T* GetMappedMemory() const { return static_cast<T*>(mapped); }

		/**
		 * \brief Maps the buffer into the memory of the host.
		 * \tparam T The type of the buffers data.
		 * \param offset The offset from where to map the buffer.
		 * \param size The size to be mapped. VK_WHOLE_SIZE to map the whole buffer.
		 * \pparam longTermMapping If the mapping is intended to be held long term. Short term mappings must be freed before mapping a different region in the same memory allocation for them to work reliable with all drivers.
		 * \return The pointer to the mapped buffer.
		 */
		template <typename T = void>
		T* Map(vk::DeviceSize offset = 0, vk::DeviceSize size = VK_WHOLE_SIZE, bool longTermMapping = true)
		{
			if (!mapped)
			{
				if (allocation->mapped || longTermMapping)
				{
					mapped = static_cast<uint8_t*>(allocation->Map()) + offset + this->offset;
				}
				else
				{
					if (size == VK_WHOLE_SIZE) size = this->size;
					mapped = allocation->MapChild(this->offset + offset, size);
				}
			}
			
			return static_cast<T*>(mapped);
		}

		/**
		 * \brief Un-maps the buffer from the host.
		 */
		void UnMap()
		{
			if (mapped)
			{
				if (allocation->mapped)
				{
					allocation->UnMap();
				}
				else
				{
					allocation->UnMapChild();
				}
				mapped = nullptr;
			}
		}

		void Copy(void* data)
		{
			if (mapped)
			{
				memcpy(mapped, data, size);
			}
			else
			{
				void* dataMapped = Map(0, VK_WHOLE_SIZE, false);
				memcpy(dataMapped, data, size);
				UnMap();
			}
		}

		void Copy(const void* data, vk::DeviceSize size, vk::DeviceSize offset)
		{
			if(mapped) memcpy(static_cast<char*>(mapped) + offset, data, size);
			else
			{
				void* dataMapped = Map(offset, size, false);
				memcpy(dataMapped, data, size);
				UnMap();
			}
		}
	};
}
