#include <vulkan/vulkan.h>

#include "stb/stb_image.h"
#include "vulkan/graphics.h"

namespace veng {

void Graphics::CreateTextureSampler() {
  VkSamplerCreateInfo sampler_info = {};
  sampler_info.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
  sampler_info.magFilter = VK_FILTER_LINEAR;
  sampler_info.minFilter = VK_FILTER_LINEAR;
  sampler_info.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
  sampler_info.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
  sampler_info.addressModeW = VK_SAMPLER_ADDRESS_MODE_REPEAT;
  sampler_info.anisotropyEnable = VK_FALSE;
  sampler_info.maxAnisotropy = 1.f;
  sampler_info.borderColor = VK_BORDER_COLOR_FLOAT_OPAQUE_BLACK;
  sampler_info.unnormalizedCoordinates = VK_FALSE;
  sampler_info.compareEnable = VK_FALSE;
  sampler_info.compareOp = VK_COMPARE_OP_ALWAYS;
  sampler_info.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR;
  sampler_info.mipLodBias = 0.f;
  sampler_info.minLod = 0.f;
  sampler_info.maxLod = 0.f;

  if (vkCreateSampler(logical_device_, &sampler_info, nullptr,
                      &texture_sampler_) != VK_SUCCESS) {
    std::exit(EXIT_FAILURE);
  }
}

TextureHandle Graphics::CreateTexture(gsl::czstring path) {
  glm::ivec2 image_extents;
  std::int32_t channels;
  std::vector<std::uint8_t> image_file_data = ReadFile(path);
  stbi_uc* pixel_data = stbi_load_from_memory(
      image_file_data.data(), image_file_data.size(), &image_extents.x,
      &image_extents.y, &channels, STBI_rgb_alpha);

  VkDeviceSize buffer_size = image_extents.x * image_extents.y * 4;
  BufferHandle staging =
      CreateBuffer(buffer_size, VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
                   VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT |
                       VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
  void* data_location;
  vkMapMemory(logical_device_, staging.memory, 0, buffer_size, 0,
              &data_location);
  std::memcpy(data_location, pixel_data, buffer_size);
  vkUnmapMemory(logical_device_, staging.memory);

  stbi_image_free(pixel_data);

  TextureHandle handle =
      CreateImage(image_extents, VK_FORMAT_R8G8B8A8_SRGB,
                  VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT,
                  VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);

  TransitionImageLayout(handle.image, VK_IMAGE_LAYOUT_UNDEFINED,
                        VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);
  CopyBufferToImage(staging.buffer, handle.image, image_extents);
  TransitionImageLayout(handle.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
                        VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);

  handle.image_view = CreateImageView(handle.image, VK_FORMAT_R8G8B8A8_SRGB,
                                      VK_IMAGE_ASPECT_COLOR_BIT);

  VkDescriptorSetAllocateInfo set_info = {};
  set_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
  set_info.descriptorPool = texture_pool_;
  set_info.descriptorSetCount = 1;
  set_info.pSetLayouts = &texture_set_layout_;

  VkResult result =
      vkAllocateDescriptorSets(logical_device_, &set_info, &handle.set);
  if (result != VK_SUCCESS) std::exit(EXIT_FAILURE);

  VkDescriptorImageInfo image_info = {};
  image_info.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
  image_info.imageView = handle.image_view;
  image_info.sampler = texture_sampler_;

  VkWriteDescriptorSet descriptor_write = {};
  descriptor_write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
  descriptor_write.dstSet = handle.set;
  descriptor_write.dstBinding = 0;
  descriptor_write.dstArrayElement = 0;
  descriptor_write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
  descriptor_write.descriptorCount = 1;
  descriptor_write.pImageInfo = &image_info;

  vkUpdateDescriptorSets(logical_device_, 1, &descriptor_write, 0, nullptr);

  DestroyBuffer(staging);
  return handle;
}

void Graphics::DestroyTexture(TextureHandle handle) {
  vkDeviceWaitIdle(logical_device_);

  if (handle.set != VK_NULL_HANDLE)
    vkFreeDescriptorSets(logical_device_, texture_pool_, 1, &handle.set);

  if (handle.image_view != VK_NULL_HANDLE)
    vkDestroyImageView(logical_device_, handle.image_view, nullptr);

  if (handle.image != VK_NULL_HANDLE)
    vkDestroyImage(logical_device_, handle.image, nullptr);

  if (handle.memory != VK_NULL_HANDLE)
    vkFreeMemory(logical_device_, handle.memory, nullptr);
}

void Graphics::SetTexture(TextureHandle handle) {
  vkCmdBindDescriptorSets(frames_[current_frame_].command_buffer,
                          VK_PIPELINE_BIND_POINT_GRAPHICS,
                          pipeline_layout_, 1, 1, &handle.set, 0, nullptr);
}

void Graphics::TransitionImageLayout(VkImage image, VkImageLayout old_layout,
                                     VkImageLayout new_layout) {
  VkCommandBuffer local_command_buffer = BeginTransientCommandBuffer();

  VkImageMemoryBarrier barrier = {};
  barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
  barrier.oldLayout = old_layout;
  barrier.newLayout = new_layout;
  barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
  barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
  barrier.image = image;
  barrier.subresourceRange.baseArrayLayer = 0;
  barrier.subresourceRange.baseMipLevel = 0;
  barrier.subresourceRange.levelCount = 1;
  barrier.subresourceRange.layerCount = 1;

  VkPipelineStageFlags source_stage = {};
  VkPipelineStageFlags destination_stage = {};

  if (old_layout == VK_IMAGE_LAYOUT_UNDEFINED &&
      new_layout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL) {
    barrier.srcAccessMask = 0;
    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) {
    barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
    barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
    source_stage = VK_PIPELINE_STAGE_TRANSFER_BIT;
    destination_stage = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
  } else if (old_layout == VK_IMAGE_LAYOUT_UNDEFINED &&
             new_layout == VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL) {
    barrier.srcAccessMask = 0;
    barrier.dstAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT |
                            VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
    source_stage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
    destination_stage = VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT;
  }

  if (new_layout == VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL)
    barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT;
  else
    barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;

  vkCmdPipelineBarrier(local_command_buffer, source_stage, destination_stage, 0,
                       0, nullptr, 0, nullptr, 1, &barrier);

  EndTransientCommandBuffer(local_command_buffer);
}

void Graphics::CopyBufferToImage(VkBuffer buffer, VkImage image,
                                 glm::ivec2 image_size) {
  VkCommandBuffer local_command_buffer = BeginTransientCommandBuffer();

  VkBufferImageCopy region = {};
  region.bufferOffset = 0;
  region.bufferRowLength = 0;
  region.bufferImageHeight = 0;
  region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
  region.imageSubresource.mipLevel = 0;
  region.imageSubresource.baseArrayLayer = 0;
  region.imageSubresource.layerCount = 1;
  region.imageOffset = {0, 0, 0};
  region.imageExtent = {static_cast<std::uint32_t>(image_size.x),
                        static_cast<std::uint32_t>(image_size.y), 1};

  vkCmdCopyBufferToImage(local_command_buffer, buffer, image,
                         VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &region);

  EndTransientCommandBuffer(local_command_buffer);
}

TextureHandle Graphics::CreateImage(glm::ivec2 size, VkFormat image_format,
                                    VkBufferUsageFlags usage,
                                    VkMemoryPropertyFlags properties) {
  TextureHandle handle = {};

  VkImageCreateInfo image_info = {};
  image_info.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
  image_info.usage = usage;
  image_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
  image_info.imageType = VK_IMAGE_TYPE_2D;
  image_info.extent.width = size.x;
  image_info.extent.height = size.y;
  image_info.extent.depth = 1;
  image_info.mipLevels = 1;
  image_info.arrayLayers = 1;
  image_info.format = image_format;
  image_info.tiling = VK_IMAGE_TILING_OPTIMAL;
  image_info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
  image_info.samples = VK_SAMPLE_COUNT_1_BIT;
  image_info.flags = 0;

  VkResult result =
      vkCreateImage(logical_device_, &image_info, nullptr, &handle.image);
  if (result != VK_SUCCESS)
    throw std::runtime_error("Failed to create vertex buffer!");

  VkMemoryRequirements memory_requirements;
  vkGetImageMemoryRequirements(logical_device_, handle.image,
                               &memory_requirements);

  std::uint32_t chosen_memory_type =
      FindMemoryType(memory_requirements.memoryTypeBits, properties);

  VkMemoryAllocateInfo allocation_info = {};
  allocation_info.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
  allocation_info.allocationSize = memory_requirements.size;
  allocation_info.memoryTypeIndex = chosen_memory_type;

  VkResult allocation_result = vkAllocateMemory(
      logical_device_, &allocation_info, nullptr, &handle.memory);
  if (allocation_result != VK_SUCCESS)
    throw std::runtime_error("Failed to allocate image memory!");

  vkBindImageMemory(logical_device_, handle.image, handle.memory, 0);

  return handle;
}

void Graphics::CreateDepthResources() {
  VkFormat kDepthFormat = VK_FORMAT_D32_SFLOAT;
  depth_texture_ = CreateImage({extent_.width, extent_.height}, kDepthFormat,
                               VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT,
                               VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
  depth_texture_.image_view = CreateImageView(
      depth_texture_.image, kDepthFormat, VK_IMAGE_ASPECT_DEPTH_BIT);
}

}  // namespace veng
