/* Copyright (c) 2018-2025, Arm Limited and Contributors
 * Copyright (c) 2022-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 <ranges>
#include <vulkan/vulkan.hpp>

#if defined(VKB_DEBUG) || defined(VKB_VALIDATION_LAYERS)
#	define USE_VALIDATION_LAYERS
#endif

#if defined(USE_VALIDATION_LAYERS) && \
    (defined(VKB_VALIDATION_LAYERS_GPU_ASSISTED) || defined(VKB_VALIDATION_LAYERS_BEST_PRACTICES) || defined(VKB_VALIDATION_LAYERS_SYNCHRONIZATION))
#	define USE_VALIDATION_LAYER_FEATURES
#endif

namespace vkb
{
class PhysicalDevice;

namespace core
{
class HPPPhysicalDevice;

/**
 * @brief A wrapper class for InstanceType
 *
 * This class is responsible for initializing the dispatcher, enumerating over all available extensions and validation layers
 * enabling them if they exist, setting up debug messaging and querying all the physical devices existing on the machine.
 */
template <vkb::BindingType bindingType>
class Instance
{
  public:
	using InstanceType = typename std::conditional<bindingType == vkb::BindingType::Cpp, vk::Instance, VkInstance>::type;
	using SurfaceType  = typename std::conditional<bindingType == vkb::BindingType::Cpp, vk::SurfaceKHR, VkSurfaceKHR>::type;

	using PhysicalDeviceType = typename std::conditional<bindingType == vkb::BindingType::Cpp, vkb::core::HPPPhysicalDevice, vkb::PhysicalDevice>::type;

  public:
	/**
	 * @brief Can be set from the GPU selection plugin to explicitly select a GPU instead
	 */
	inline static uint32_t selected_gpu_index = ~0;

	/**
	 * @brief Initializes the connection to Vulkan
	 * @param application_name The name of the application
	 * @param requested_extensions The extensions requested to be enabled
	 * @param requested_layers The validation layers to be enabled
	 * @param requested_layer_settings The layer settings to be enabled
	 * @param api_version The Vulkan API version that the instance will be using
	 * @throws runtime_error if the required extensions and validation layers are not found
	 */
	Instance(std::string const                            &application_name,
	         std::unordered_map<char const *, bool> const &requested_extensions     = {},
	         std::unordered_map<char const *, bool> const &requested_layers         = {},
	         const std::vector<vk::LayerSettingEXT>       &requested_layer_settings = {},
	         uint32_t                                      api_version              = VK_API_VERSION_1_1);
	Instance(std::string const                            &application_name,
	         std::unordered_map<char const *, bool> const &requested_extensions,
	         std::unordered_map<char const *, bool> const &requested_layers,
	         std::vector<VkLayerSettingEXT> const         &requested_layer_settings,
	         uint32_t                                      api_version = VK_API_VERSION_1_1);

	/**
	 * @brief Queries the GPUs of a InstanceType that is already created
	 * @param instance A valid InstanceType
	 * @param externally_enabled_extensions List of extensions that have been enabled, used for following checks e.g. during device creation
	 * @param needsToInitializeDispatcher If the sample uses the C-bindings and some "non-standard" initialization, the dispatcher needs to be initialized
	 */
	Instance(vk::Instance instance, const std::vector<const char *> &externally_enabled_extensions = {}, bool needsToInitializeDispatcher = false);
	Instance(VkInstance instance, const std::vector<const char *> &externally_enabled_extensions = {});

	Instance(Instance const &) = delete;
	Instance(Instance &&)      = delete;

	~Instance();

	Instance &operator=(Instance const &) = delete;
	Instance &operator=(Instance &&)      = delete;

	const std::vector<const char *> &get_extensions();

	/**
	 * @brief Tries to find the first available discrete GPU
	 * @returns A valid physical device
	 */
	PhysicalDeviceType &get_first_gpu();

	InstanceType get_handle() const;

	/**
	 * @brief Tries to find the first available discrete GPU that can render to the given surface
	 * @param surface to test against
	 * @param headless_surface Is surface created with VK_EXT_headless_surface
	 * @returns A valid physical device
	 */
	PhysicalDeviceType &get_suitable_gpu(SurfaceType surface, bool headless_surface);

	/**
	 * @brief Checks if the given extension is enabled in the InstanceType
	 * @param extension An extension to check
	 */
	bool is_enabled(char const *extension) const;

  private:
	/**
	 * @brief Queries the instance for the physical devices on the machine
	 */
	void query_gpus();

  private:
	std::vector<const char *>                                  enabled_extensions;        // The enabled extensions
	std::vector<std::unique_ptr<vkb::core::HPPPhysicalDevice>> gpus;                      // The physical devices found on the machine
	vk::Instance                                               handle;                    // The Vulkan instance

#if defined(VKB_DEBUG) || defined(VKB_VALIDATION_LAYERS)
	vk::DebugReportCallbackEXT debug_report_callback;        // The debug report callback
	vk::DebugUtilsMessengerEXT debug_utils_messenger;        // Debug utils messenger callback for VK_EXT_Debug_Utils
#endif
};

using InstanceC   = Instance<vkb::BindingType::C>;
using InstanceCpp = Instance<vkb::BindingType::Cpp>;
}        // namespace core
}        // namespace vkb

#include "core/hpp_physical_device.h"

namespace vkb
{
namespace core
{
namespace
{
#ifdef USE_VALIDATION_LAYERS
inline VKAPI_ATTR vk::Bool32 VKAPI_CALL debug_utils_messenger_callback(vk::DebugUtilsMessageSeverityFlagBitsEXT      message_severity,
                                                                       vk::DebugUtilsMessageTypeFlagsEXT             message_type,
                                                                       vk::DebugUtilsMessengerCallbackDataEXT const *callback_data,
                                                                       void                                         *user_data)
{
	// Log debug message
	if (message_severity & vk::DebugUtilsMessageSeverityFlagBitsEXT::eWarning)
	{
		LOGW("{} - {}: {}", callback_data->messageIdNumber, callback_data->pMessageIdName, callback_data->pMessage);
	}
	else if (message_severity & vk::DebugUtilsMessageSeverityFlagBitsEXT::eError)
	{
		LOGE("{} - {}: {}", callback_data->messageIdNumber, callback_data->pMessageIdName, callback_data->pMessage);
	}
	return false;
}

inline VKAPI_ATTR vk::Bool32 VKAPI_CALL debug_callback(vk::DebugReportFlagsEXT flags,
                                                       vk::DebugReportObjectTypeEXT /*type*/,
                                                       uint64_t /*object*/,
                                                       size_t /*location*/,
                                                       int32_t /*message_code*/,
                                                       char const *layer_prefix,
                                                       char const *message,
                                                       void * /*user_data*/)
{
	if (flags & vk::DebugReportFlagBitsEXT::eError)
	{
		LOGE("{}: {}", layer_prefix, message);
	}
	else if (flags & vk::DebugReportFlagBitsEXT::eWarning)
	{
		LOGW("{}: {}", layer_prefix, message);
	}
	else if (flags & vk::DebugReportFlagBitsEXT::ePerformanceWarning)
	{
		LOGW("{}: {}", layer_prefix, message);
	}
	else
	{
		LOGI("{}: {}", layer_prefix, message);
	}
	return false;
}
#endif

inline bool enable_extension(char const                                 *requested_extension,
                             std::vector<vk::ExtensionProperties> const &available_extensions,
                             std::vector<char const *>                  &enabled_extensions)
{
	bool is_available = std::ranges::any_of(available_extensions,
	                                        [&requested_extension](auto const &available_extension) { return strcmp(requested_extension, available_extension.extensionName) == 0; });
	if (is_available)
	{
		bool is_already_enabled = std::ranges::any_of(
		    enabled_extensions, [&requested_extension](auto const &enabled_extension) { return strcmp(requested_extension, enabled_extension) == 0; });
		if (!is_already_enabled)
		{
			LOGI("Extension {} available, enabling it", requested_extension);
			enabled_extensions.emplace_back(requested_extension);
		}
	}
	else
	{
		LOGI("Extension {} not available", requested_extension);
	}

	return is_available;
}

inline bool enable_layer(char const *requested_layer, std::vector<vk::LayerProperties> const &available_layers, std::vector<char const *> &enabled_layers)
{
	bool is_available = std::ranges::any_of(
	    available_layers, [&requested_layer](auto const &available_layer) { return strcmp(requested_layer, available_layer.layerName) == 0; });
	if (is_available)
	{
		bool is_already_enabled =
		    std::ranges::any_of(enabled_layers, [&requested_layer](auto const &enabled_layer) { return strcmp(requested_layer, enabled_layer) == 0; });
		if (!is_already_enabled)
		{
			LOGI("Layer {} available, enabling it", requested_layer);
			enabled_layers.emplace_back(requested_layer);
		}
	}
	else
	{
		LOGI("Layer {} not available", requested_layer);
	}

	return is_available;
}

inline bool enable_layer_setting(vk::LayerSettingEXT const        &requested_layer_setting,
                                 std::vector<char const *> const  &enabled_layers,
                                 std::vector<vk::LayerSettingEXT> &enabled_layer_settings)
{
	// We are checking if the layer is available.
	// Vulkan does not provide a reflection API for layer settings. Layer settings are described in each layer JSON manifest.
	bool is_available = std::ranges::any_of(
	    enabled_layers, [&requested_layer_setting](auto const &available_layer) { return strcmp(available_layer, requested_layer_setting.pLayerName) == 0; });
#if defined(PLATFORM__MACOS)
	// On Apple platforms the MoltenVK layer is implicitly enabled and available, and cannot be explicitly added or checked via enabled_layers.
	is_available = is_available || strcmp(requested_layer_setting.pLayerName, "MoltenVK") == 0;
#endif

	if (!is_available)
	{
		LOGW("Layer: {} not found. Disabling layer setting: {}", requested_layer_setting.pLayerName, requested_layer_setting.pSettingName);
		return false;
	}

	bool is_already_enabled = std::ranges::any_of(enabled_layer_settings,
	                                              [&requested_layer_setting](VkLayerSettingEXT const &enabled_layer_setting) {
		                                              return (strcmp(requested_layer_setting.pLayerName, enabled_layer_setting.pLayerName) == 0) &&
		                                                     (strcmp(requested_layer_setting.pSettingName, enabled_layer_setting.pSettingName) == 0);
	                                              });

	if (is_already_enabled)
	{
		LOGW("Ignoring duplicated layer setting {} in layer {}.", requested_layer_setting.pSettingName, requested_layer_setting.pLayerName);
		return false;
	}

	LOGI("Enabling layer setting {} in layer {}.", requested_layer_setting.pSettingName, requested_layer_setting.pLayerName);
	enabled_layer_settings.push_back(requested_layer_setting);
	return true;
}

inline bool enable_layer_setting(VkLayerSettingEXT const          &requested_layer_setting,
                                 std::vector<char const *> const  &enabled_layers,
                                 std::vector<vk::LayerSettingEXT> &enabled_layer_settings)
{
	return enable_layer_setting(reinterpret_cast<vk::LayerSettingEXT const &>(requested_layer_setting), enabled_layers, enabled_layer_settings);
}

inline bool validate_layers(std::vector<char const *> const &required, std::vector<vk::LayerProperties> const &available)
{
	for (auto layer : required)
	{
		bool found = false;
		for (auto &available_layer : available)
		{
			if (strcmp(available_layer.layerName, layer) == 0)
			{
				found = true;
				break;
			}
		}

		if (!found)
		{
			LOGE("Validation Layer {} not found", layer);
			return false;
		}
	}

	return true;
}
}        // namespace

template <vkb::BindingType bindingType>
inline Instance<bindingType>::Instance(std::string const                            &application_name,
                                       std::unordered_map<char const *, bool> const &requested_extensions,
                                       std::unordered_map<char const *, bool> const &requested_layers,
                                       std::vector<vk::LayerSettingEXT> const       &requested_layer_settings,
                                       uint32_t                                      api_version)
{
	std::vector<vk::ExtensionProperties> available_instance_extensions = vk::enumerateInstanceExtensionProperties();

#ifdef USE_VALIDATION_LAYERS
	// Check if VK_EXT_debug_utils is supported, which supersedes VK_EXT_Debug_Report
	const bool has_debug_utils  = enable_extension(VK_EXT_DEBUG_UTILS_EXTENSION_NAME, available_instance_extensions, enabled_extensions);
	bool       has_debug_report = false;

	if (!has_debug_utils)
	{
		has_debug_report = enable_extension(VK_EXT_DEBUG_REPORT_EXTENSION_NAME, available_instance_extensions, enabled_extensions);
		if (!has_debug_report)
		{
			LOGW("Neither of {} or {} are available; disabling debug reporting", VK_EXT_DEBUG_UTILS_EXTENSION_NAME, VK_EXT_DEBUG_REPORT_EXTENSION_NAME);
		}
	}
#endif

#if (defined(VKB_ENABLE_PORTABILITY))
	enable_extension(VK_KHR_GET_PHYSICAL_DEVICE_PROPERTIES_2_EXTENSION_NAME, available_instance_extensions, enabled_extensions);
	bool portability_enumeration_available = enable_extension(VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME, available_instance_extensions, enabled_extensions);
#endif

#ifdef USE_VALIDATION_LAYERS
	const char *validation_layer_name = "VK_LAYER_KHRONOS_validation";
#	ifdef USE_VALIDATION_LAYER_FEATURES
	bool validation_features = false;
	{
		std::vector<vk::ExtensionProperties> available_layer_instance_extensions = vk::enumerateInstanceExtensionProperties(std::string(validation_layer_name));
		validation_features                                                      = enable_extension(VK_EXT_LAYER_SETTINGS_EXTENSION_NAME, available_layer_instance_extensions, enabled_extensions);
	}
#	endif        // USE_VALIDATION_LAYER_FEATURES
#endif            // USE_VALIDATION_LAYERS

	// Specific surface extensions are obtained from  Window::get_required_surface_extensions
	// They are already added to requested_extensions by VulkanSample::prepare

	// Even for a headless surface a swapchain is still required
	enable_extension(VK_KHR_SURFACE_EXTENSION_NAME, available_instance_extensions, enabled_extensions);

	// VK_KHR_get_physical_device_properties2 is a prerequisite of VK_KHR_performance_query
	// which will be used for stats gathering where available.
	enable_extension(VK_KHR_GET_PHYSICAL_DEVICE_PROPERTIES_2_EXTENSION_NAME, available_instance_extensions, enabled_extensions);

	for (auto requested_extension : requested_extensions)
	{
		auto const &extension_name        = requested_extension.first;
		auto        extension_is_optional = requested_extension.second;
		if (!enable_extension(extension_name, available_instance_extensions, enabled_extensions))
		{
			if (extension_is_optional)
			{
				LOGW("Optional instance extension {} not available, some features may be disabled", extension_name);
			}
			else
			{
				LOGE("Required instance extension {} not available, cannot run", extension_name);
				throw std::runtime_error("Required instance extensions are missing.");
			}
		}
	}

	std::vector<vk::LayerProperties> supported_layers = vk::enumerateInstanceLayerProperties();

	std::vector<char const *> enabled_layers;

	auto layer_error = false;
	for (auto const &requested_layer : requested_layers)
	{
		auto const &layer_name        = requested_layer.first;
		auto        layer_is_optional = requested_layer.second;
		if (!enable_layer(layer_name, supported_layers, enabled_layers))
		{
			if (layer_is_optional)
			{
				LOGW("Optional layer {} not available, some features may be disabled", layer_name);
			}
			else
			{
				LOGE("Required layer {} not available, cannot run", layer_name);
				throw std::runtime_error("Required layers are missing.");
			}
		}
	}

#ifdef USE_VALIDATION_LAYERS
	// NOTE: It's important to have the validation layer as the last one here!!!!
	//			 Otherwise, device creation fails !?!
	enable_layer(validation_layer_name, supported_layers, enabled_layers);
#endif

	vk::ApplicationInfo app_info{.pApplicationName = application_name.c_str(), .pEngineName = "Vulkan Samples", .apiVersion = api_version};

	vk::InstanceCreateInfo instance_info{.pApplicationInfo        = &app_info,
	                                     .enabledLayerCount       = static_cast<uint32_t>(enabled_layers.size()),
	                                     .ppEnabledLayerNames     = enabled_layers.data(),
	                                     .enabledExtensionCount   = static_cast<uint32_t>(enabled_extensions.size()),
	                                     .ppEnabledExtensionNames = enabled_extensions.data()};

	std::vector<vk::LayerSettingEXT> enabled_layer_settings;

	for (auto const &layer_setting : requested_layer_settings)
	{
		enable_layer_setting(layer_setting, enabled_layers, enabled_layer_settings);
	}

#ifdef USE_VALIDATION_LAYERS
	vk::DebugUtilsMessengerCreateInfoEXT debug_utils_create_info;
	vk::DebugReportCallbackCreateInfoEXT debug_report_create_info;
	if (has_debug_utils)
	{
		debug_utils_create_info = vk::DebugUtilsMessengerCreateInfoEXT{
		    .messageSeverity = vk::DebugUtilsMessageSeverityFlagBitsEXT::eError | vk::DebugUtilsMessageSeverityFlagBitsEXT::eWarning,
		    .messageType     = vk::DebugUtilsMessageTypeFlagBitsEXT::eValidation | vk::DebugUtilsMessageTypeFlagBitsEXT::ePerformance,
		    .pfnUserCallback = debug_utils_messenger_callback};

		instance_info.pNext = &debug_utils_create_info;
	}
	else if (has_debug_report)
	{
		debug_report_create_info = {.flags =
		                                vk::DebugReportFlagBitsEXT::eError | vk::DebugReportFlagBitsEXT::eWarning | vk::DebugReportFlagBitsEXT::ePerformanceWarning,
		                            .pfnCallback = debug_callback};

		instance_info.pNext = &debug_report_create_info;
	}
#endif

#if (defined(VKB_ENABLE_PORTABILITY))
	if (portability_enumeration_available)
	{
		instance_info.flags |= vk::InstanceCreateFlagBits::eEnumeratePortabilityKHR;
	}
#endif

	// Some of the specialized layers need to be enabled explicitly
	// The validation layer does not need to be enabled in code and it can also be configured using the vulkan configurator.
#ifdef USE_VALIDATION_LAYER_FEATURES

#	if defined(VKB_VALIDATION_LAYERS_GPU_ASSISTED)
	const vk::Bool32 setting_validate_gpuav = true;
	if (validation_features)
	{
		enable_layer_setting(vk::LayerSettingEXT(validation_layer_name, "gpuav_enable", vk::LayerSettingTypeEXT::eBool32, 1, &setting_validate_gpuav), enabled_layers, enabled_layer_settings);
	}
#	endif

#	if defined(VKB_VALIDATION_LAYERS_BEST_PRACTICES)
	const vk::Bool32 setting_validate_best_practices        = true;
	const vk::Bool32 setting_validate_best_practices_arm    = true;
	const vk::Bool32 setting_validate_best_practices_amd    = true;
	const vk::Bool32 setting_validate_best_practices_img    = true;
	const vk::Bool32 setting_validate_best_practices_nvidia = true;
	if (validation_features)
	{
		enable_layer_setting(
		    vk::LayerSettingEXT(validation_layer_name, "validate_best_practices", vk::LayerSettingTypeEXT::eBool32, 1, &setting_validate_best_practices),
		    enabled_layers,
		    enabled_layer_settings);
		enable_layer_setting(
		    vk::LayerSettingEXT(validation_layer_name, "validate_best_practices_arm", vk::LayerSettingTypeEXT::eBool32, 1, &setting_validate_best_practices_arm),
		    enabled_layers,
		    enabled_layer_settings);
		enable_layer_setting(
		    vk::LayerSettingEXT(validation_layer_name, "validate_best_practices_amd", vk::LayerSettingTypeEXT::eBool32, 1, &setting_validate_best_practices_amd),
		    enabled_layers,
		    enabled_layer_settings);
		enable_layer_setting(
		    vk::LayerSettingEXT(validation_layer_name, "validate_best_practices_img", vk::LayerSettingTypeEXT::eBool32, 1, &setting_validate_best_practices_img),
		    enabled_layers,
		    enabled_layer_settings);
		enable_layer_setting(
		    vk::LayerSettingEXT(
		        validation_layer_name, "validate_best_practices_nvidia", vk::LayerSettingTypeEXT::eBool32, 1, &setting_validate_best_practices_nvidia),
		    enabled_layers,
		    enabled_layer_settings);
	}
#	endif

#	if defined(VKB_VALIDATION_LAYERS_SYNCHRONIZATION)
	const vk::Bool32 setting_validate_sync            = true;
	const vk::Bool32 setting_validate_sync_heuristics = true;
	if (validation_features)
	{
		enable_layer_setting(vk::LayerSettingEXT(validation_layer_name, "validate_sync", vk::LayerSettingTypeEXT::eBool32, 1, &setting_validate_sync),
		                     enabled_layers,
		                     enabled_layer_settings);
		enable_layer_setting(
		    vk::LayerSettingEXT(
		        validation_layer_name, "syncval_shader_accesses_heuristic", vk::LayerSettingTypeEXT::eBool32, 1, &setting_validate_sync_heuristics),
		    enabled_layers,
		    enabled_layer_settings);
	}
#	endif
#endif

	vk::LayerSettingsCreateInfoEXT layerSettingsCreateInfo;

	// If layer settings are defined, then activate the sample's required layer settings during instance creation
	if (enabled_layer_settings.size() > 0)
	{
		layerSettingsCreateInfo.settingCount = static_cast<uint32_t>(enabled_layer_settings.size());
		layerSettingsCreateInfo.pSettings    = enabled_layer_settings.data();
		layerSettingsCreateInfo.pNext        = instance_info.pNext;
		instance_info.pNext                  = &layerSettingsCreateInfo;
	}

	// Create the Vulkan instance
	handle = vk::createInstance(instance_info);

	// initialize the Vulkan-Hpp default dispatcher on the instance
	VULKAN_HPP_DEFAULT_DISPATCHER.init(handle);

	// Need to load volk for all the not-yet Vulkan-Hpp calls
	volkLoadInstance(handle);

#ifdef USE_VALIDATION_LAYERS
	if (has_debug_utils)
	{
		debug_utils_messenger = handle.createDebugUtilsMessengerEXT(debug_utils_create_info);
	}
	else if (has_debug_report)
	{
		debug_report_callback = handle.createDebugReportCallbackEXT(debug_report_create_info);
	}
#endif

	query_gpus();
}

template <vkb::BindingType bindingType>
inline Instance<bindingType>::Instance(std::string const                            &application_name,
                                       std::unordered_map<char const *, bool> const &requested_extensions,
                                       std::unordered_map<char const *, bool> const &requested_layers,
                                       std::vector<VkLayerSettingEXT> const         &requested_layer_settings,
                                       uint32_t                                      api_version) :
    Instance<bindingType>(application_name,
                          requested_extensions,
                          requested_layers,
                          reinterpret_cast<std::vector<vk::LayerSettingEXT> const &>(requested_layer_settings),
                          api_version)
{}

template <vkb::BindingType bindingType>
inline Instance<bindingType>::Instance(vk::Instance instance, const std::vector<const char *> &externally_enabled_extensions, bool needsToInitializeDispatcher) :
    handle{instance}
{
	if (needsToInitializeDispatcher)
	{
#if defined(_HPP_VULKAN_LIBRARY)
		static vk::detail::DynamicLoader dl(_HPP_VULKAN_LIBRARY);
#else
		static vk::detail::DynamicLoader dl;
#endif
		PFN_vkGetInstanceProcAddr vkGetInstanceProcAddr = dl.getProcAddress<PFN_vkGetInstanceProcAddr>("vkGetInstanceProcAddr");
		VULKAN_HPP_DEFAULT_DISPATCHER.init(vkGetInstanceProcAddr);
		VULKAN_HPP_DEFAULT_DISPATCHER.init(instance);
	}

	// Some parts of the framework will check for certain extensions to be enabled
	// To make those work we need to copy over externally enabled extensions into this class
	for (auto extension : externally_enabled_extensions)
	{
		enabled_extensions.push_back(extension);
	}

	if (handle)
	{
		query_gpus();
	}
	else
	{
		throw std::runtime_error("Instance not valid");
	}
}

template <vkb::BindingType bindingType>
inline Instance<bindingType>::Instance(VkInstance instance, const std::vector<const char *> &externally_enabled_extensions) :
    Instance<bindingType>(static_cast<vk::Instance>(instance), externally_enabled_extensions, true)
{}

template <vkb::BindingType bindingType>
inline Instance<bindingType>::~Instance()
{
#ifdef USE_VALIDATION_LAYERS
	if (debug_utils_messenger)
	{
		handle.destroyDebugUtilsMessengerEXT(debug_utils_messenger);
	}
	if (debug_report_callback)
	{
		handle.destroyDebugReportCallbackEXT(debug_report_callback);
	}
#endif

	if (handle)
	{
		handle.destroy();
	}
}

#if defined(USE_VALIDATION_LAYERS)
#	undef USE_VALIDATION_LAYERS
#endif

#if defined(USE_VALIDATION_LAYER_FEATURES)
#	undef USE_VALIDATION_LAYER_FEATURES
#endif

template <vkb::BindingType bindingType>
inline typename Instance<bindingType>::PhysicalDeviceType &Instance<bindingType>::get_first_gpu()
{
	assert(!gpus.empty() && "No physical devices were found on the system.");

	// Find a discrete GPU
	auto gpuIt = std::ranges::find_if(gpus, [](auto const &gpu) { return gpu->get_properties().deviceType == vk::PhysicalDeviceType::eDiscreteGpu; });
	if (gpuIt == gpus.end())
	{
		LOGW("Couldn't find a discrete physical device, picking default GPU");
		gpuIt = gpus.begin();
	}
	if constexpr (bindingType == BindingType::Cpp)
	{
		return **gpuIt;
	}
	else
	{
		return reinterpret_cast<vkb::PhysicalDevice &>(**gpuIt);
	}
}

template <vkb::BindingType bindingType>
inline typename Instance<bindingType>::InstanceType Instance<bindingType>::get_handle() const
{
	if constexpr (bindingType == BindingType::Cpp)
	{
		return handle;
	}
	else
	{
		return static_cast<VkInstance>(handle);
	}
	return handle;
}

template <vkb::BindingType bindingType>
inline typename Instance<bindingType>::PhysicalDeviceType &Instance<bindingType>::get_suitable_gpu(SurfaceType surface, bool headless_surface)
{
	assert(!gpus.empty() && "No physical devices were found on the system.");

	// A GPU can be explicitly selected via the command line (see plugins/gpu_selection.cpp), this overrides the below GPU selection algorithm
	auto gpuIt = gpus.begin();
	if (selected_gpu_index != ~0)
	{
		LOGI("Explicitly selecting GPU {}", selected_gpu_index);
		if (selected_gpu_index > gpus.size() - 1)
		{
			throw std::runtime_error("Selected GPU index is not within no. of available GPUs");
		}
		gpuIt = gpus.begin() + selected_gpu_index;
	}
	else
	{
		if (headless_surface)
		{
			LOGW("Using headless surface with multiple GPUs. Considered explicitly selecting the target GPU.")
		}

		// Find a discrete GPU
#if 0
		gpuIt = std::ranges::find_if(gpus,
		                             [&surface](auto const &gpu) {
			                             return (gpu->get_properties().deviceType == vk::PhysicalDeviceType::eDiscreteGpu) &&
			                                    std::ranges::any_of(std::views::iota(size_t(0), gpu->get_queue_family_properties().size()),
			                                                        [&gpu, &surface](auto const &queue_idx) {
				                                                        return gpu->get_handle().getSurfaceSupportKHR(static_cast<uint32_t>(queue_idx), surface);
			                                                        });
		                             });
#else
		gpuIt = std::ranges::find_if(gpus,
		                             [&surface](auto const &gpu) {
			                             auto gpu_supports_surface = [&gpu, &surface]() {
				                             for (uint32_t queue_idx = 0; queue_idx < gpu->get_queue_family_properties().size(); ++queue_idx)
				                             {
					                             if (gpu->get_handle().getSurfaceSupportKHR(queue_idx, surface))
					                             {
						                             return true;
					                             }
				                             }
				                             return false;
			                             };
			                             return (gpu->get_properties().deviceType == vk::PhysicalDeviceType::eDiscreteGpu) && gpu_supports_surface();
		                             });
#endif
		if (gpuIt == gpus.end())
		{
			LOGW("Couldn't find a discrete physical device that supports the surface, picking default GPU");
			gpuIt = gpus.begin();
		}
	}

	if constexpr (bindingType == BindingType::Cpp)
	{
		return **gpuIt;
	}
	else
	{
		return reinterpret_cast<vkb::PhysicalDevice &>(**gpuIt);
	}
}

template <vkb::BindingType bindingType>
inline bool Instance<bindingType>::is_enabled(char const *extension) const
{
	return std::ranges::find_if(enabled_extensions, [extension](char const *enabled_extension) { return strcmp(extension, enabled_extension) == 0; }) !=
	       enabled_extensions.end();
}

template <vkb::BindingType bindingType>
inline void Instance<bindingType>::query_gpus()
{
	// Querying valid physical devices on the machine
	std::vector<vk::PhysicalDevice> physical_devices = handle.enumeratePhysicalDevices();
	if (physical_devices.empty())
	{
		throw std::runtime_error("Couldn't find a physical device that supports Vulkan.");
	}

	// Create gpus wrapper objects from the vk::PhysicalDevice's
	for (auto &physical_device : physical_devices)
	{
		if constexpr (bindingType == BindingType::Cpp)
		{
			gpus.push_back(std::make_unique<vkb::core::HPPPhysicalDevice>(*this, physical_device));
		}
		else
		{
			gpus.push_back(std::make_unique<vkb::core::HPPPhysicalDevice>(*reinterpret_cast<vkb::core::InstanceCpp *>(this), physical_device));
		}
	}
}

template <vkb::BindingType bindingType>
inline std::vector<char const *> const &Instance<bindingType>::get_extensions()
{
	return enabled_extensions;
}
}        // namespace core
}        // namespace vkb
