/* Copyright (c) 2021-2025, NVIDIA CORPORATION. All rights reserved.
 *
 * SPDX-License-Identifier: Apache-2.0
 *
 * Licensed under the Apache License, Version 2.0 the "License";
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#pragma once

#include "common/vk_common.h"

#include "core/util/logging.hpp"
#include "vulkan/vulkan.hpp"
#include "vulkan/vulkan_format_traits.hpp"

namespace vkb
{
namespace common
{
/**
 * @brief facade helper functions and structs around the functions and structs in common/vk_common, providing a vulkan.hpp-based interface
 */

struct HPPBufferMemoryBarrier
{
	vk::PipelineStageFlags src_stage_mask  = vk::PipelineStageFlagBits::eBottomOfPipe;
	vk::PipelineStageFlags dst_stage_mask  = vk::PipelineStageFlagBits::eTopOfPipe;
	vk::AccessFlags        src_access_mask = {};
	vk::AccessFlags        dst_access_mask = {};
};

struct HPPImageMemoryBarrier
{
	vk::PipelineStageFlags src_stage_mask = vk::PipelineStageFlagBits::eBottomOfPipe;
	vk::PipelineStageFlags dst_stage_mask = vk::PipelineStageFlagBits::eTopOfPipe;
	vk::AccessFlags        src_access_mask;
	vk::AccessFlags        dst_access_mask;
	vk::ImageLayout        old_layout       = vk::ImageLayout::eUndefined;
	vk::ImageLayout        new_layout       = vk::ImageLayout::eUndefined;
	uint32_t               src_queue_family = VK_QUEUE_FAMILY_IGNORED;
	uint32_t               dst_queue_family = VK_QUEUE_FAMILY_IGNORED;
};

struct HPPLoadStoreInfo
{
	vk::AttachmentLoadOp  load_op  = vk::AttachmentLoadOp::eClear;
	vk::AttachmentStoreOp store_op = vk::AttachmentStoreOp::eStore;
};

inline int32_t get_bits_per_pixel(vk::Format format)
{
	return vkb::get_bits_per_pixel(static_cast<VkFormat>(format));
}

inline vk::Format get_suitable_depth_format(vk::PhysicalDevice             physical_device,
                                            bool                           depth_only                 = false,
                                            const std::vector<vk::Format> &depth_format_priority_list = {
                                                vk::Format::eD32Sfloat, vk::Format::eD24UnormS8Uint, vk::Format::eD16Unorm})
{
	return static_cast<vk::Format>(
	    vkb::get_suitable_depth_format(physical_device, depth_only, reinterpret_cast<std::vector<VkFormat> const &>(depth_format_priority_list)));
}

inline bool is_buffer_descriptor_type(vk::DescriptorType descriptor_type)
{
	return vkb::is_buffer_descriptor_type(static_cast<VkDescriptorType>(descriptor_type));
}

inline bool is_depth_only_format(vk::Format format)
{
	assert(vkb::is_depth_only_format(static_cast<VkFormat>(format)) ==
	       ((vk::componentCount(format) == 1) && (std::string(vk::componentName(format, 0)) == "D")));
	return vkb::is_depth_only_format(static_cast<VkFormat>(format));
}

inline bool is_depth_stencil_format(vk::Format format)
{
	assert(vkb::is_depth_stencil_format(static_cast<VkFormat>(format)) ==
	       ((vk::componentCount(format) == 2) && (std::string(vk::componentName(format, 0)) == "D") &&
	        (std::string(vk::componentName(format, 1)) == "S")));
	return vkb::is_depth_stencil_format(static_cast<VkFormat>(format));
}

inline bool is_depth_format(vk::Format format)
{
	assert(vkb::is_depth_format(static_cast<VkFormat>(format)) == (std::string(vk::componentName(format, 0)) == "D"));
	return vkb::is_depth_format(static_cast<VkFormat>(format));
}

inline bool is_dynamic_buffer_descriptor_type(vk::DescriptorType descriptor_type)
{
	return vkb::is_dynamic_buffer_descriptor_type(static_cast<VkDescriptorType>(descriptor_type));
}

inline vk::ShaderModule load_shader(const std::string &filename, vk::Device device, vk::ShaderStageFlagBits stage)
{
	return static_cast<vk::ShaderModule>(vkb::load_shader(filename, device, static_cast<VkShaderStageFlagBits>(stage)));
}

inline void image_layout_transition(vk::CommandBuffer command_buffer,
                                    vk::Image         image,
                                    vk::ImageLayout   old_layout,
                                    vk::ImageLayout   new_layout)
{
	vkb::image_layout_transition(static_cast<VkCommandBuffer>(command_buffer),
	                             static_cast<VkImage>(image),
	                             static_cast<VkImageLayout>(old_layout),
	                             static_cast<VkImageLayout>(new_layout));
}

inline void image_layout_transition(vk::CommandBuffer         command_buffer,
                                    vk::Image                 image,
                                    vk::ImageLayout           old_layout,
                                    vk::ImageLayout           new_layout,
                                    vk::ImageSubresourceRange subresource_range)
{
	vkb::image_layout_transition(static_cast<VkCommandBuffer>(command_buffer),
	                             static_cast<VkImage>(image),
	                             static_cast<VkImageLayout>(old_layout),
	                             static_cast<VkImageLayout>(new_layout),
	                             static_cast<VkImageSubresourceRange>(subresource_range));
}

inline void image_layout_transition(vk::CommandBuffer                command_buffer,
                                    vk::Image                        image,
                                    vk::PipelineStageFlags           src_stage_mask,
                                    vk::PipelineStageFlags           dst_stage_mask,
                                    vk::AccessFlags                  src_access_mask,
                                    vk::AccessFlags                  dst_access_mask,
                                    vk::ImageLayout                  old_layout,
                                    vk::ImageLayout                  new_layout,
                                    vk::ImageSubresourceRange const &subresource_range)
{
	vkb::image_layout_transition(static_cast<VkCommandBuffer>(command_buffer),
	                             static_cast<VkImage>(image),
	                             static_cast<VkPipelineStageFlags>(src_stage_mask),
	                             static_cast<VkPipelineStageFlags>(dst_stage_mask),
	                             static_cast<VkAccessFlags>(src_access_mask),
	                             static_cast<VkAccessFlags>(dst_access_mask),
	                             static_cast<VkImageLayout>(old_layout),
	                             static_cast<VkImageLayout>(new_layout),
	                             static_cast<VkImageSubresourceRange const &>(subresource_range));
}

inline void make_filters_valid(vk::PhysicalDevice physical_device, vk::Format format, vk::Filter *filter, vk::SamplerMipmapMode *mipmapMode = nullptr)
{
	// Not all formats support linear filtering, so we need to adjust them if they don't
	if (*filter == vk::Filter::eNearest && (mipmapMode == nullptr || *mipmapMode == vk::SamplerMipmapMode::eNearest))
	{
		return;        // These must already be valid
	}

	vk::FormatProperties properties = physical_device.getFormatProperties(format);

	if (!(properties.optimalTilingFeatures & vk::FormatFeatureFlagBits::eSampledImageFilterLinear))
	{
		*filter = vk::Filter::eNearest;
		if (mipmapMode)
		{
			*mipmapMode = vk::SamplerMipmapMode::eNearest;
		}
	}
}

inline vk::SurfaceFormatKHR select_surface_format(vk::PhysicalDevice             gpu,
                                                  vk::SurfaceKHR                 surface,
                                                  std::vector<vk::Format> const &preferred_formats = {
                                                      vk::Format::eR8G8B8A8Srgb, vk::Format::eB8G8R8A8Srgb, vk::Format::eA8B8G8R8SrgbPack32})
{
	std::vector<vk::SurfaceFormatKHR> supported_surface_formats = gpu.getSurfaceFormatsKHR(surface);
	assert(!supported_surface_formats.empty());

	auto it = std::ranges::find_if(supported_surface_formats,
	                               [&preferred_formats](vk::SurfaceFormatKHR surface_format) {
		                               return std::ranges::any_of(preferred_formats,
		                                                          [&surface_format](vk::Format format) { return format == surface_format.format; });
	                               });

	// We use the first supported format as a fallback in case none of the preferred formats is available
	return it != supported_surface_formats.end() ? *it : supported_surface_formats[0];
}

inline vk::Format choose_blendable_format(vk::PhysicalDevice gpu, const std::vector<vk::Format> &format_priority_list)
{
	for (const auto &format : format_priority_list)
	{
		vk::FormatProperties fmt_props = gpu.getFormatProperties(format);

		if (fmt_props.optimalTilingFeatures & vk::FormatFeatureFlagBits::eColorAttachmentBlend)
			return format;
	}

	throw std::runtime_error("No suitable blendable format could be determined");
}

// helper functions not backed by vk_common.h
inline vk::CommandBuffer
    allocate_command_buffer(vk::Device device, vk::CommandPool command_pool, vk::CommandBufferLevel level = vk::CommandBufferLevel::ePrimary)
{
	vk::CommandBufferAllocateInfo command_buffer_allocate_info{.commandPool = command_pool, .level = level, .commandBufferCount = 1};
	return device.allocateCommandBuffers(command_buffer_allocate_info).front();
}

inline vk::DescriptorSet allocate_descriptor_set(vk::Device device, vk::DescriptorPool descriptor_pool, vk::DescriptorSetLayout descriptor_set_layout)
{
	vk::DescriptorSetAllocateInfo descriptor_set_allocate_info{.descriptorPool     = descriptor_pool,
	                                                           .descriptorSetCount = 1,
	                                                           .pSetLayouts        = &descriptor_set_layout};
	return device.allocateDescriptorSets(descriptor_set_allocate_info).front();
}

inline vk::Framebuffer
    create_framebuffer(vk::Device device, vk::RenderPass render_pass, std::vector<vk::ImageView> const &attachments, vk::Extent2D const &extent)
{
	vk::FramebufferCreateInfo framebuffer_create_info{.renderPass      = render_pass,
	                                                  .attachmentCount = static_cast<uint32_t>(attachments.size()),
	                                                  .pAttachments    = attachments.data(),
	                                                  .width           = extent.width,
	                                                  .height          = extent.height,
	                                                  .layers          = 1};
	return device.createFramebuffer(framebuffer_create_info);
}

inline vk::Pipeline create_graphics_pipeline(vk::Device                                                device,
                                             vk::PipelineCache                                         pipeline_cache,
                                             std::vector<vk::PipelineShaderStageCreateInfo> const     &shader_stages,
                                             vk::PipelineVertexInputStateCreateInfo const             &vertex_input_state,
                                             vk::PrimitiveTopology                                     primitive_topology,
                                             uint32_t                                                  patch_control_points,
                                             vk::PolygonMode                                           polygon_mode,
                                             vk::CullModeFlags                                         cull_mode,
                                             vk::FrontFace                                             front_face,
                                             std::vector<vk::PipelineColorBlendAttachmentState> const &blend_attachment_states,
                                             vk::PipelineDepthStencilStateCreateInfo const            &depth_stencil_state,
                                             vk::PipelineLayout                                        pipeline_layout,
                                             vk::RenderPass                                            render_pass)
{
	vk::PipelineInputAssemblyStateCreateInfo input_assembly_state{.topology = primitive_topology};

	vk::PipelineTessellationStateCreateInfo tessellation_state{.patchControlPoints = patch_control_points};

	vk::PipelineViewportStateCreateInfo viewport_state{.viewportCount = 1, .scissorCount = 1};

	vk::PipelineRasterizationStateCreateInfo rasterization_state{
	    .polygonMode = polygon_mode, .cullMode = cull_mode, .frontFace = front_face, .lineWidth = 1.0f};

	vk::PipelineMultisampleStateCreateInfo multisample_state{.rasterizationSamples = vk::SampleCountFlagBits::e1};

	vk::PipelineColorBlendStateCreateInfo color_blend_state{.attachmentCount = static_cast<uint32_t>(blend_attachment_states.size()),
	                                                        .pAttachments    = blend_attachment_states.data()};

	std::array<vk::DynamicState, 2>    dynamic_state_enables = {vk::DynamicState::eViewport, vk::DynamicState::eScissor};
	vk::PipelineDynamicStateCreateInfo dynamic_state{.dynamicStateCount = static_cast<uint32_t>(dynamic_state_enables.size()),
	                                                 .pDynamicStates    = dynamic_state_enables.data()};

	// Final fullscreen composition pass pipeline
	vk::GraphicsPipelineCreateInfo pipeline_create_info{.stageCount          = static_cast<uint32_t>(shader_stages.size()),
	                                                    .pStages             = shader_stages.data(),
	                                                    .pVertexInputState   = &vertex_input_state,
	                                                    .pInputAssemblyState = &input_assembly_state,
	                                                    .pTessellationState  = &tessellation_state,
	                                                    .pViewportState      = &viewport_state,
	                                                    .pRasterizationState = &rasterization_state,
	                                                    .pMultisampleState   = &multisample_state,
	                                                    .pDepthStencilState  = &depth_stencil_state,
	                                                    .pColorBlendState    = &color_blend_state,
	                                                    .pDynamicState       = &dynamic_state,
	                                                    .layout              = pipeline_layout,
	                                                    .renderPass          = render_pass,
	                                                    .basePipelineIndex   = -1};

	vk::Result   result;
	vk::Pipeline pipeline;
	std::tie(result, pipeline) = device.createGraphicsPipeline(pipeline_cache, pipeline_create_info);
	assert(result == vk::Result::eSuccess);
	return pipeline;
}

inline vk::ImageView create_image_view(vk::Device           device,
                                       vk::Image            image,
                                       vk::ImageViewType    view_type,
                                       vk::Format           format,
                                       vk::ImageAspectFlags aspect_mask      = vk::ImageAspectFlagBits::eColor,
                                       uint32_t             base_mip_level   = 0,
                                       uint32_t             level_count      = 1,
                                       uint32_t             base_array_layer = 0,
                                       uint32_t             layer_count      = 1)
{
	vk::ImageViewCreateInfo image_view_create_info{.image            = image,
	                                               .viewType         = view_type,
	                                               .format           = format,
	                                               .subresourceRange = {.aspectMask     = aspect_mask,
	                                                                    .baseMipLevel   = base_mip_level,
	                                                                    .levelCount     = level_count,
	                                                                    .baseArrayLayer = base_array_layer,
	                                                                    .layerCount     = layer_count}};
	return device.createImageView(image_view_create_info);
}

inline vk::QueryPool create_query_pool(vk::Device device, vk::QueryType query_type, uint32_t query_count, vk::QueryPipelineStatisticFlags pipeline_statistics = {})
{
	vk::QueryPoolCreateInfo query_pool_create_info{.queryType = query_type, .queryCount = query_count, .pipelineStatistics = pipeline_statistics};
	return device.createQueryPool(query_pool_create_info);
}

inline vk::Sampler create_sampler(vk::Device             device,
                                  vk::Filter             mag_filter,
                                  vk::Filter             min_filter,
                                  vk::SamplerMipmapMode  mipmap_mode,
                                  vk::SamplerAddressMode sampler_address_mode,
                                  float                  max_anisotropy,
                                  float                  max_LOD)
{
	vk::SamplerCreateInfo sampler_create_info{.magFilter        = mag_filter,
	                                          .minFilter        = min_filter,
	                                          .mipmapMode       = mipmap_mode,
	                                          .addressModeU     = sampler_address_mode,
	                                          .addressModeV     = sampler_address_mode,
	                                          .addressModeW     = sampler_address_mode,
	                                          .anisotropyEnable = (1.0f < max_anisotropy),
	                                          .maxAnisotropy    = max_anisotropy,
	                                          .compareOp        = vk::CompareOp::eNever,
	                                          .minLod           = 0.0f,
	                                          .maxLod           = max_LOD,
	                                          .borderColor      = vk::BorderColor::eFloatOpaqueWhite};
	return device.createSampler(sampler_create_info);
}

inline vk::Sampler create_sampler(vk::PhysicalDevice     gpu,
                                  vk::Device             device,
                                  vk::Format             format,
                                  vk::Filter             filter,
                                  vk::SamplerAddressMode sampler_address_mode,
                                  float                  max_anisotropy,
                                  float                  max_LOD)
{
	const vk::FormatProperties fmt_props = gpu.getFormatProperties(format);

	bool has_linear_filter = !!(fmt_props.optimalTilingFeatures & vk::FormatFeatureFlagBits::eSampledImageFilterLinear);

	return create_sampler(device,
	                      has_linear_filter ? filter : vk::Filter::eNearest,
	                      has_linear_filter ? filter : vk::Filter::eNearest,
	                      has_linear_filter ? vk::SamplerMipmapMode::eLinear : vk::SamplerMipmapMode::eNearest,
	                      sampler_address_mode,
	                      max_anisotropy,
	                      max_LOD);
}

inline vk::ImageAspectFlags get_image_aspect_flags(vk::ImageUsageFlagBits usage, vk::Format format)
{
	vk::ImageAspectFlags image_aspect_flags;

	switch (usage)
	{
		case vk::ImageUsageFlagBits::eColorAttachment:
			assert(!vkb::common::is_depth_format(format));
			image_aspect_flags = vk::ImageAspectFlagBits::eColor;
			break;
		case vk::ImageUsageFlagBits::eDepthStencilAttachment:
			assert(vkb::common::is_depth_format(format));
			image_aspect_flags = vk::ImageAspectFlagBits::eDepth;
			// Stencil aspect should only be set on depth + stencil formats
			if (vkb::common::is_depth_stencil_format(format))
			{
				image_aspect_flags |= vk::ImageAspectFlagBits::eStencil;
			}
			break;
		default:
			assert(false);
	}

	return image_aspect_flags;
}

inline void submit_and_wait(vk::Device device, vk::Queue queue, std::vector<vk::CommandBuffer> command_buffers, std::vector<vk::Semaphore> semaphores = {})
{
	// Submit command_buffer
	vk::SubmitInfo submit_info{.commandBufferCount   = static_cast<uint32_t>(command_buffers.size()),
	                           .pCommandBuffers      = command_buffers.data(),
	                           .signalSemaphoreCount = static_cast<uint32_t>(semaphores.size()),
	                           .pSignalSemaphores    = semaphores.data()};

	// Create fence to ensure that command_buffer has finished executing
	vk::Fence fence = device.createFence({});

	// Submit to the queue
	queue.submit(submit_info, fence);

	// Wait for the fence to signal that command_buffer has finished executing
	vk::Result result = device.waitForFences(fence, true, DEFAULT_FENCE_TIMEOUT);
	if (result != vk::Result::eSuccess)
	{
		LOGE("Vulkan error on waitForFences: {}", vk::to_string(result));
		abort();
	}

	// Destroy the fence
	device.destroyFence(fence);
}

inline uint32_t get_queue_family_index(std::vector<vk::QueueFamilyProperties> const &queue_family_properties, vk::QueueFlagBits queue_flag)
{
	// Dedicated queue for compute
	// Try to find a queue family index that supports compute but not graphics
	if (queue_flag & vk::QueueFlagBits::eCompute)
	{
		auto propertyIt = std::ranges::find_if(queue_family_properties,
		                                       [queue_flag](const vk::QueueFamilyProperties &property) { return (property.queueFlags & queue_flag) && !(property.queueFlags & vk::QueueFlagBits::eGraphics); });
		if (propertyIt != queue_family_properties.end())
		{
			return static_cast<uint32_t>(std::distance(queue_family_properties.begin(), propertyIt));
		}
	}

	// Dedicated queue for transfer
	// Try to find a queue family index that supports transfer but not graphics and compute
	if (queue_flag & vk::QueueFlagBits::eTransfer)
	{
		auto propertyIt = std::ranges::find_if(queue_family_properties,
		                                       [queue_flag](const vk::QueueFamilyProperties &property) {
			                                       return (property.queueFlags & queue_flag) && !(property.queueFlags & vk::QueueFlagBits::eGraphics) &&
			                                              !(property.queueFlags & vk::QueueFlagBits::eCompute);
		                                       });
		if (propertyIt != queue_family_properties.end())
		{
			return static_cast<uint32_t>(std::distance(queue_family_properties.begin(), propertyIt));
		}
	}

	// For other queue types or if no separate compute queue is present, return the first one to support the requested flags
	auto propertyIt = std::ranges::find_if(
	    queue_family_properties, [queue_flag](const vk::QueueFamilyProperties &property) { return (property.queueFlags & queue_flag) == queue_flag; });
	if (propertyIt != queue_family_properties.end())
	{
		return static_cast<uint32_t>(std::distance(queue_family_properties.begin(), propertyIt));
	}

	throw std::runtime_error("Could not find a matching queue family index");
}

}        // namespace common
}        // namespace vkb
