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

#include <vulkan/vulkan.hpp>
#include <set>
#include <functional>
#include <fstream>
#include "Base/ICloseable.hpp"

namespace OpenVulkano
{
	namespace Vulkan
	{
		class Device : public ICloseable
		{
		public:
			vk::PhysicalDevice physicalDevice;
			std::vector<vk::QueueFamilyProperties> queueFamilyProperties; // Queue family properties
			vk::PhysicalDeviceProperties properties; // Physical device properties (for e.g. checking device limits)
			vk::PhysicalDeviceFeatures features; // Physical device features (for e.g. checking if a feature is available)
			vk::PhysicalDeviceMemoryProperties memoryProperties; // available memory properties
			vk::Device device; // Logical device, application's view of the physical device (GPU)
			vk::PipelineCache pipelineCache;
			vk::CommandPool graphicsCommandPool;
			std::set<std::string> supportedExtensions;
			vk::Queue graphicsQueue;

			struct QueueIndices {
				uint32_t graphics = VK_QUEUE_FAMILY_IGNORED;
				uint32_t compute = VK_QUEUE_FAMILY_IGNORED;
				uint32_t transfer = VK_QUEUE_FAMILY_IGNORED;

				uint32_t GetGraphics() const { return graphics; }
				uint32_t GetCompute() const { return compute != graphics ? compute : VK_QUEUE_FAMILY_IGNORED; }
				uint32_t GetTransfer() const { return (transfer != graphics && transfer != compute) ? transfer : VK_QUEUE_FAMILY_IGNORED; }
			} queueIndices;

			bool useDebugMarkers;

		public:
			Device(vk::PhysicalDevice& physicalDevice);

			~Device() { if (device) Close(); }

			[[nodiscard]] std::string GetDeviceName() const
			{
				return properties.deviceName.data();
			}

			void PrepareDevice(const vk::ArrayProxy<const std::string>& requestedExtensions, const vk::SurfaceKHR& surface);

			[[nodiscard]] const std::set<std::string>& GetExtensions() const
			{
				return supportedExtensions;
			}

			[[nodiscard]] bool IsExtensionAvailable(const vk::ArrayProxy<const std::string>& extensions) const
			{
				for(const auto& extension : extensions)
				{
					if (supportedExtensions.count(extension) == 0) return false;
				}
				return true;
			}

			void WaitIdle() const;


			[[nodiscard]] vk::CommandBuffer CreateCommandBuffer(vk::CommandBufferLevel level = vk::CommandBufferLevel::ePrimary) const;

			void FlushCommandBuffer(vk::CommandBuffer& cmdBuffer) const;

			void ExecuteNow(const std::function<void(const vk::CommandBuffer& commandBuffer)>& function) const;

			[[nodiscard]] vk::ShaderModule CreateShaderModule(const std::string& filename) const;

			vk::ShaderModule CreateShaderModule(vk::ShaderModuleCreateInfo& createInfo) const;

		private:
			void QueryDevice();

			void BuildDevice(const vk::ArrayProxy<const std::string>& requestedExtensions);

			[[nodiscard]] uint32_t FindBestQueue(const vk::QueueFlags& desiredFlags, const vk::SurfaceKHR& surface = nullptr) const;

		public:
			/**
			 * \brief Vulkan does not define a depth buffer format that must be supported. This method checks for the first supported format from an given array of formats.
			 * \param depthFormats Array of depth formats
			 * \return The first format supported as a depth buffer
			 * \throws If no depth buffer format is supported
			 */
			[[nodiscard]] vk::Format GetSupportedDepthFormat(const std::vector<vk::Format>& depthFormats = { vk::Format::eD32SfloatS8Uint, vk::Format::eD32Sfloat, vk::Format::eD24UnormS8Uint, vk::Format::eD16UnormS8Uint, vk::Format::eD16Unorm }) const;

			bool GetMemoryType(uint32_t typeBits, const vk::MemoryPropertyFlags& properties, uint32_t* typeIndex) const;

			[[nodiscard]] uint32_t GetMemoryType(uint32_t typeBits, const vk::MemoryPropertyFlags& properties) const;

			[[nodiscard]] vk::MemoryPropertyFlags GetMemoryFlags(uint32_t memoryType) const
			{
				return memoryProperties.memoryTypes[memoryType].propertyFlags;
			}

			void Close() override;
		};
	}
}
