/**
 * @authors Artur Wasmut
 * @file src/vkcv/Core.cpp
 * @brief Handling of global states regarding dependencies
 */

#include <GLFW/glfw3.h>

#include "vkcv/Core.hpp"
#include "PassManager.hpp"
#include "PipelineManager.hpp"
#include "vkcv/BufferManager.hpp"
#include "SamplerManager.hpp"
#include "ImageManager.hpp"
#include "DescriptorManager.hpp"
#include "ImageLayoutTransitions.hpp"
#include "vkcv/CommandStreamManager.hpp"
#include <cmath>
#include "vkcv/Logger.hpp"

namespace vkcv
{
	
	static std::vector<vk::ImageView> createSwapchainImageViews( Context &context, const std::vector<vk::Image>& images,
																 vk::Format format){
		std::vector<vk::ImageView> imageViews;
		imageViews.reserve( images.size() );
		//here can be swizzled with vk::ComponentSwizzle if needed
		vk::ComponentMapping componentMapping(
				vk::ComponentSwizzle::eR,
				vk::ComponentSwizzle::eG,
				vk::ComponentSwizzle::eB,
				vk::ComponentSwizzle::eA );
		
		vk::ImageSubresourceRange subResourceRange( vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1 );
		
		for ( auto image : images )
		{
			vk::ImageViewCreateInfo imageViewCreateInfo(
					vk::ImageViewCreateFlags(),
					image,
					vk::ImageViewType::e2D,
					format,
					componentMapping,
					subResourceRange);
			
			imageViews.push_back(context.getDevice().createImageView(imageViewCreateInfo));
		}
		
		return imageViews;
	}

    Core Core::create(Window &window,
                      const char *applicationName,
                      uint32_t applicationVersion,
                      const std::vector<vk::QueueFlagBits>& queueFlags,
					  const Features& features,
                      const std::vector<const char *>& instanceExtensions)
    {
        Context context = Context::create(
        		applicationName, applicationVersion,
        		queueFlags,
				features,
        		instanceExtensions
		);

        Swapchain swapChain = Swapchain::create(window, context);
	
		const auto swapchainImages = context.getDevice().getSwapchainImagesKHR(swapChain.getSwapchain());
		const auto swapchainImageViews = createSwapchainImageViews( context, swapchainImages, swapChain.getFormat());

        const auto& queueManager = context.getQueueManager();
        
		const std::unordered_set<int>	queueFamilySet			= generateQueueFamilyIndexSet(queueManager);
		const auto						commandResources		= createCommandResources(context.getDevice(), queueFamilySet);
		const auto						defaultSyncResources	= createSyncResources(context.getDevice());

        return Core(std::move(context) , window, swapChain, swapchainImageViews, commandResources, defaultSyncResources);
    }

    const Context &Core::getContext() const
    {
        return m_Context;
    }
    
    const Swapchain& Core::getSwapchain() const {
    	return m_swapchain;
    }

    Core::Core(Context &&context, Window &window, const Swapchain& swapChain,  std::vector<vk::ImageView> swapchainImageViews,
        const CommandResources& commandResources, const SyncResources& syncResources) noexcept :
            m_Context(std::move(context)),
			m_swapchain(swapChain),
            m_window(window),
            m_PassManager{std::make_unique<PassManager>(m_Context.m_Device)},
            m_PipelineManager{std::make_unique<PipelineManager>(m_Context.m_Device)},
            m_DescriptorManager(std::make_unique<DescriptorManager>(m_Context.m_Device)),
            m_BufferManager{std::unique_ptr<BufferManager>(new BufferManager())},
            m_SamplerManager(std::unique_ptr<SamplerManager>(new SamplerManager(m_Context.m_Device))),
            m_ImageManager{std::unique_ptr<ImageManager>(new ImageManager(*m_BufferManager))},
            m_CommandStreamManager{std::unique_ptr<CommandStreamManager>(new CommandStreamManager)},
            m_CommandResources(commandResources),
            m_SyncResources(syncResources)
	{
		m_BufferManager->m_core = this;
		m_BufferManager->init();
		m_CommandStreamManager->init(this);

		m_ImageManager->m_core = this;
		
		e_resizeHandle = m_window.e_resize.add( [&](int width, int height) {
			m_swapchain.signalSwapchainRecreation();
		});

		const auto swapchainImages = m_Context.getDevice().getSwapchainImagesKHR(m_swapchain.getSwapchain());
		m_ImageManager->setSwapchainImages(
			swapchainImages, 
			swapchainImageViews, 
			swapChain.getExtent().width,
			swapChain.getExtent().height,
			swapChain.getFormat()
		);
	}

	Core::~Core() noexcept {
    	m_window.e_resize.remove(e_resizeHandle);
    	
		m_Context.getDevice().waitIdle();

		destroyCommandResources(m_Context.getDevice(), m_CommandResources);
		destroySyncResources(m_Context.getDevice(), m_SyncResources);

		m_Context.m_Device.destroySwapchainKHR(m_swapchain.getSwapchain());
		m_Context.m_Instance.destroySurfaceKHR(m_swapchain.getSurface());
	}

    PipelineHandle Core::createGraphicsPipeline(const PipelineConfig &config)
    {
        return m_PipelineManager->createPipeline(config, *m_PassManager);
    }

    PipelineHandle Core::createComputePipeline(
        const ShaderProgram &shaderProgram, 
        const std::vector<vk::DescriptorSetLayout>& descriptorSetLayouts)
    {
        return m_PipelineManager->createComputePipeline(shaderProgram, descriptorSetLayouts);
    }

    PassHandle Core::createPass(const PassConfig &config)
    {
        return m_PassManager->createPass(config);
    }

	Result Core::acquireSwapchainImage() {
    	uint32_t imageIndex;
    	vk::Result result;
    	
		try {
			result = m_Context.getDevice().acquireNextImageKHR(
					m_swapchain.getSwapchain(),
					std::numeric_limits<uint64_t>::max(),
					m_SyncResources.swapchainImageAcquired,
					nullptr,
					&imageIndex, {}
			);
		} catch (const vk::OutOfDateKHRError& e) {
			result = vk::Result::eErrorOutOfDateKHR;
		} catch (const vk::DeviceLostError& e) {
			result = vk::Result::eErrorDeviceLost;
		}
		
		if ((result != vk::Result::eSuccess) &&
			(result != vk::Result::eSuboptimalKHR)) {
			vkcv_log(LogLevel::ERROR, "%s", vk::to_string(result).c_str());
			return Result::ERROR;
		} else
		if (result == vk::Result::eSuboptimalKHR) {
			vkcv_log(LogLevel::WARNING, "Acquired image is suboptimal");
			m_swapchain.signalSwapchainRecreation();
		}
		
		m_currentSwapchainImageIndex = imageIndex;
		return Result::SUCCESS;
	}

	bool Core::beginFrame(uint32_t& width, uint32_t& height) {
		if (m_swapchain.shouldUpdateSwapchain()) {
			m_Context.getDevice().waitIdle();

			m_swapchain.updateSwapchain(m_Context, m_window);
			
			if (!m_swapchain.getSwapchain()) {
				return false;
			}
			
			const auto swapchainImages = m_Context.getDevice().getSwapchainImagesKHR(m_swapchain.getSwapchain());
			const auto swapchainViews = createSwapchainImageViews(m_Context, swapchainImages, m_swapchain.getFormat());
			
			const auto& extent = m_swapchain.getExtent();

			m_ImageManager->setSwapchainImages(
					swapchainImages,
					swapchainViews,
					extent.width, extent.height,
					m_swapchain.getFormat()
			);
		}
		
		const auto& extent = m_swapchain.getExtent();
		
		width = extent.width;
		height = extent.height;
		
		if ((width < MIN_SWAPCHAIN_SIZE) || (height < MIN_SWAPCHAIN_SIZE)) {
			return false;
		}
		
    	if (acquireSwapchainImage() != Result::SUCCESS) {
			vkcv_log(LogLevel::ERROR, "Acquire failed");
    		
    		m_currentSwapchainImageIndex = std::numeric_limits<uint32_t>::max();
    	}
		
		m_Context.getDevice().waitIdle(); // TODO: this is a sin against graphics programming, but its getting late - Alex
		
		m_ImageManager->setCurrentSwapchainImageIndex(m_currentSwapchainImageIndex);

		return (m_currentSwapchainImageIndex != std::numeric_limits<uint32_t>::max());
	}

	std::array<uint32_t, 2> getWidthHeightFromRenderTargets(
		const std::vector<ImageHandle>& renderTargets,
		const Swapchain& swapchain,
		const ImageManager& imageManager) {

		std::array<uint32_t, 2> widthHeight;

		if (renderTargets.size() > 0) {
			const vkcv::ImageHandle firstImage = renderTargets[0];
			if (firstImage.isSwapchainImage()) {
				const auto& swapchainExtent = swapchain.getExtent();
				widthHeight[0] = swapchainExtent.width;
				widthHeight[1] = swapchainExtent.height;
			}
			else {
				widthHeight[0] = imageManager.getImageWidth(firstImage);
				widthHeight[1] = imageManager.getImageHeight(firstImage);
			}
		}
		else {
			widthHeight[0] = 1;
			widthHeight[1] = 1;
		}
		// TODO: validate that width/height match for all attachments
		return widthHeight;
	}

	vk::Framebuffer createFramebuffer(
		const std::vector<ImageHandle>& renderTargets,
		const ImageManager&             imageManager,
		const Swapchain&                swapchain,
		vk::RenderPass                  renderpass,
		vk::Device                      device) {

		std::vector<vk::ImageView> attachmentsViews;
		for (const ImageHandle& handle : renderTargets) {
			attachmentsViews.push_back(imageManager.getVulkanImageView(handle));
		}

		const std::array<uint32_t, 2> widthHeight = getWidthHeightFromRenderTargets(renderTargets, swapchain, imageManager);

		const vk::FramebufferCreateInfo createInfo(
			{},
			renderpass,
			static_cast<uint32_t>(attachmentsViews.size()),
			attachmentsViews.data(),
			widthHeight[0],
			widthHeight[1],
			1);

		return device.createFramebuffer(createInfo);
	}

	void transitionRendertargetsToAttachmentLayout(
		const std::vector<ImageHandle>& renderTargets,
		ImageManager&                   imageManager,
		const vk::CommandBuffer         cmdBuffer) {

		for (const ImageHandle& handle : renderTargets) {
			const bool isDepthImage = isDepthFormat(imageManager.getImageFormat(handle));
			const vk::ImageLayout targetLayout =
				isDepthImage ? vk::ImageLayout::eDepthStencilAttachmentOptimal : vk::ImageLayout::eColorAttachmentOptimal;
			imageManager.recordImageLayoutTransition(handle, targetLayout, cmdBuffer);
		}
	}

	std::vector<vk::ClearValue> createAttachmentClearValues(const std::vector<AttachmentDescription>& attachments) {
		std::vector<vk::ClearValue> clearValues;
		for (const auto& attachment : attachments) {
			if (attachment.load_operation == AttachmentOperation::CLEAR) {
				float clear = 0.0f;

				if (isDepthFormat(attachment.format)) {
					clear = 1.0f;
				}

				clearValues.emplace_back(std::array<float, 4>{
					clear,
						clear,
						clear,
						1.f
				});
			}
		}
		return clearValues;
	}

	void recordDynamicViewport(vk::CommandBuffer cmdBuffer, uint32_t width, uint32_t height) {
		vk::Viewport dynamicViewport(
			0.0f, 0.0f,
			static_cast<float>(width), static_cast<float>(height),
			0.0f, 1.0f
		);

		vk::Rect2D dynamicScissor({ 0, 0 }, { width, height });

		cmdBuffer.setViewport(0, 1, &dynamicViewport);
		cmdBuffer.setScissor(0, 1, &dynamicScissor);
	}

	void Core::recordDrawcallsToCmdStream(
		const CommandStreamHandle&      cmdStreamHandle,
		const PassHandle&               renderpassHandle,
		const PipelineHandle            pipelineHandle, 
        const PushConstants             &pushConstantData,
        const std::vector<DrawcallInfo> &drawcalls,
		const std::vector<ImageHandle>  &renderTargets) {

		if (m_currentSwapchainImageIndex == std::numeric_limits<uint32_t>::max()) {
			return;
		}

		const std::array<uint32_t, 2> widthHeight = getWidthHeightFromRenderTargets(renderTargets, m_swapchain, *m_ImageManager);
		const auto width  = widthHeight[0];
		const auto height = widthHeight[1];

		const vk::RenderPass        renderpass      = m_PassManager->getVkPass(renderpassHandle);
		const PassConfig            passConfig      = m_PassManager->getPassConfig(renderpassHandle);

		const vk::Pipeline          pipeline        = m_PipelineManager->getVkPipeline(pipelineHandle);
		const vk::PipelineLayout    pipelineLayout  = m_PipelineManager->getVkPipelineLayout(pipelineHandle);
		const vk::Rect2D            renderArea(vk::Offset2D(0, 0), vk::Extent2D(width, height));

		vk::CommandBuffer cmdBuffer = m_CommandStreamManager->getStreamCommandBuffer(cmdStreamHandle);
		transitionRendertargetsToAttachmentLayout(renderTargets, *m_ImageManager, cmdBuffer);

		const vk::Framebuffer framebuffer = createFramebuffer(renderTargets, *m_ImageManager, m_swapchain, renderpass, m_Context.m_Device);

		if (!framebuffer) {
			vkcv_log(LogLevel::ERROR, "Failed to create temporary framebuffer");
			return;
		}

		SubmitInfo submitInfo;
		submitInfo.queueType = QueueType::Graphics;
		submitInfo.signalSemaphores = { m_SyncResources.renderFinished };

		auto submitFunction = [&](const vk::CommandBuffer& cmdBuffer) {
			const std::vector<vk::ClearValue> clearValues = createAttachmentClearValues(passConfig.attachments);

			const vk::RenderPassBeginInfo beginInfo(renderpass, framebuffer, renderArea, clearValues.size(), clearValues.data());
			cmdBuffer.beginRenderPass(beginInfo, {}, {});

			cmdBuffer.bindPipeline(vk::PipelineBindPoint::eGraphics, pipeline, {});

			const PipelineConfig &pipeConfig = m_PipelineManager->getPipelineConfig(pipelineHandle);
			if (pipeConfig.m_UseDynamicViewport) {
				recordDynamicViewport(cmdBuffer, width, height);
			}

			for (size_t i = 0; i < drawcalls.size(); i++) {
				recordDrawcall(drawcalls[i], cmdBuffer, pipelineLayout, pushConstantData, i);
			}

			cmdBuffer.endRenderPass();
		};

		auto finishFunction = [framebuffer, this]()
		{
			m_Context.m_Device.destroy(framebuffer);
		};

		recordCommandsToStream(cmdStreamHandle, submitFunction, finishFunction);
	}

	void Core::recordMeshShaderDrawcalls(
		const CommandStreamHandle&                          cmdStreamHandle,
		const PassHandle&                                   renderpassHandle,
		const PipelineHandle                                pipelineHandle,
		const PushConstants&                                pushConstantData,
		const std::vector<MeshShaderDrawcall>&              drawcalls,
		const std::vector<ImageHandle>&                     renderTargets) {

		if (m_currentSwapchainImageIndex == std::numeric_limits<uint32_t>::max()) {
			return;
		}

		const std::array<uint32_t, 2> widthHeight = getWidthHeightFromRenderTargets(renderTargets, m_swapchain, *m_ImageManager);
		const auto width  = widthHeight[0];
		const auto height = widthHeight[1];

		const vk::RenderPass        renderpass = m_PassManager->getVkPass(renderpassHandle);
		const PassConfig            passConfig = m_PassManager->getPassConfig(renderpassHandle);

		const vk::Pipeline          pipeline = m_PipelineManager->getVkPipeline(pipelineHandle);
		const vk::PipelineLayout    pipelineLayout = m_PipelineManager->getVkPipelineLayout(pipelineHandle);
		const vk::Rect2D            renderArea(vk::Offset2D(0, 0), vk::Extent2D(width, height));

		vk::CommandBuffer cmdBuffer = m_CommandStreamManager->getStreamCommandBuffer(cmdStreamHandle);
		transitionRendertargetsToAttachmentLayout(renderTargets, *m_ImageManager, cmdBuffer);

		const vk::Framebuffer framebuffer = createFramebuffer(renderTargets, *m_ImageManager, m_swapchain, renderpass, m_Context.m_Device);

		if (!framebuffer) {
			vkcv_log(LogLevel::ERROR, "Failed to create temporary framebuffer");
			return;
		}

		SubmitInfo submitInfo;
		submitInfo.queueType = QueueType::Graphics;
		submitInfo.signalSemaphores = { m_SyncResources.renderFinished };

		auto submitFunction = [&](const vk::CommandBuffer& cmdBuffer) {
			const std::vector<vk::ClearValue> clearValues = createAttachmentClearValues(passConfig.attachments);

			const vk::RenderPassBeginInfo beginInfo(renderpass, framebuffer, renderArea, clearValues.size(), clearValues.data());
			cmdBuffer.beginRenderPass(beginInfo, {}, {});

			cmdBuffer.bindPipeline(vk::PipelineBindPoint::eGraphics, pipeline, {});

			const PipelineConfig& pipeConfig = m_PipelineManager->getPipelineConfig(pipelineHandle);
			if (pipeConfig.m_UseDynamicViewport) {
				recordDynamicViewport(cmdBuffer, width, height);
			}

			for (size_t i = 0; i < drawcalls.size(); i++) {
                const uint32_t pushConstantOffset = i * pushConstantData.getSizePerDrawcall();
                recordMeshShaderDrawcall(
                    cmdBuffer,
                    pipelineLayout,
                    pushConstantData,
                    pushConstantOffset,
                    drawcalls[i],
                    0
				);
			}

			cmdBuffer.endRenderPass();
		};

		auto finishFunction = [framebuffer, this]() {
			m_Context.m_Device.destroy(framebuffer);
		};

		recordCommandsToStream(cmdStreamHandle, submitFunction, finishFunction);
	}

	void Core::recordComputeDispatchToCmdStream(
		CommandStreamHandle cmdStreamHandle,
		PipelineHandle computePipeline,
		const uint32_t dispatchCount[3],
		const std::vector<DescriptorSetUsage>& descriptorSetUsages,
		const PushConstants& pushConstants) {

		auto submitFunction = [&](const vk::CommandBuffer& cmdBuffer) {

			const auto pipelineLayout = m_PipelineManager->getVkPipelineLayout(computePipeline);

			cmdBuffer.bindPipeline(vk::PipelineBindPoint::eCompute, m_PipelineManager->getVkPipeline(computePipeline));
			for (const auto& usage : descriptorSetUsages) {
				cmdBuffer.bindDescriptorSets(
					vk::PipelineBindPoint::eCompute,
					pipelineLayout,
					usage.setLocation,
					{ usage.vulkanHandle },
					usage.dynamicOffsets
				);
			}
			if (pushConstants.getSizePerDrawcall() > 0) {
				cmdBuffer.pushConstants(
					pipelineLayout,
					vk::ShaderStageFlagBits::eCompute,
					0,
					pushConstants.getSizePerDrawcall(),
					pushConstants.getData());
			}
			cmdBuffer.dispatch(dispatchCount[0], dispatchCount[1], dispatchCount[2]);
		};

		recordCommandsToStream(cmdStreamHandle, submitFunction, nullptr);
	}
	
	void Core::recordBeginDebugLabel(const CommandStreamHandle &cmdStream,
									 const std::string& label,
									 const std::array<float, 4>& color) {
#ifndef NDEBUG
		static PFN_vkCmdBeginDebugUtilsLabelEXT beginDebugLabel = reinterpret_cast<PFN_vkCmdBeginDebugUtilsLabelEXT>(
				m_Context.getDevice().getProcAddr("vkCmdBeginDebugUtilsLabelEXT")
		);
		
		if (!beginDebugLabel) {
			return;
		}
		
		auto submitFunction = [&](const vk::CommandBuffer& cmdBuffer) {
			const vk::DebugUtilsLabelEXT debug (
					label.c_str(),
					color
			);
			
			beginDebugLabel(cmdBuffer, &(static_cast<const VkDebugUtilsLabelEXT&>(debug)));
		};

		recordCommandsToStream(cmdStream, submitFunction, nullptr);
#endif
	}
	
	void Core::recordEndDebugLabel(const CommandStreamHandle &cmdStream) {
#ifndef NDEBUG
		static PFN_vkCmdEndDebugUtilsLabelEXT endDebugLabel = reinterpret_cast<PFN_vkCmdEndDebugUtilsLabelEXT>(
				m_Context.getDevice().getProcAddr("vkCmdEndDebugUtilsLabelEXT")
		);
		
		if (!endDebugLabel) {
			return;
		}
		
		auto submitFunction = [&](const vk::CommandBuffer& cmdBuffer) {
			endDebugLabel(cmdBuffer);
		};

		recordCommandsToStream(cmdStream, submitFunction, nullptr);
#endif
	}
	
	void Core::recordComputeIndirectDispatchToCmdStream(
		const CommandStreamHandle               cmdStream,
		const PipelineHandle                    computePipeline,
		const vkcv::BufferHandle                buffer,
		const size_t                            bufferArgOffset,
		const std::vector<DescriptorSetUsage>&  descriptorSetUsages,
		const PushConstants&                    pushConstants) {

		auto submitFunction = [&](const vk::CommandBuffer& cmdBuffer) {

			const auto pipelineLayout = m_PipelineManager->getVkPipelineLayout(computePipeline);

			cmdBuffer.bindPipeline(vk::PipelineBindPoint::eCompute, m_PipelineManager->getVkPipeline(computePipeline));
			for (const auto& usage : descriptorSetUsages) {
				cmdBuffer.bindDescriptorSets(
					vk::PipelineBindPoint::eCompute,
					pipelineLayout,
					usage.setLocation,
					{ usage.vulkanHandle },
					usage.dynamicOffsets
				);
			}
			if (pushConstants.getSizePerDrawcall() > 0) {
				cmdBuffer.pushConstants(
					pipelineLayout,
					vk::ShaderStageFlagBits::eCompute,
					0,
					pushConstants.getSizePerDrawcall(),
					pushConstants.getData());
			}
			cmdBuffer.dispatchIndirect(m_BufferManager->getBuffer(buffer), bufferArgOffset);
		};

		recordCommandsToStream(cmdStream, submitFunction, nullptr);
	}

	void Core::endFrame() {
		if (m_currentSwapchainImageIndex == std::numeric_limits<uint32_t>::max()) {
			return;
		}
  
		const auto swapchainImages = m_Context.getDevice().getSwapchainImagesKHR(m_swapchain.getSwapchain());

		const auto& queueManager = m_Context.getQueueManager();
		std::array<vk::Semaphore, 2> waitSemaphores{
			m_SyncResources.renderFinished,
			m_SyncResources.swapchainImageAcquired
		};

		const vk::SwapchainKHR& swapchain = m_swapchain.getSwapchain();
		const vk::PresentInfoKHR presentInfo(
			waitSemaphores,
			swapchain,
			m_currentSwapchainImageIndex
		);
		
		vk::Result result;
		
		try {
			result = queueManager.getPresentQueue().handle.presentKHR(presentInfo);
		} catch (const vk::OutOfDateKHRError& e) {
			result = vk::Result::eErrorOutOfDateKHR;
		} catch (const vk::DeviceLostError& e) {
			result = vk::Result::eErrorDeviceLost;
		}
		
		if ((result != vk::Result::eSuccess) &&
			(result != vk::Result::eSuboptimalKHR)) {
			vkcv_log(LogLevel::ERROR, "Swapchain presentation failed (%s)", vk::to_string(result).c_str());
		} else
		if (result == vk::Result::eSuboptimalKHR) {
			vkcv_log(LogLevel::WARNING, "Swapchain presentation is suboptimal");
			m_swapchain.signalSwapchainRecreation();
		}
	}
	
	void Core::recordAndSubmitCommandsImmediate(
		const SubmitInfo &submitInfo, 
		const RecordCommandFunction &record, 
		const FinishCommandFunction &finish)
	{
		const vk::Device& device = m_Context.getDevice();

		const vkcv::Queue		queue		= getQueueForSubmit(submitInfo.queueType, m_Context.getQueueManager());
		const vk::CommandPool	cmdPool		= chooseCmdPool(queue, m_CommandResources);
		const vk::CommandBuffer	cmdBuffer	= allocateCommandBuffer(device, cmdPool);

		beginCommandBuffer(cmdBuffer, vk::CommandBufferUsageFlagBits::eOneTimeSubmit);
		record(cmdBuffer);
		cmdBuffer.end();
		
		vk::Fence waitFence = createFence(device);

		submitCommandBufferToQueue(queue.handle, cmdBuffer, waitFence, submitInfo.waitSemaphores, submitInfo.signalSemaphores);
		waitForFence(device, waitFence);
		
		device.destroyFence(waitFence);
		
		device.freeCommandBuffers(cmdPool, cmdBuffer);
		
		if (finish) {
			finish();
		}
	}

	CommandStreamHandle Core::createCommandStream(QueueType queueType) {
		const vkcv::Queue       queue   = getQueueForSubmit(queueType, m_Context.getQueueManager());
		const vk::CommandPool   cmdPool = chooseCmdPool(queue, m_CommandResources);

		return m_CommandStreamManager->createCommandStream(queue.handle, cmdPool);
	}

    void Core::recordCommandsToStream(
		const CommandStreamHandle   cmdStreamHandle,
		const RecordCommandFunction &record, 
		const FinishCommandFunction &finish) {

		m_CommandStreamManager->recordCommandsToStream(cmdStreamHandle, record);
		if (finish) {
			m_CommandStreamManager->addFinishCallbackToStream(cmdStreamHandle, finish);
		}
	}

	void Core::submitCommandStream(const CommandStreamHandle& handle) {
		std::vector<vk::Semaphore> waitSemaphores;
		// FIXME: add proper user controllable sync
		std::vector<vk::Semaphore> signalSemaphores = { m_SyncResources.renderFinished };
		m_CommandStreamManager->submitCommandStreamSynchronous(handle, waitSemaphores, signalSemaphores);
	}

	SamplerHandle Core::createSampler(SamplerFilterType magFilter, SamplerFilterType minFilter,
									  SamplerMipmapMode mipmapMode, SamplerAddressMode addressMode,
									  float mipLodBias) {
		return m_SamplerManager->createSampler(magFilter, minFilter, mipmapMode, addressMode, mipLodBias);
	}

	Image Core::createImage(
		vk::Format      format,
		uint32_t        width,
		uint32_t        height,
		uint32_t        depth,
		bool            createMipChain,
		bool            supportStorage,
		bool            supportColorAttachment,
		Multisampling   multisampling)
	{

		uint32_t mipCount = 1;
		if (createMipChain) {
			mipCount = 1 + (uint32_t)std::floor(std::log2(std::max(width, std::max(height, depth))));
		}

		return Image::create(
			m_ImageManager.get(), 
			format,
			width,
			height,
			depth,
			mipCount,
			supportStorage,
			supportColorAttachment,
			multisampling);
	}

	uint32_t Core::getImageWidth(const ImageHandle& image)
	{
		return m_ImageManager->getImageWidth(image);
	}

	uint32_t Core::getImageHeight(const ImageHandle& image)
	{
		return m_ImageManager->getImageHeight(image);
	}
	
	vk::Format Core::getImageFormat(const ImageHandle& image) {
		return m_ImageManager->getImageFormat(image);
	}

    DescriptorSetHandle Core::createDescriptorSet(const std::vector<DescriptorBinding>& bindings)
    {
        return m_DescriptorManager->createDescriptorSet(bindings);
    }

	void Core::writeDescriptorSet(DescriptorSetHandle handle, const DescriptorWrites &writes) {
		m_DescriptorManager->writeDescriptorSet(
			handle,
			writes, 
			*m_ImageManager, 
			*m_BufferManager, 
			*m_SamplerManager);
	}

	DescriptorSet Core::getDescriptorSet(const DescriptorSetHandle handle) const {
		return m_DescriptorManager->getDescriptorSet(handle);
	}

	void Core::prepareSwapchainImageForPresent(const CommandStreamHandle& cmdStream) {
		auto swapchainHandle = ImageHandle::createSwapchainImageHandle();
		recordCommandsToStream(cmdStream, [swapchainHandle, this](const vk::CommandBuffer cmdBuffer) {
			m_ImageManager->recordImageLayoutTransition(swapchainHandle, vk::ImageLayout::ePresentSrcKHR, cmdBuffer);
		}, nullptr);
	}

	void Core::prepareImageForSampling(const CommandStreamHandle& cmdStream, const ImageHandle& image) {
		recordCommandsToStream(cmdStream, [image, this](const vk::CommandBuffer cmdBuffer) {
			m_ImageManager->recordImageLayoutTransition(image, vk::ImageLayout::eShaderReadOnlyOptimal, cmdBuffer);
		}, nullptr);
	}

	void Core::prepareImageForStorage(const CommandStreamHandle& cmdStream, const ImageHandle& image) {
		recordCommandsToStream(cmdStream, [image, this](const vk::CommandBuffer cmdBuffer) {
			m_ImageManager->recordImageLayoutTransition(image, vk::ImageLayout::eGeneral, cmdBuffer);
		}, nullptr);
	}

	void Core::recordImageMemoryBarrier(const CommandStreamHandle& cmdStream, const ImageHandle& image) {
		recordCommandsToStream(cmdStream, [image, this](const vk::CommandBuffer cmdBuffer) {
			m_ImageManager->recordImageMemoryBarrier(image, cmdBuffer);
		}, nullptr);
	}

	void Core::recordBufferMemoryBarrier(const CommandStreamHandle& cmdStream, const BufferHandle& buffer) {
		recordCommandsToStream(cmdStream, [buffer, this](const vk::CommandBuffer cmdBuffer) {
			m_BufferManager->recordBufferMemoryBarrier(buffer, cmdBuffer);
		}, nullptr);
	}
	
	void Core::resolveMSAAImage(const CommandStreamHandle& cmdStream, const ImageHandle& src, const ImageHandle& dst) {
		recordCommandsToStream(cmdStream, [src, dst, this](const vk::CommandBuffer cmdBuffer) {
			m_ImageManager->recordMSAAResolve(cmdBuffer, src, dst);
		}, nullptr);
	}

	vk::ImageView Core::getSwapchainImageView() const {
    	return m_ImageManager->getVulkanImageView(vkcv::ImageHandle::createSwapchainImageHandle());
    }
    
    void Core::recordMemoryBarrier(const CommandStreamHandle& cmdStream) {
		recordCommandsToStream(cmdStream, [](const vk::CommandBuffer cmdBuffer) {
			vk::MemoryBarrier barrier (
					vk::AccessFlagBits::eMemoryWrite | vk::AccessFlagBits::eMemoryRead,
					vk::AccessFlagBits::eMemoryWrite | vk::AccessFlagBits::eMemoryRead
			);
			
			cmdBuffer.pipelineBarrier(
					vk::PipelineStageFlagBits::eAllCommands,
					vk::PipelineStageFlagBits::eAllCommands,
					vk::DependencyFlags(),
					1, &barrier,
					0, nullptr,
					0, nullptr
			);
		}, nullptr);
	}
	
	void Core::recordBlitImage(const CommandStreamHandle& cmdStream, const ImageHandle& src, const ImageHandle& dst,
							   SamplerFilterType filterType) {
		recordCommandsToStream(cmdStream, [&](const vk::CommandBuffer cmdBuffer) {
			m_ImageManager->recordImageLayoutTransition(
					src, vk::ImageLayout::eTransferSrcOptimal, cmdBuffer
			);
			
			m_ImageManager->recordImageLayoutTransition(
					dst, vk::ImageLayout::eTransferDstOptimal, cmdBuffer
			);
			
			const std::array<vk::Offset3D, 2> srcOffsets = {
					vk::Offset3D(0, 0, 0),
					vk::Offset3D(
							m_ImageManager->getImageWidth(src),
							m_ImageManager->getImageHeight(src),
							1
					)
			};
			
			const std::array<vk::Offset3D, 2> dstOffsets = {
					vk::Offset3D(0, 0, 0),
					vk::Offset3D(
							m_ImageManager->getImageWidth(dst),
							m_ImageManager->getImageHeight(dst),
							1
					)
			};
			
			const bool srcDepth = isDepthFormat(m_ImageManager->getImageFormat(src));
			const bool dstDepth = isDepthFormat(m_ImageManager->getImageFormat(dst));
			
			const vk::ImageBlit blit = vk::ImageBlit(
					vk::ImageSubresourceLayers(
							srcDepth?
							vk::ImageAspectFlagBits::eDepth :
							vk::ImageAspectFlagBits::eColor,
							0, 0, 1
					),
					srcOffsets,
					vk::ImageSubresourceLayers(
							dstDepth?
							vk::ImageAspectFlagBits::eDepth :
							vk::ImageAspectFlagBits::eColor,
							0, 0, 1
					),
					dstOffsets
			);
			
			cmdBuffer.blitImage(
					m_ImageManager->getVulkanImage(src),
					vk::ImageLayout::eTransferSrcOptimal,
					m_ImageManager->getVulkanImage(dst),
					vk::ImageLayout::eTransferDstOptimal,
					1,
					&blit,
					filterType == SamplerFilterType::LINEAR?
					vk::Filter::eLinear :
					vk::Filter::eNearest
			);
		}, nullptr);
	}
	
	static void setDebugObjectLabel(const vk::Device& device, const vk::ObjectType& type,
									uint64_t handle, const std::string& label) {
#ifndef NDEBUG
		static PFN_vkSetDebugUtilsObjectNameEXT setDebugLabel = reinterpret_cast<PFN_vkSetDebugUtilsObjectNameEXT>(
				device.getProcAddr("vkSetDebugUtilsObjectNameEXT")
		);
		
		if (!setDebugLabel) {
			return;
		}
		
		const vk::DebugUtilsObjectNameInfoEXT debug (
				type,
				handle,
				label.c_str()
		);
		
		setDebugLabel(device, &(static_cast<const VkDebugUtilsObjectNameInfoEXT&>(debug)));
#endif
	}
	
	void Core::setDebugLabel(const BufferHandle &handle, const std::string &label) {
		if (!handle) {
			vkcv_log(LogLevel::WARNING, "Can't set debug label to invalid handle");
			return;
		}
		
		setDebugObjectLabel(
				m_Context.getDevice(),
				vk::ObjectType::eBuffer,
				reinterpret_cast<uint64_t>(static_cast<VkBuffer>(
						m_BufferManager->getBuffer(handle)
				)),
				label
		);
	}
	
	void Core::setDebugLabel(const PassHandle &handle, const std::string &label) {
		if (!handle) {
			vkcv_log(LogLevel::WARNING, "Can't set debug label to invalid handle");
			return;
		}
		
		setDebugObjectLabel(
				m_Context.getDevice(),
				vk::ObjectType::eRenderPass,
				reinterpret_cast<uint64_t>(static_cast<VkRenderPass>(
						m_PassManager->getVkPass(handle)
				)),
				label
		);
	}
	
	void Core::setDebugLabel(const PipelineHandle &handle, const std::string &label) {
		if (!handle) {
			vkcv_log(LogLevel::WARNING, "Can't set debug label to invalid handle");
			return;
		}
		
		setDebugObjectLabel(
				m_Context.getDevice(),
				vk::ObjectType::ePipeline,
				reinterpret_cast<uint64_t>(static_cast<VkPipeline>(
						m_PipelineManager->getVkPipeline(handle)
				)),
				label
		);
	}
	
	void Core::setDebugLabel(const DescriptorSetHandle &handle, const std::string &label) {
		if (!handle) {
			vkcv_log(LogLevel::WARNING, "Can't set debug label to invalid handle");
			return;
		}
		
		setDebugObjectLabel(
				m_Context.getDevice(),
				vk::ObjectType::eDescriptorSet,
				reinterpret_cast<uint64_t>(static_cast<VkDescriptorSet>(
						m_DescriptorManager->getDescriptorSet(handle).vulkanHandle
				)),
				label
		);
	}
	
	void Core::setDebugLabel(const SamplerHandle &handle, const std::string &label) {
		if (!handle) {
			vkcv_log(LogLevel::WARNING, "Can't set debug label to invalid handle");
			return;
		}
		
		setDebugObjectLabel(
				m_Context.getDevice(),
				vk::ObjectType::eSampler,
				reinterpret_cast<uint64_t>(static_cast<VkSampler>(
						m_SamplerManager->getVulkanSampler(handle)
				)),
				label
		);
	}
	
	void Core::setDebugLabel(const ImageHandle &handle, const std::string &label) {
		if (!handle) {
			vkcv_log(LogLevel::WARNING, "Can't set debug label to invalid handle");
			return;
		} else
		if (handle.isSwapchainImage()) {
			vkcv_log(LogLevel::WARNING, "Can't set debug label to swapchain image");
			return;
		}
		
		setDebugObjectLabel(
				m_Context.getDevice(),
				vk::ObjectType::eImage,
				reinterpret_cast<uint64_t>(static_cast<VkImage>(
						m_ImageManager->getVulkanImage(handle)
				)),
				label
		);
	}
	
	void Core::setDebugLabel(const CommandStreamHandle &handle, const std::string &label) {
		if (!handle) {
			vkcv_log(LogLevel::WARNING, "Can't set debug label to invalid handle");
			return;
		}
		
		setDebugObjectLabel(
				m_Context.getDevice(),
				vk::ObjectType::eCommandBuffer,
				reinterpret_cast<uint64_t>(static_cast<VkCommandBuffer>(
						m_CommandStreamManager->getStreamCommandBuffer(handle)
				)),
				label
		);
	}
	
}