#include "vulkan_image.h"

#include <SDL3/SDL_log.h>
#include <stdint.h>

#include "src/graphics_context.h"
#include "utilities/commons.h"
#include "utilities/errors_common.h"
#include "utilities/file_utils.h"
#include "vulkan/vulkan_core.h"
#include "vulkan_buffers.h"
#include "vulkan_commands.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define IMAGE_TAKEN 1U
#define IMAGE_FREE 0U

rse_err_t sampler_create(struct graphics_context_t* context, VkSampler* sampler)
{
    VkSamplerCreateInfo create_info = {0};

    create_info.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
    create_info.pNext = NULL;
    create_info.flags = 0U;
    create_info.magFilter = VK_FILTER_LINEAR;
    create_info.minFilter = VK_FILTER_LINEAR;
    create_info.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR;
    create_info.addressModeU = VK_SAMPLER_ADDRESS_MODE_REPEAT;
    create_info.addressModeV = VK_SAMPLER_ADDRESS_MODE_REPEAT;
    create_info.addressModeW = VK_SAMPLER_ADDRESS_MODE_REPEAT;
    create_info.mipLodBias = 0.0f;
    create_info.anisotropyEnable = VK_FALSE;  // TODO: Enable this
    create_info.maxAnisotropy = 1.0f;
    create_info.compareEnable = VK_FALSE;
    create_info.compareOp = VK_COMPARE_OP_ALWAYS;
    create_info.minLod = 0.0f;
    create_info.maxLod = 0.0f;
    create_info.borderColor = VK_BORDER_COLOR_INT_OPAQUE_BLACK;
    create_info.unnormalizedCoordinates = VK_FALSE;

    if (VK_SUCCESS != vkCreateSampler(context->vulkan_handles.device, &create_info, NULL, sampler)) {
        SDL_LogCritical(SDL_LOG_CATEGORY_GPU, "Failed to create image view");
        return RSE_ERROR_INTERNAL_ERROR;
    }

    return RSE_ERROR_NO_ERROR;
}

static rse_err_t find_supported_format(struct graphics_context_t* context,
                                       const VkFormat* candidates,
                                       size_t candidates_count,
                                       VkImageTiling tiling,
                                       VkFormatFeatureFlags features,
                                       VkFormat* found_format)
{
    size_t i = 0U;

    for (i = 0U; i < candidates_count; ++i) {
        VkFormatProperties props;
        vkGetPhysicalDeviceFormatProperties(context->vulkan_handles.physical_device, candidates[i], &props);

        if (tiling == VK_IMAGE_TILING_LINEAR && (props.linearTilingFeatures & features) == features) {
            *found_format = candidates[i];
            return RSE_ERROR_NO_ERROR;
        } else if (tiling == VK_IMAGE_TILING_OPTIMAL && (props.optimalTilingFeatures & features) == features) {
            *found_format = candidates[i];
            return RSE_ERROR_NO_ERROR;
        }
    }

    SDL_LogCritical(SDL_LOG_CATEGORY_GPU, "Failed to find correct format");
    return RSE_ERROR_INTERNAL_ERROR;
}

static rse_err_t create_image(struct graphics_context_t* context,
                              struct vulkan_image_t* image,
                              uint32_t width,
                              uint32_t height,
                              VkFormat format,
                              VkSampleCountFlagBits maa_samples,
                              VkImageUsageFlags usage,
                              VmaAllocationCreateFlags allocationFlags)
{
    VkResult result;
    VkExtent3D image_extent = {0};
    VkImageCreateInfo image_info = {0};
    VmaAllocationCreateInfo alloc_create_info = {0};

    image_extent.width = width;
    image_extent.height = height;
    image_extent.depth = 1U;

    image_info.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
    image_info.pNext = NULL;
    image_info.flags = 0U;
    image_info.imageType = VK_IMAGE_TYPE_2D;
    image_info.format = format;
    image_info.extent = image_extent;
    image_info.mipLevels = 1U;
    image_info.arrayLayers = 1U;
    image_info.samples = maa_samples;
    image_info.tiling = VK_IMAGE_TILING_OPTIMAL;
    image_info.usage = usage;
    image_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;  // TODO: Change if used by more than one
                                                         // queue family
    image_info.queueFamilyIndexCount = 0U;
    image_info.pQueueFamilyIndices = NULL;
    image_info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;

    alloc_create_info.flags = allocationFlags;
    alloc_create_info.usage = VMA_MEMORY_USAGE_AUTO;
    alloc_create_info.requiredFlags = 0U;
    alloc_create_info.preferredFlags = 0U;
    alloc_create_info.memoryTypeBits = 0U;
    alloc_create_info.pool = NULL;
    alloc_create_info.pUserData = NULL;
    alloc_create_info.priority = 1.0f;

    result = vmaCreateImage(context->vulkan_handles.allocator,
                            &image_info,
                            &alloc_create_info,
                            &image->image,
                            &image->allocation,
                            &image->allocation_info);
    if (VK_SUCCESS != result) {
        SDL_LogCritical(SDL_LOG_CATEGORY_GPU, "Failed to create image");
        return RSE_ERROR_INTERNAL_ERROR;
    }

    return RSE_ERROR_NO_ERROR;
}

static rse_err_t create_image_view(struct graphics_context_t* context,
                                   struct vulkan_image_t* image,
                                   VkFormat format,
                                   VkImageAspectFlags aspectFlags)
{
    VkImageViewCreateInfo image_view_create_info = {0};

    image_view_create_info.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
    image_view_create_info.pNext = NULL;
    image_view_create_info.flags = 0U;
    image_view_create_info.image = image->image;
    image_view_create_info.viewType = VK_IMAGE_VIEW_TYPE_2D; /* 2D Texture */
    image_view_create_info.format = format;
    image_view_create_info.components.a = VK_COMPONENT_SWIZZLE_IDENTITY;
    image_view_create_info.components.r = VK_COMPONENT_SWIZZLE_IDENTITY;
    image_view_create_info.components.g = VK_COMPONENT_SWIZZLE_IDENTITY;
    image_view_create_info.components.b = VK_COMPONENT_SWIZZLE_IDENTITY;
    image_view_create_info.subresourceRange.aspectMask = aspectFlags;
    image_view_create_info.subresourceRange.baseMipLevel = 0U;
    image_view_create_info.subresourceRange.levelCount = 1U;
    image_view_create_info.subresourceRange.baseArrayLayer = 0U;
    image_view_create_info.subresourceRange.layerCount = 1U;

    if (VK_SUCCESS !=
        vkCreateImageView(context->vulkan_handles.device, &image_view_create_info, NULL, &image->image_view)) {
        SDL_LogCritical(SDL_LOG_CATEGORY_GPU, "Failed to create swapchain image view");
        return RSE_ERROR_INTERNAL_ERROR;
    }
    return RSE_ERROR_NO_ERROR;
}

rse_err_t create_color_resource(struct graphics_context_t* context)
{
    rse_err_t status = RSE_ERROR_NO_ERROR;
    VkFormat color_format = IMAGE_FORMAT;
    VkSurfaceCapabilitiesKHR physical_device_surface_capabilities;

    vkGetPhysicalDeviceSurfaceCapabilitiesKHR(context->vulkan_handles.physical_device,
                                              context->vulkan_handles.surface,
                                              &physical_device_surface_capabilities);

    STATUS_CHECK(create_image(context,
                              &context->color_image,
                              physical_device_surface_capabilities.currentExtent.width,
                              physical_device_surface_capabilities.currentExtent.height,
                              color_format,
                              VK_SAMPLE_COUNT_8_BIT,
                              VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT | VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT,
                              0U));
    STATUS_CHECK(create_image_view(context, &context->color_image, color_format, VK_IMAGE_ASPECT_COLOR_BIT));

    return status;
}

rse_err_t create_depth_resources(struct graphics_context_t* context)
{
    rse_err_t status = RSE_ERROR_NO_ERROR;
    VkFormat depth_format = {0};
    VkSurfaceCapabilitiesKHR physical_device_surface_capabilities;

    STATUS_CHECK(find_depth_format(context, &depth_format));
    vkGetPhysicalDeviceSurfaceCapabilitiesKHR(context->vulkan_handles.physical_device,
                                              context->vulkan_handles.surface,
                                              &physical_device_surface_capabilities);

    STATUS_CHECK(create_image(context,
                              &context->depth_image,
                              physical_device_surface_capabilities.currentExtent.width,
                              physical_device_surface_capabilities.currentExtent.height,
                              depth_format,
                              VK_SAMPLE_COUNT_8_BIT,
                              VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT,
                              0U));
    STATUS_CHECK(create_image_view(context, &context->depth_image, depth_format, VK_IMAGE_ASPECT_DEPTH_BIT));

    return status;
}

/**
 * @brief Translate vulkan format to pixel width
 *
 * @param[int] format Format to translate
 * @param[out] format_size Format size in bytes to return
 * @return RSE_ERROR_NO_ERROR on success, error code otherwise
 */
static rse_err_t format_to_pixel_size(VkFormat format, VkDeviceSize* format_size)
{
    switch (format) {
        case VK_FORMAT_R8_SRGB:
        case VK_FORMAT_R8_SNORM:
        case VK_FORMAT_R8_UNORM:
        case VK_FORMAT_R8_USCALED:
        case VK_FORMAT_R8_SSCALED:
        case VK_FORMAT_R8_UINT:
            *format_size = 1;
            break;
        case VK_FORMAT_R8G8B8_UINT:
        case VK_FORMAT_R8G8B8_UNORM:
            *format_size = 3;
            break;
        case VK_FORMAT_R8G8B8A8_SRGB:
        case VK_FORMAT_R8G8B8A8_UNORM:
            *format_size = 4;
            break;

        default:
            SDL_LogCritical(SDL_LOG_CATEGORY_GPU, "Failed translate vulkan format to pixel width");
            break;
    }

    return RSE_ERROR_NO_ERROR;
}

static rse_err_t transition_image_layout(struct graphics_context_t* context,
                                         struct vulkan_image_t* image,
                                         VkImageLayout old_layout,
                                         VkImageLayout new_layout)
{
    rse_err_t status = RSE_ERROR_NO_ERROR;
    VkImageSubresourceRange range = {0};
    VkImageMemoryBarrier image_barrier = {0};
    VkCommandBuffer command_buffer = {0};
    VkPipelineStageFlags source_stage = {0};
    VkPipelineStageFlags destination_stage = {0};

    /* Put image into correct layout to copy pixels from buffer to image memory
     */
    range.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
    range.baseMipLevel = 0;
    range.levelCount = 1;
    range.baseArrayLayer = 0;
    range.layerCount = 1;

    image_barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
    image_barrier.pNext = NULL;
    image_barrier.oldLayout = old_layout;
    image_barrier.newLayout = new_layout;
    image_barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
    image_barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
    image_barrier.image = image->image;
    image_barrier.subresourceRange = range;

    if (old_layout == VK_IMAGE_LAYOUT_UNDEFINED && new_layout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL) {
        image_barrier.srcAccessMask = 0;
        image_barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;

        source_stage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
        destination_stage = VK_PIPELINE_STAGE_TRANSFER_BIT;
    } else if (old_layout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL &&
               new_layout == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) {
        image_barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
        image_barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;

        source_stage = VK_PIPELINE_STAGE_TRANSFER_BIT;
        destination_stage = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
    } else {
        return RSE_ERROR_INTERNAL_ERROR;
    }

    STATUS_CHECK(begin_single_time_command(context, &command_buffer));
    vkCmdPipelineBarrier(command_buffer, source_stage, destination_stage, 0, 0, NULL, 0, NULL, 1, &image_barrier);
    STATUS_CHECK(end_single_time_comands(context, command_buffer));

    return RSE_ERROR_NO_ERROR;
}

static rse_err_t create_textured_image(struct graphics_context_t* context,
                                       uint32_t width,
                                       uint32_t height,
                                       VkFormat format,
                                       const unsigned char* pixels,
                                       rse_id_t* texture_id)
{
    rse_err_t status = RSE_ERROR_NO_ERROR;
    struct vulkan_buffer_t staging_buffer = {0};
    VkDeviceSize staging_buffer_size = 0U;
    VkCommandBuffer command_buffer = {0};
    VkBufferImageCopy copy_region = {0};
    VkExtent3D image_extent = {0};
    struct vulkan_image_t* free_texture_image = NULL;

    image_extent.width = width;
    image_extent.height = height;
    image_extent.depth = 1U;

    *texture_id = 0U;

    while (free_texture_image == NULL && *texture_id < RSE_MAX_IMAGE_COUNT) {
        if (context->texture_images[*texture_id].id_taken == IMAGE_FREE) {
            free_texture_image = &context->texture_images[*texture_id];
        } else {
            *texture_id += 1;
        }
    }

    /* Check if we found free image handle */
    if (free_texture_image == NULL) {
        SDL_LogCritical(SDL_LOG_CATEGORY_GPU, "Failed to find free texture image handle");
        *texture_id = -1;
        return RSE_ERROR_INTERNAL_ERROR;
    }

    STATUS_CHECK(format_to_pixel_size(format, &staging_buffer_size));

    staging_buffer_size = staging_buffer_size * width * height;

    STATUS_CHECK(
        create_image(context,
                     free_texture_image,
                     width,
                     height,
                     format,
                     VK_SAMPLE_COUNT_1_BIT,
                     VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT,
                     VMA_ALLOCATION_CREATE_DEDICATED_MEMORY_BIT));

    transition_image_layout(context,
                            free_texture_image,
                            VK_IMAGE_LAYOUT_UNDEFINED,
                            VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);

    /* Copy pixel data to GPU memory */
    STATUS_CHECK(
        create_buffer(context,
                      staging_buffer_size,
                      VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
                      VMA_MEMORY_USAGE_AUTO_PREFER_HOST,
                      VMA_ALLOCATION_CREATE_MAPPED_BIT | VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT,
                      &staging_buffer));

    memcpy(staging_buffer.allocation_info.pMappedData, pixels, staging_buffer_size);

    copy_region.bufferOffset = 0U;
    copy_region.bufferRowLength = 0U;
    copy_region.bufferImageHeight = 0U;
    copy_region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
    copy_region.imageSubresource.mipLevel = 0U;
    copy_region.imageSubresource.baseArrayLayer = 0U;
    copy_region.imageSubresource.layerCount = 1U;
    copy_region.imageOffset.x = 0;
    copy_region.imageOffset.y = 0;
    copy_region.imageOffset.z = 0;
    copy_region.imageExtent = image_extent;

    STATUS_CHECK(begin_single_time_command(context, &command_buffer));
    vkCmdCopyBufferToImage(command_buffer,
                           staging_buffer.buffer,
                           free_texture_image->image,
                           VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
                           1,
                           &copy_region);
    end_single_time_comands(context, command_buffer);
    destroy_buffer(context, &staging_buffer);

    transition_image_layout(context,
                            free_texture_image,
                            VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
                            VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
    STATUS_CHECK(create_image_view(context, free_texture_image, format, VK_IMAGE_ASPECT_COLOR_BIT));

    free_texture_image->id_taken = IMAGE_TAKEN;
    return RSE_ERROR_NO_ERROR;
}

rse_err_t init_vulkan_images(struct graphics_context_t* context)
{
    rse_err_t status = RSE_ERROR_NO_ERROR;

    STATUS_CHECK(create_depth_resources(context));
    STATUS_CHECK(create_color_resource(context));

    return status;
}

rse_err_t texture_update_image(struct graphics_context_t* context,
                               const unsigned char* pixels,
                               int width,
                               int height,
                               rse_id_t texture_id,
                               VkFormat format)
{
    rse_err_t status = RSE_ERROR_NO_ERROR;
    VkDeviceSize staging_buffer_size = 0U;
    VkBufferImageCopy copy_region = {0};
    struct vulkan_buffer_t staging_buffer = {0};
    VkExtent3D image_extent = {0};
    VkCommandBuffer command_buffer = {0};

    image_extent.width = width;
    image_extent.height = height;
    image_extent.depth = 1U;

    STATUS_CHECK(format_to_pixel_size(format, &staging_buffer_size));

    staging_buffer_size = staging_buffer_size * width * height;

    transition_image_layout(context,
                            &context->texture_images[texture_id],
                            VK_IMAGE_LAYOUT_UNDEFINED,
                            VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);

    /* Copy pixel data to GPU memory */
    STATUS_CHECK(
        create_buffer(context,
                      staging_buffer_size,
                      VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
                      VMA_MEMORY_USAGE_AUTO_PREFER_HOST,
                      VMA_ALLOCATION_CREATE_MAPPED_BIT | VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT,
                      &staging_buffer));

    memcpy(staging_buffer.allocation_info.pMappedData, pixels, staging_buffer_size);

    copy_region.bufferOffset = 0U;
    copy_region.bufferRowLength = 0U;
    copy_region.bufferImageHeight = 0U;
    copy_region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
    copy_region.imageSubresource.mipLevel = 0U;
    copy_region.imageSubresource.baseArrayLayer = 0U;
    copy_region.imageSubresource.layerCount = 1U;
    copy_region.imageOffset.x = 0;
    copy_region.imageOffset.y = 0;
    copy_region.imageOffset.z = 0;
    copy_region.imageExtent = image_extent;

    STATUS_CHECK(begin_single_time_command(context, &command_buffer));
    vkCmdCopyBufferToImage(command_buffer,
                           staging_buffer.buffer,
                           context->texture_images[texture_id].image,
                           VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
                           1,
                           &copy_region);
    end_single_time_comands(context, command_buffer);
    destroy_buffer(context, &staging_buffer);

    transition_image_layout(context,
                            &context->texture_images[texture_id],
                            VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
                            VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);

    return RSE_ERROR_NO_ERROR;
}

rse_err_t load_texture_from_bitmat(struct graphics_context_t* context,
                                   const unsigned char* pixel_buffer,
                                   int width,
                                   int height,
                                   rse_id_t* texture_id,
                                   VkFormat color_format)
{
    rse_err_t status = RSE_ERROR_NO_ERROR;

    /* Create actual Vulkan Image */
    STATUS_CHECK(create_textured_image(context, width, height, color_format, pixel_buffer, texture_id));

    return status;
}

rse_err_t texture_load_from_file(struct graphics_context_t* context, const char* file_path, rse_id_t* texture_id)
{
    rse_err_t status = RSE_ERROR_NO_ERROR;
    int width = 0;
    int height = 0;
    size_t buffer_size = 0U;
    unsigned char* pixel_buffer = NULL;

    STATUS_CHECK(file_load_pixels(file_path, NULL, &buffer_size, &width, &height));
    rse_malloc(pixel_buffer, buffer_size);
    if (file_load_pixels(file_path, pixel_buffer, &buffer_size, &width, &height) != RSE_ERROR_NO_ERROR) {
        status = RSE_ERROR_INTERNAL_ERROR;

        goto mem_free;
    }

    if (load_texture_from_bitmat(context, pixel_buffer, width, height, texture_id, VK_FORMAT_R8G8B8A8_SRGB) != RSE_ERROR_NO_ERROR) {
        status = RSE_ERROR_INTERNAL_ERROR;

        goto mem_free;
    }

mem_free:

    rse_free(pixel_buffer);

    return status;
}

void destroy_textures(struct graphics_context_t* context)
{
    size_t i = 0U;

    for (i = 0U; i < RSE_MAX_IMAGE_COUNT; ++i) {
        if (context->texture_images[i].id_taken == IMAGE_TAKEN) {
            vkDestroyImageView(context->vulkan_handles.device, context->texture_images[i].image_view, NULL);
            vmaDestroyImage(context->vulkan_handles.allocator,
                            context->texture_images[i].image,
                            context->texture_images[i].allocation);
        }
    }

    vkDestroySampler(context->vulkan_handles.device, context->vulkan_handles.sampler, NULL);
    vkDestroySampler(context->vulkan_handles.device, context->debug_overlay.sampler, NULL);
}

void destroy_depth_resource(struct graphics_context_t* context)
{
    vkDestroyImageView(context->vulkan_handles.device, context->depth_image.image_view, NULL);
    vmaDestroyImage(context->vulkan_handles.allocator, context->depth_image.image, context->depth_image.allocation);
}

void destroy_color_resource(struct graphics_context_t* context)
{
    vkDestroyImageView(context->vulkan_handles.device, context->color_image.image_view, NULL);
    vmaDestroyImage(context->vulkan_handles.allocator, context->color_image.image, context->color_image.allocation);
}

uint8_t image_exists(struct graphics_context_t* context, rse_id_t image_id)
{
    return context->texture_images[image_id].id_taken == IMAGE_TAKEN;
}

size_t get_textures_count(struct graphics_context_t* context)
{
    size_t count = 0U;
    size_t i = 0U;

    for (i = 0U; i < RSE_MAX_IMAGE_COUNT; ++i) {
        if (context->texture_images[i].id_taken == IMAGE_TAKEN) {
            count++;
        }
    }

    return count;
}

rse_err_t find_depth_format(struct graphics_context_t* context, VkFormat* found_format)
{
#define formats_count 3
    rse_err_t status;
    VkFormat formats[formats_count] = {VK_FORMAT_D32_SFLOAT};

    STATUS_CHECK(find_supported_format(context,
                                       formats,
                                       formats_count,
                                       VK_IMAGE_TILING_OPTIMAL,
                                       VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT,
                                       found_format));

    return status;
#undef formats_count
}
