/**
 * @file   video_display/vulkan_context.cpp
 * @author Martin Bela      <492789@mail.muni.cz>
 */
/*
 * Copyright (c) 2021-2023 CESNET, z. s. p. o.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, is permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * 3. Neither the name of CESNET nor the names of its contributors may be
 *    used to endorse or promote products derived from this software without
 *    specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHORS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING,
 * BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
 * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
 * EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
 * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
 * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include "vulkan_context.hpp"
#include <algorithm>            // for std::any_of, std::find_if
#include <cassert>
#include <iostream>
#include "debug.h"

#define MOD_NAME "[vulkan] "

using namespace vulkan_display_detail;
using namespace vulkan_display;

namespace {

#if VK_HEADER_VERSION >= 304
VKAPI_ATTR VkBool32 VKAPI_CALL debug_callback(
        [[maybe_unused]] vk::DebugUtilsMessageSeverityFlagBitsEXT message_severity,
        [[maybe_unused]] vk::DebugUtilsMessageTypeFlagsEXT message_type,
        const vk::DebugUtilsMessengerCallbackDataEXT* callback_data,
        [[maybe_unused]] void* user_data)
{
        int level = LOG_LEVEL_NOTICE;
        if      (vk::DebugUtilsMessageSeverityFlagBitsEXT::eError & message_severity)   level = LOG_LEVEL_ERROR;
        else if (vk::DebugUtilsMessageSeverityFlagBitsEXT::eWarning & message_severity) level = LOG_LEVEL_WARNING;
        else if (vk::DebugUtilsMessageSeverityFlagBitsEXT::eInfo & message_severity)    level = LOG_LEVEL_INFO;
        else if (vk::DebugUtilsMessageSeverityFlagBitsEXT::eVerbose & message_severity) level = LOG_LEVEL_VERBOSE;

        log_msg(level, MOD_NAME "validation layer: %s\n", callback_data->pMessage);

        if (message_type != vk::DebugUtilsMessageTypeFlagBitsEXT::eGeneral){
                //assert(false);
        }

        return VK_FALSE;
}
#else /// compat (eg. Debian 11) @todo TOREMOVE later
 VKAPI_ATTR VkBool32 VKAPI_CALL debug_callback(
         [[maybe_unused]] VkDebugUtilsMessageSeverityFlagBitsEXT message_severity,
         [[maybe_unused]] VkDebugUtilsMessageTypeFlagsEXT message_type,
         const VkDebugUtilsMessengerCallbackDataEXT* callback_data,
         [[maybe_unused]] void* user_data)
{
        int level = LOG_LEVEL_NOTICE;
        if      (VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT & message_severity)   level = LOG_LEVEL_ERROR;
        else if (VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT & message_severity) level = LOG_LEVEL_WARNING;
        else if (VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT & message_severity)    level = LOG_LEVEL_INFO;
        else if (VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT & message_severity) level = LOG_LEVEL_VERBOSE;

        log_msg(level, MOD_NAME "validation layer: %s\n", callback_data->pMessage);

        if (message_type != VkDebugUtilsMessageTypeFlagBitsEXT::VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT){
                //assert(false);
        }

        return VK_FALSE;
}
#endif

void check_validation_layers(const std::vector<const char*>& required_layers) {
        std::vector<vk::LayerProperties>  layers = vk::enumerateInstanceLayerProperties();
        //for (auto& l : layers) puts(l.layerName);

        for (const auto& req_layer : required_layers) {
                auto layer_equals = [req_layer](auto const &layer) { return strcmp(req_layer, layer.layerName) == 0; };
                bool found = std::any_of(layers.begin(), layers.end(), layer_equals);
                if (!found) {
                        throw VulkanError{"Layer "s + req_layer + " is not supported."};
                }
        }
}

/**
 * handle VK_KHR_portability_enumeration (non-)presence
 *
 * @returns if VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR can be added
 *
 * Also append the extension if supported to handle non-native drivers (as
 * MoltenVK, that set VK_KHR_portability_subset, otherwise those won't be
 * enumerated).
 *
 * In case the VK_KHR_portability_enumeration is explicitly required but not
 * present, remove it from extension list in macOS - this will allow linking
 * directly to MoltenVK in mac (not to vulkan-loader), which doesn't have the
 * enumerate extension.
 */
bool
handle_enumerate_portability_extension(
    std::vector<const char *> &required_extensions)
{
#if VK_HEADER_VERSION >= 270
        std::vector<vk::ExtensionProperties> extensions =
            vk::enumerateInstanceExtensionProperties(nullptr);

        auto eq_portability_ext = [](const char *extension) {
                return strcmp(VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME,
                              extension) == 0;
        };

        if (std::any_of(
                extensions.begin(), extensions.end(), [](auto const &exten) {
                        return strcmp(
                                   VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME,
                                   exten.extensionName) == 0;
                })) {
                if (!std::any_of(required_extensions.begin(),
                                required_extensions.end(),
                                eq_portability_ext)) {
                        log_msg(
                            LOG_LEVEL_DEBUG,
                            MOD_NAME "adding"
                            " " VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME
                            " to list of required extensions\n");
                        required_extensions.push_back(
                            VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME);
                }
                return true;
        }
#ifdef __APPLE__ // SDL3 sets it always in macOS  but not present if linked with
                 // MoltenVK (only with vulkan-loader)
        const auto it = std::find_if(required_extensions.begin(),
                               required_extensions.end(), eq_portability_ext);
        if (it != required_extensions.end()) {
                log_msg(LOG_LEVEL_INFO,
                               MOD_NAME VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME
                               " required but not found... erasing "
                               "this extension...\n");
                required_extensions.erase(it);
        }
#endif
#else // VK_HEADER_VERSION < 270
        (void) required_extensions;
#endif // VK_HEADER_VERSION < 270

        return false;
}

/**
 * this checks required_extensions prior to vk::createInstance(), which would
 * throw the exception as well if extension is missing but without providing
 * its name.
 */
void check_instance_extensions(const std::vector<const char*>& required_extensions) {
        std::vector<vk::ExtensionProperties> extensions = vk::enumerateInstanceExtensionProperties(nullptr);

        for (const auto& req_exten : required_extensions) {
                auto extension_equals = [req_exten](auto const &exten) { return strcmp(req_exten, exten.extensionName) == 0; };
                bool found = std::any_of(extensions.begin(), extensions.end(), extension_equals);
                if (!found) {
                        throw VulkanError{"Instance extension "s + req_exten + " is not supported."};
                }
        }
}

bool check_device_extensions(bool propagate_error,
        const std::vector<const char*>& required_extensions, const vk::PhysicalDevice& device)
{
        std::vector<vk::ExtensionProperties> extensions = device.enumerateDeviceExtensionProperties(nullptr);

        for (const auto& req_exten : required_extensions) {
                auto extension_equals = [req_exten](auto const &exten) { return strcmp(req_exten, exten.extensionName) == 0; };
                bool found = std::any_of(extensions.begin(), extensions.end(), extension_equals);
                if (!found) {
                        if (propagate_error) {
                                throw VulkanError{"Device extension "s + req_exten + " is not supported."};
                        }
                        return false;
                }
        }
        return true;
}

uint32_t choose_queue_family_index(vk::PhysicalDevice gpu, vk::SurfaceKHR surface) {
        assert(gpu);

        std::vector<vk::QueueFamilyProperties> families = gpu.getQueueFamilyProperties();

        for (uint32_t i = 0; i < families.size(); i++) {
                VkBool32 surface_supported = true;
                if (surface) {
                        surface_supported = gpu.getSurfaceSupportKHR(i, surface);
                }

                if (surface_supported &&
                        (families[i].queueFlags & vk::QueueFlagBits::eGraphics) &&
                        (families[i].queueFlags & vk::QueueFlagBits::eCompute))
                {
                        return i;
                }
        }
        return no_queue_index_found;
}

const std::vector required_gpu_extensions = { "VK_KHR_swapchain" };

bool is_gpu_suitable(bool propagate_error, vk::PhysicalDevice gpu, vk::SurfaceKHR surface = nullptr) {
        bool result = check_device_extensions(propagate_error, required_gpu_extensions, gpu);
        if (!result) {
                return false;
        }
        uint32_t index = choose_queue_family_index(gpu, surface);
        return index != no_queue_index_found;
}

vk::PhysicalDevice choose_suitable_GPU(const std::vector<vk::PhysicalDevice>& gpus, vk::SurfaceKHR surface, uint32_t req_device_type) {
        assert(surface);
        for (const auto& gpu : gpus) {
                auto properties = gpu.getProperties();
                if (properties.deviceType == vk::PhysicalDeviceType::eDiscreteGpu) {
                        if (is_gpu_suitable(false, gpu, surface) && (req_device_type == vulkan_display::gpu_discrete
                                                || req_device_type == vulkan_display::no_gpu_selected)) {
                                return gpu;
                        }
                }
        }

        for (const auto& gpu : gpus) {
                auto properties = gpu.getProperties();
                if (properties.deviceType == vk::PhysicalDeviceType::eIntegratedGpu) {
                        if (is_gpu_suitable(false, gpu, surface) && (req_device_type == vulkan_display::gpu_integrated
                                                || req_device_type == vulkan_display::no_gpu_selected)) {
                                return gpu;
                        }
                }
        }

        for (const auto& gpu : gpus) {
                if (is_gpu_suitable(false, gpu, surface) && req_device_type == vulkan_display::no_gpu_selected) {
                        return gpu;
                }
        }

        throw VulkanError{"No suitable gpu found."};
}

vk::PhysicalDevice choose_gpu_by_index(const std::vector<vk::PhysicalDevice>& gpus, uint32_t gpu_index) {
        if (gpu_index >= gpus.size()) {
                throw VulkanError{"GPU index is not valid."};
        }
        std::vector<std::pair<std::string, vk::PhysicalDevice>> gpu_names;
        gpu_names.reserve(gpus.size());

        auto get_gpu_name = [](auto gpu) -> std::pair<std::string, vk::PhysicalDevice> {
                return { gpu.getProperties().deviceName, gpu };
        };

        std::transform(gpus.begin(), gpus.end(), std::back_inserter(gpu_names), get_gpu_name);

        std::sort(gpu_names.begin(), gpu_names.end());
        return gpu_names[gpu_index].second;
}

vk::CompositeAlphaFlagBitsKHR get_composite_alpha(vk::CompositeAlphaFlagsKHR capabilities) {
        uint32_t result = 1;
        while (!(result & static_cast<uint32_t>(capabilities))) {
                result <<= 1u;
        }
        return static_cast<vk::CompositeAlphaFlagBitsKHR>(result);
}

template<typename T>
bool contains(const std::vector<T>& vec, const T& key){
        return std::find(vec.begin(), vec.end(), key) != vec.end();
}

vk::SurfaceFormatKHR get_surface_format(vk::PhysicalDevice gpu, vk::SurfaceKHR surface) {
        std::vector<vk::SurfaceFormatKHR> available_formats = gpu.getSurfaceFormatsKHR(surface);

        std::array<vk::SurfaceFormatKHR, 5> preferred_formats {{
                {vk::Format::eA2B10G10R10UnormPack32, vk::ColorSpaceKHR::eSrgbNonlinear},
                {vk::Format::eB8G8R8A8Unorm, vk::ColorSpaceKHR::eSrgbNonlinear},
                {vk::Format::eR8G8B8A8Unorm, vk::ColorSpaceKHR::eSrgbNonlinear},
        }};

        for(auto& format: preferred_formats){
                if (contains(available_formats, format)) {
                        return format;
                }
        }

        return available_formats[0];
}

vk::PresentModeKHR get_present_mode(vk::PhysicalDevice gpu, vk::SurfaceKHR surface, vk::PresentModeKHR preferred_present_mode) {
        std::vector<vk::PresentModeKHR> modes = gpu.getSurfacePresentModesKHR(surface);

        vk::PresentModeKHR preferred = preferred_present_mode;
        if (contains(modes, preferred)) {
                return preferred;
        }
        
        // Mailbox is alternative to Immediate, Fifo to everything else
        auto alternative = (preferred == vk::PresentModeKHR::eImmediate 
                ? vk::PresentModeKHR::eMailbox
                : vk::PresentModeKHR::eFifo);

        if (contains(modes, alternative)) {
                return alternative;
        }

        return modes[0];
}

void log_gpu_info(vk::PhysicalDeviceProperties const &gpu_properties, uint32_t vulkan_version){
        log_msg(LOG_LEVEL_INFO, MOD_NAME "Vulkan uses GPU called: %s\n", &gpu_properties.deviceName[0]);
        log_msg(LOG_LEVEL_INFO, MOD_NAME "Used Vulkan API: %d.%d\n",
                        VK_VERSION_MAJOR(vulkan_version),
                        VK_VERSION_MINOR(vulkan_version));
}

vk::PhysicalDevice create_physical_device(vk::Instance instance, vk::SurfaceKHR surface, uint32_t gpu_index) {
        assert(instance);
        assert(surface);
        std::vector<vk::PhysicalDevice> gpus = instance.enumeratePhysicalDevices();

        if (gpu_index >= vulkan_display::gpu_macro_min) {
                return choose_suitable_GPU(gpus, surface, gpu_index);
        } else {
                auto gpu = choose_gpu_by_index(gpus, gpu_index);
                is_gpu_suitable(true, gpu, surface);
                return gpu;
        }
}

void create_swapchain_views(vk::Device device, vk::SwapchainKHR swapchain, vk::Format format, std::vector<SwapchainImage>& swapchain_images) {
        std::vector<vk::Image> images = device.getSwapchainImagesKHR(swapchain);
        auto image_count = static_cast<uint32_t>(images.size());

        vk::ImageViewCreateInfo image_view_info = 
                default_image_view_create_info(format);

        swapchain_images.resize(image_count);
        for (uint32_t i = 0; i < image_count; i++) {
                SwapchainImage& swapchain_image = swapchain_images[i];
                swapchain_image.image = images[i];

                image_view_info.setImage(swapchain_image.image);
                swapchain_image.view = device.createImageView(image_view_info);
        }
}

} //namespace ------------------------------------------------------------------------


namespace vulkan_display {

void VulkanInstance::init(std::vector<const char*>& required_extensions, bool enable_validation) {
        std::vector<const char*> validation_layers{};
        if (enable_validation) {
                validation_layers.push_back("VK_LAYER_KHRONOS_validation");
                check_validation_layers(validation_layers);

                const char* debug_extension = VK_EXT_DEBUG_UTILS_EXTENSION_NAME;
                required_extensions.push_back(debug_extension);
        }

        const bool create_enumerate_portability_bit_supported =
            handle_enumerate_portability_extension(required_extensions);
        check_instance_extensions(required_extensions);

        vk::ApplicationInfo app_info{};
        app_info.setApiVersion(VK_API_VERSION_1_1);
        vulkan_version = VK_API_VERSION_1_1;

        vk::InstanceCreateInfo instance_info{};
        instance_info
                .setPApplicationInfo(&app_info)
                .setEnabledLayerCount(static_cast<uint32_t>(validation_layers.size()))
                .setPpEnabledLayerNames(validation_layers.data())
                .setEnabledExtensionCount(static_cast<uint32_t>(required_extensions.size()))
                .setPpEnabledExtensionNames(required_extensions.data());
        if (create_enumerate_portability_bit_supported) {
#if VK_HEADER_VERSION >= 208
                instance_info.setFlags(vk::InstanceCreateFlagBits::eEnumeratePortabilityKHR);
#endif // VK_HEADER_VERSION >= 208
        }
        auto result = vk::createInstance(&instance_info, nullptr, &instance);
        
        switch (result) {
                case vk::Result::eSuccess:
                        break;
                case vk::Result::eErrorIncompatibleDriver:
                        app_info.apiVersion = VK_API_VERSION_1_0;
                        vulkan_version = VK_API_VERSION_1_0;
                        result = vk::createInstance(&instance_info, nullptr, &instance);
                        if(result != vk::Result::eSuccess){
                            throw VulkanError{"Vulkan instance cannot be created: "s + vk::to_string(result)};
                        }
                        break;
                default:
                        throw VulkanError{"Vulkan instance cannot be created: "s + vk::to_string(result)};
        }

        if (enable_validation) {
#if VK_HEADER_VERSION >= 301
                dynamic_dispatcher = std::make_unique<vk::detail::DispatchLoaderDynamic>((VkInstance) instance, vkGetInstanceProcAddr);
#else
                dynamic_dispatcher = std::make_unique<vk::DispatchLoaderDynamic>((VkInstance) instance, vkGetInstanceProcAddr);
#endif
                init_validation_layers_error_messenger();
        }
}

void VulkanInstance::init_validation_layers_error_messenger() {
        vk::DebugUtilsMessengerCreateInfoEXT messenger_info{};
        using Severity = vk::DebugUtilsMessageSeverityFlagBitsEXT;
        using Type = vk::DebugUtilsMessageTypeFlagBitsEXT;
        messenger_info
                .setMessageSeverity(Severity::eError | Severity::eInfo | Severity::eWarning) // severity::eInfo |
                .setMessageType(Type::eGeneral | Type::ePerformance | Type::eValidation)
                .setPfnUserCallback(debug_callback)
                .setPUserData(nullptr);
        messenger = instance.createDebugUtilsMessengerEXT(messenger_info, nullptr, *dynamic_dispatcher);
}

void VulkanInstance::get_available_gpus(std::vector<std::pair<std::string, bool>>& gpus) {
        assert(instance);

        std::vector<vk::PhysicalDevice> physical_devices = instance.enumeratePhysicalDevices();
        gpus.clear();
        gpus.reserve(physical_devices.size());
        for (const auto& gpu : physical_devices) {
                auto properties = gpu.getProperties();
                gpus.emplace_back(properties.deviceName, true);
        }
        std::sort(gpus.begin(), gpus.end());
}

void VulkanInstance::destroy() {
        if (instance) {
                instance.destroy();
                if (messenger) {
                        instance.destroy(messenger, nullptr, *dynamic_dispatcher);
                }
                dynamic_dispatcher = nullptr;
                instance = nullptr;
        }
}


} // namespace vulkan_display ----------------------------------------------------------------------------


namespace vulkan_display_detail { //------------------------------------------------------------------------

void VulkanContext::create_logical_device() {
        assert(gpu);
        assert(queue_family_index != no_queue_index_found);

        constexpr std::array priorities = { 1.0f };
        vk::DeviceQueueCreateInfo queue_info{};
        queue_info
                .setQueueFamilyIndex(queue_family_index)
                .setPQueuePriorities(priorities.data())
                .setQueueCount(1);

        vk::DeviceCreateInfo device_info{};
        device_info
                .setPNext(nullptr)
                .setQueueCreateInfoCount(1)
                .setPQueueCreateInfos(&queue_info)
                .setEnabledExtensionCount(static_cast<uint32_t>(required_gpu_extensions.size()))
                .setPpEnabledExtensionNames(required_gpu_extensions.data());

        vk::PhysicalDeviceFeatures2 features2{};
        vk::PhysicalDeviceSamplerYcbcrConversionFeatures yCbCr_feature{};
        if (vulkan_version == VK_API_VERSION_1_1) {
                features2.setPNext(&yCbCr_feature);
                gpu.getFeatures2(&features2);
                if (yCbCr_feature.samplerYcbcrConversion) {
                        yCbCr_supported = true;
                        device_info.setPNext(&features2);
                        log_msg(LOG_LEVEL_INFO, MOD_NAME "yCbCr feature supported.\n");
                }
        }

        //reset features
        features2 = vk::PhysicalDeviceFeatures2{};
        features2.setPNext(&yCbCr_feature);

        device = gpu.createDevice(device_info);
}

void VulkanContext::create_swap_chain(vk::SwapchainKHR&& old_swapchain) {
        constexpr int initialization_attempts = 3;
        for (int attempt = 0; attempt < initialization_attempts; attempt++) {
                auto& capabilities = swapchain_attributes.capabilities;
                capabilities = gpu.getSurfaceCapabilitiesKHR(surface);

                swapchain_attributes.format = get_surface_format(gpu, surface);
                swapchain_attributes.mode = get_present_mode(gpu, surface, preferred_present_mode);

                vk::Extent2D swapchain_image_size;
                swapchain_image_size.width = std::clamp(window_parameters.width,
                        capabilities.minImageExtent.width,
                        capabilities.maxImageExtent.width);
                swapchain_image_size.height = std::clamp(window_parameters.height,
                        capabilities.minImageExtent.height,
                        capabilities.maxImageExtent.height);
                swapchain_attributes.image_size = swapchain_image_size;

                uint32_t image_count = std::max(uint32_t{2}, capabilities.minImageCount);
                if (capabilities.maxImageCount != 0) {
                        image_count = std::min(image_count, capabilities.maxImageCount);
                }

                log_msg(LOG_LEVEL_INFO, MOD_NAME "Recreating swapchain, size: %dx%d, format %s\n", swapchain_image_size.width, swapchain_image_size.height, vk::to_string(swapchain_attributes.format.format).c_str());

                //assert(capabilities.supportedUsageFlags & vk::ImageUsageFlagBits::eTransferDst);
                vk::SwapchainCreateInfoKHR swapchain_info{};
                swapchain_info
                        .setSurface(surface)
                        .setImageFormat(swapchain_attributes.format.format)
                        .setImageColorSpace(swapchain_attributes.format.colorSpace)
                        .setPresentMode(swapchain_attributes.mode)
                        .setMinImageCount(image_count)
                        .setImageExtent(swapchain_image_size)
                        .setImageArrayLayers(1)
                        .setImageUsage(vk::ImageUsageFlagBits::eColorAttachment)
                        .setImageSharingMode(vk::SharingMode::eExclusive)
                        .setPreTransform(swapchain_attributes.capabilities.currentTransform)
                        .setCompositeAlpha(get_composite_alpha(swapchain_attributes.capabilities.supportedCompositeAlpha))
                        .setClipped(true)
                        .setOldSwapchain(old_swapchain);
                try{
                        swapchain = device.createSwapchainKHR(swapchain_info);
                        device.destroy(old_swapchain);
                        old_swapchain = nullptr;
                        return;
                }
                catch(std::exception& err){
                        log_msg(LOG_LEVEL_ERROR, MOD_NAME "Recreation unsuccessful: %s\n", err.what());
                        device.destroy(old_swapchain);
                        old_swapchain = nullptr;
                        if(attempt + 1 == initialization_attempts){
                            throw;
                        }
                }
        }
        assert(false);
}

void VulkanContext::init(vulkan_display::VulkanInstance&& instance, vk::SurfaceKHR surface,
        WindowParameters parameters, uint32_t gpu_index, vk::PresentModeKHR preferredMode)
{
        assert(!this->instance);
        this->instance = instance.instance;
        this->dynamic_dispatcher = std::move(instance.dynamic_dispatcher);
        this->messenger = instance.messenger;
        this->vulkan_version = instance.vulkan_version;
        instance.instance = nullptr;
        instance.messenger = nullptr;

        this->surface = surface;
        this->preferred_present_mode = preferredMode;
        window_parameters = parameters;

        gpu = create_physical_device(this->instance, surface, gpu_index);

        queue_family_index = choose_queue_family_index(gpu, surface);
        assert(queue_family_index != no_queue_index_found);

        create_logical_device();
        auto properties = gpu.getProperties();
        if (properties.apiVersion < VK_API_VERSION_1_1) {
                vulkan_version = VK_API_VERSION_1_0;
        }

        log_gpu_info(properties, vulkan_version);
        
        queue = device.getQueue(queue_family_index, 0);
        create_swap_chain();
        create_swapchain_views(device, swapchain, swapchain_attributes.format.format, swapchain_images);
}

void VulkanContext::create_framebuffers(vk::RenderPass render_pass) {
        vk::FramebufferCreateInfo framebuffer_info;
        framebuffer_info
                .setRenderPass(render_pass)
                .setWidth(swapchain_attributes.image_size.width)
                .setHeight(swapchain_attributes.image_size.height)
                .setLayers(1);

        for(auto& swapchain_image : swapchain_images){
                framebuffer_info
                        .setAttachmentCount(1)
                        .setPAttachments(&swapchain_image.view);
                swapchain_image.framebuffer = device.createFramebuffer(framebuffer_info);
        }
}

void VulkanContext::recreate_swapchain(WindowParameters parameters, vk::RenderPass render_pass) {
        window_parameters = parameters;

        device.waitIdle();

        destroy_framebuffers();
        destroy_swapchain_views();
        vk::SwapchainKHR old_swap_chain = swapchain;
        create_swap_chain(std::move(old_swap_chain));
        create_swapchain_views(device, swapchain, swapchain_attributes.format.format, swapchain_images);
        create_framebuffers(render_pass);
        swapchain_was_suboptimal = false;
}

uint32_t VulkanContext::acquire_next_swapchain_image(vk::Semaphore acquire_semaphore) {
        if(swapchain_was_suboptimal){
                return swapchain_image_out_of_date;
        }

        constexpr uint64_t timeout = 1'000'000'000; // 1s = 1 000 000 000 nanoseconds
        uint32_t image_index;
        auto acquired = device.acquireNextImageKHR(swapchain, timeout, acquire_semaphore, nullptr, &image_index);
        switch (acquired) {
                case vk::Result::eSuccess:
                        break;
                case vk::Result::eSuboptimalKHR:
                        swapchain_was_suboptimal = true;
                        break;
                case vk::Result::eErrorOutOfDateKHR:
                        image_index = swapchain_image_out_of_date;
                        break;
                case vk::Result::eTimeout:
                        image_index = swapchain_image_timeout;
                        break;
                default:
                        throw VulkanError{"Next swapchain image cannot be acquired."s + vk::to_string(acquired)};
        }
        return image_index;
}

void VulkanContext::destroy() {
        if (device) {
                // static_cast to silence nodiscard warning
                device.waitIdle();
                destroy_framebuffers();
                destroy_swapchain_views();
                device.destroy(swapchain);
                device.destroy();
        }
        if (instance) {
                instance.destroy(surface);
                if (messenger) {
                        instance.destroy(messenger, nullptr, *dynamic_dispatcher);
                }
                instance.destroy();
        }
        dynamic_dispatcher = nullptr;
}


} //vulkan_display_detail

