/******************************************************************************
 * The MIT License (MIT)
 *
 * Copyright (c) 2019-2024 Baldur Karlsson
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 ******************************************************************************/

#include "vk_test.h"

RD_TEST(VK_Resource_Lifetimes, VulkanGraphicsTest)
{
  static constexpr const char *Description =
      "Test various edge-case resource lifetimes: a resource that is first dirtied within a frame "
      "so needs initial contents created for it, and a resource that is created and destroyed "
      "mid-frame (which also gets dirtied after use).";

  std::string common = R"EOSHADER(

#version 420 core

struct v2f
{
	vec4 pos;
	vec4 col;
	vec4 uv;
};

)EOSHADER";

  const std::string vertex = R"EOSHADER(

layout(location = 0) in vec3 Position;
layout(location = 1) in vec4 Color;
layout(location = 2) in vec2 UV;

layout(location = 0) out v2f vertOut;

void main()
{
	vertOut.pos = vec4(Position.xyz*vec3(1,-1,1), 1);
	gl_Position = vertOut.pos;
	vertOut.col = Color;
	vertOut.uv = vec4(UV.xy, 0, 1);
}

)EOSHADER";

  const std::string pixel = R"EOSHADER(

layout(location = 0) in v2f vertIn;

layout(location = 0, index = 0) out vec4 Color;

layout(binding = 0) uniform sampler2D smiley;
layout(binding = 1) uniform sampler2D checker;

layout(binding = 2, std140) uniform constsbuf
{
  vec4 flags;
};

void main()
{
  if(flags.x != 1.0f || flags.y != 2.0f || flags.z != 4.0f || flags.w != 8.0f)
  {
    Color = vec4(1.0f, 0.0f, 1.0f, 1.0f);
    return;
  }

	Color = texture(smiley, vertIn.uv.xy * 2.0f) * texture(checker, vertIn.uv.xy * 5.0f);
  Color.w = 1.0f;
}

)EOSHADER";

  int main()
  {
    // initialise, create window, create context, etc
    if(!Init())
      return 3;

    VkDescriptorSetLayout setlayout = createDescriptorSetLayout(vkh::DescriptorSetLayoutCreateInfo({
        {
            0,
            VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
            1,
            VK_SHADER_STAGE_FRAGMENT_BIT,
        },
        {
            1,
            VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
            1,
            VK_SHADER_STAGE_FRAGMENT_BIT,
        },
        {
            2,
            VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER,
            1,
            VK_SHADER_STAGE_FRAGMENT_BIT,
        },
    }));

    VkPipelineLayout layout = createPipelineLayout(vkh::PipelineLayoutCreateInfo({setlayout}));

    vkh::GraphicsPipelineCreateInfo pipeCreateInfo;

    pipeCreateInfo.layout = layout;
    pipeCreateInfo.renderPass = mainWindow->rp;

    pipeCreateInfo.vertexInputState.vertexBindingDescriptions = {vkh::vertexBind(0, DefaultA2V)};
    pipeCreateInfo.vertexInputState.vertexAttributeDescriptions = {
        vkh::vertexAttr(0, 0, DefaultA2V, pos),
        vkh::vertexAttr(1, 0, DefaultA2V, col),
        vkh::vertexAttr(2, 0, DefaultA2V, uv),
    };

    pipeCreateInfo.stages = {
        CompileShaderModule(common + vertex, ShaderLang::glsl, ShaderStage::vert, "main"),
        CompileShaderModule(common + pixel, ShaderLang::glsl, ShaderStage::frag, "main"),
    };

    VkPipeline pipe = createGraphicsPipeline(pipeCreateInfo);

    AllocatedBuffer vb(
        this,
        vkh::BufferCreateInfo(sizeof(DefaultTri),
                              VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT),
        VmaAllocationCreateInfo({0, VMA_MEMORY_USAGE_CPU_TO_GPU}));

    vb.upload(DefaultTri);

    Texture rgba8;
    LoadXPM(SmileyTexture, rgba8);

    AllocatedImage smiley(
        this,
        vkh::ImageCreateInfo(rgba8.width, rgba8.height, 0, VK_FORMAT_R8G8B8A8_UNORM,
                             VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT),
        VmaAllocationCreateInfo({0, VMA_MEMORY_USAGE_GPU_ONLY}));

    VkImageView smileyview = createImageView(
        vkh::ImageViewCreateInfo(smiley.image, VK_IMAGE_VIEW_TYPE_2D, VK_FORMAT_R8G8B8A8_UNORM));

    AllocatedImage badimg(
        this,
        vkh::ImageCreateInfo(4, 4, 0, VK_FORMAT_R8G8B8A8_UNORM,
                             VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT),
        VmaAllocationCreateInfo({0, VMA_MEMORY_USAGE_GPU_ONLY}));

    VkImageView badview = createImageView(
        vkh::ImageViewCreateInfo(badimg.image, VK_IMAGE_VIEW_TYPE_2D, VK_FORMAT_R8G8B8A8_UNORM));

    AllocatedBuffer uploadBuf(this,
                              vkh::BufferCreateInfo(rgba8.data.size() * sizeof(uint32_t),
                                                    VK_BUFFER_USAGE_TRANSFER_SRC_BIT),
                              VmaAllocationCreateInfo({0, VMA_MEMORY_USAGE_CPU_TO_GPU}));

    uploadBuf.upload(rgba8.data.data(), rgba8.data.size() * sizeof(uint32_t));

    uploadBufferToImage(smiley.image, {rgba8.width, rgba8.height, 1}, uploadBuf.buffer,
                        VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);

    {
      VkCommandBuffer cmd = GetCommandBuffer();

      vkBeginCommandBuffer(cmd, vkh::CommandBufferBeginInfo());

      vkh::cmdPipelineBarrier(
          cmd, {
                   vkh::ImageMemoryBarrier(0, VK_ACCESS_SHADER_READ_BIT, VK_IMAGE_LAYOUT_UNDEFINED,
                                           VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, badimg.image),
               });

      vkEndCommandBuffer(cmd);

      Submit(99, 99, {cmd});
    }

    Vec4f flags = {};
    AllocatedBuffer badcb(this,
                          vkh::BufferCreateInfo(sizeof(flags), VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT |
                                                                   VK_BUFFER_USAGE_TRANSFER_DST_BIT),
                          VmaAllocationCreateInfo({0, VMA_MEMORY_USAGE_CPU_TO_GPU}));

    badcb.upload(&flags, sizeof(flags));

    VkSampler checkersampler = createSampler(vkh::SamplerCreateInfo(VK_FILTER_NEAREST));
    VkSampler smileysampler = createSampler(
        vkh::SamplerCreateInfo(VK_FILTER_NEAREST, VK_FILTER_NEAREST, VK_SAMPLER_MIPMAP_MODE_LINEAR,
                               VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE));

    auto SetupBuffer = [this]() {
      VkBuffer cb = VK_NULL_HANDLE;

      vkCreateBuffer(device,
                     vkh::BufferCreateInfo(sizeof(Vec4f), VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT |
                                                              VK_BUFFER_USAGE_TRANSFER_DST_BIT),
                     NULL, &cb);

      return cb;
    };

    const VkPhysicalDeviceMemoryProperties *props = NULL;
    vmaGetMemoryProperties(allocator, &props);

    auto SetupBufferMemory = [this, props](VkBuffer cb) {
      VkMemoryRequirements mrq;
      vkGetBufferMemoryRequirements(device, cb, &mrq);

      VkMemoryAllocateInfo info = {};
      info.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
      info.allocationSize = mrq.size;

      for(uint32_t i = 0; i < props->memoryTypeCount; i++)
      {
        if((mrq.memoryTypeBits & (1u << i)) &&
           (props->memoryTypes[i].propertyFlags & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT))
        {
          info.memoryTypeIndex = i;
          break;
        }
      }

      VkDeviceMemory mem = VK_NULL_HANDLE;
      vkAllocateMemory(device, &info, NULL, &mem);
      vkBindBufferMemory(device, cb, mem, 0);

      Vec4f *data = NULL;
      vkMapMemory(device, mem, 0, sizeof(Vec4f), 0, (void **)&data);
      {
        *data = Vec4f(1.0f, 2.0f, 4.0f, 8.0f);
      }
      vkUnmapMemory(device, mem);

      return mem;
    };

    auto TrashBuffer = [this](VkBuffer cb, VkDeviceMemory mem) {
      Vec4f *data = NULL;
      vkMapMemory(device, mem, 0, sizeof(Vec4f), 0, (void **)&data);
      {
        *data = Vec4f();
      }
      vkUnmapMemory(device, mem);

      vkDestroyBuffer(device, cb, NULL);
      vkFreeMemory(device, mem, NULL);
    };

    auto SetupImage = [this]() {
      VkImage img = VK_NULL_HANDLE;

      vkCreateImage(
          device,
          vkh::ImageCreateInfo(4, 4, 0, VK_FORMAT_R8G8B8A8_UNORM,
                               VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT),
          NULL, &img);

      return img;
    };

    auto SetupImageMemory = [this, props, &uploadBuf](VkImage img) {
      VkMemoryRequirements mrq;
      vkGetImageMemoryRequirements(device, img, &mrq);

      VkMemoryAllocateInfo info = {};
      info.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
      info.allocationSize = mrq.size;

      for(uint32_t i = 0; i < props->memoryTypeCount; i++)
      {
        if(mrq.memoryTypeBits & (1 << i))
        {
          info.memoryTypeIndex = i;
          break;
        }
      }

      VkDeviceMemory mem = VK_NULL_HANDLE;
      vkAllocateMemory(device, &info, NULL, &mem);
      vkBindImageMemory(device, img, mem, 0);

      const uint32_t checker[4 * 4] = {
          // X X O O
          0xffffffff,
          0xffffffff,
          0,
          0,
          // X X O O
          0xffffffff,
          0xffffffff,
          0,
          0,
          // O O X X
          0,
          0,
          0xffffffff,
          0xffffffff,
          // O O X X
          0,
          0,
          0xffffffff,
          0xffffffff,
      };

      uploadBuf.upload(checker);

      uploadBufferToImage(img, {4, 4, 1}, uploadBuf.buffer, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);

      return mem;
    };

    auto SetupImageView = [this](VkImage img) {
      VkImageView ret;
      vkCreateImageView(
          device, vkh::ImageViewCreateInfo(img, VK_IMAGE_VIEW_TYPE_2D, VK_FORMAT_R8G8B8A8_UNORM),
          NULL, &ret);
      return ret;
    };

    auto TrashImage = [this](VkImage img, VkDeviceMemory mem, VkImageView view) {
      vkDestroyImageView(device, view, NULL);
      vkDestroyImage(device, img, NULL);
      vkFreeMemory(device, mem, NULL);
    };

    VkDescriptorPool descpool = VK_NULL_HANDLE;

    {
      CHECK_VKR(vkCreateDescriptorPool(
          device,
          vkh::DescriptorPoolCreateInfo(8,
                                        {
                                            {VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1024},
                                            {VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 1024},
                                        },
                                        VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT),
          NULL, &descpool));
    }

    auto SetupDescSet = [this, setlayout, descpool, smileysampler, checkersampler, smileyview](
                            VkBuffer cb, VkImageView view) {
      VkDescriptorSet descset = VK_NULL_HANDLE;

      vkAllocateDescriptorSets(device, vkh::DescriptorSetAllocateInfo(descpool, {setlayout}),
                               &descset);

      vkh::updateDescriptorSets(
          device,
          {
              vkh::WriteDescriptorSet(
                  descset, 0, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
                  {
                      vkh::DescriptorImageInfo(smileyview, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
                                               smileysampler),
                  }),
              vkh::WriteDescriptorSet(
                  descset, 1, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
                  {
                      vkh::DescriptorImageInfo(view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
                                               checkersampler),
                  }),
              vkh::WriteDescriptorSet(descset, 2, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER,
                                      {vkh::DescriptorBufferInfo(cb)}),
          });

      return descset;
    };

    auto TrashDescSet = [this, descpool, checkersampler, &badcb, badview](VkDescriptorSet descset) {
      // update with bad data
      vkh::updateDescriptorSets(
          device, {
                      vkh::WriteDescriptorSet(
                          descset, 0, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
                          {
                              vkh::DescriptorImageInfo(
                                  badview, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, checkersampler),
                          }),
                      vkh::WriteDescriptorSet(
                          descset, 1, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
                          {
                              vkh::DescriptorImageInfo(
                                  badview, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, checkersampler),
                          }),
                      vkh::WriteDescriptorSet(descset, 2, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER,
                                              {vkh::DescriptorBufferInfo(badcb.buffer)}),
                  });

      // assume we are able to re-use the same descriptor pool indefinitely since we only allocate
      // one set at a time.
      vkFreeDescriptorSets(device, descpool, 1, &descset);
    };

    VkBuffer cb = SetupBuffer();
    VkDeviceMemory cbmem = SetupBufferMemory(cb);
    VkImage img = SetupImage();
    VkDeviceMemory imgmem = SetupImageMemory(img);
    VkImageView imgview = SetupImageView(img);
    VkDescriptorSet descset = SetupDescSet(cb, imgview);
    while(Running())
    {
      // acquire and clear the backbuffer
      {
        VkCommandBuffer cmd = GetCommandBuffer();

        vkBeginCommandBuffer(cmd, vkh::CommandBufferBeginInfo());

        VkImage swapimg =
            StartUsingBackbuffer(cmd, VK_ACCESS_TRANSFER_WRITE_BIT, VK_IMAGE_LAYOUT_GENERAL);

        vkCmdClearColorImage(cmd, swapimg, VK_IMAGE_LAYOUT_GENERAL,
                             vkh::ClearColorValue(0.2f, 0.2f, 0.2f, 1.0f), 1,
                             vkh::ImageSubresourceRange());

        vkEndCommandBuffer(cmd);

        Submit(0, 4, {cmd});
      }

      // render with last frame's resources then trash them
      {
        VkCommandBuffer cmd = GetCommandBuffer();

        vkBeginCommandBuffer(cmd, vkh::CommandBufferBeginInfo());

        vkCmdBeginRenderPass(
            cmd, vkh::RenderPassBeginInfo(mainWindow->rp, mainWindow->GetFB(), mainWindow->scissor),
            VK_SUBPASS_CONTENTS_INLINE);

        VkClearAttachment att = {VK_IMAGE_ASPECT_COLOR_BIT, 0,
                                 vkh::ClearValue(0.0f, 1.0f, 0.0f, 1.0f)};
        VkClearRect rect = {vkh::Rect2D({0, 0}, {128, 128}), 0, 1};
        vkCmdClearAttachments(cmd, 1, &att, 1, &rect);

        vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipe);
        vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, layout, 0, 1, &descset, 0,
                                NULL);
        VkViewport view = {0, 0, 128, 128, 0, 1};
        vkCmdSetViewport(cmd, 0, 1, &view);
        vkCmdSetScissor(cmd, 0, 1, &mainWindow->scissor);
        vkh::cmdBindVertexBuffers(cmd, 0, {vb.buffer}, {0});
        vkCmdDraw(cmd, 3, 1, 0, 0);

        vkCmdEndRenderPass(cmd);

        vkEndCommandBuffer(cmd);

        Submit(1, 4, {cmd});

        vkDeviceWaitIdle(device);

        TrashBuffer(cb, cbmem);
        TrashImage(img, imgmem, imgview);
        TrashDescSet(descset);
      }

      // create resources mid-frame and use then trash them
      {
        cb = SetupBuffer();
        cbmem = SetupBufferMemory(cb);
        img = SetupImage();
        imgmem = SetupImageMemory(img);
        imgview = SetupImageView(img);
        descset = SetupDescSet(cb, imgview);

        vkDeviceWaitIdle(device);

        VkCommandBuffer cmd = GetCommandBuffer();

        vkBeginCommandBuffer(cmd, vkh::CommandBufferBeginInfo());

        vkCmdBeginRenderPass(
            cmd, vkh::RenderPassBeginInfo(mainWindow->rp, mainWindow->GetFB(), mainWindow->scissor),
            VK_SUBPASS_CONTENTS_INLINE);

        VkClearAttachment att = {VK_IMAGE_ASPECT_COLOR_BIT, 0,
                                 vkh::ClearValue(0.0f, 0.0f, 1.0f, 1.0f)};
        VkClearRect rect = {vkh::Rect2D({128, 0}, {128, 128}), 0, 1};
        vkCmdClearAttachments(cmd, 1, &att, 1, &rect);

        vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipe);
        vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, layout, 0, 1, &descset, 0,
                                NULL);
        VkViewport view = {128, 0, 128, 128, 0, 1};
        vkCmdSetViewport(cmd, 0, 1, &view);
        vkCmdSetScissor(cmd, 0, 1, &mainWindow->scissor);
        vkh::cmdBindVertexBuffers(cmd, 0, {vb.buffer}, {0});
        vkCmdDraw(cmd, 3, 1, 0, 0);

        vkCmdEndRenderPass(cmd);

        vkEndCommandBuffer(cmd);

        Submit(2, 4, {cmd});

        vkDeviceWaitIdle(device);

        TrashBuffer(cb, cbmem);
        TrashImage(img, imgmem, imgview);
        TrashDescSet(descset);
      }

      // finish with the backbuffer
      {
        VkCommandBuffer cmd = GetCommandBuffer();

        vkBeginCommandBuffer(cmd, vkh::CommandBufferBeginInfo());

        FinishUsingBackbuffer(cmd, VK_ACCESS_TRANSFER_WRITE_BIT, VK_IMAGE_LAYOUT_GENERAL);

        vkEndCommandBuffer(cmd);

        Submit(3, 4, {cmd});
      }

      // set up resources for next frame
      cb = SetupBuffer();
      cbmem = SetupBufferMemory(cb);
      img = SetupImage();
      imgmem = SetupImageMemory(img);
      imgview = SetupImageView(img);
      descset = SetupDescSet(cb, imgview);

      Present();
    }

    vkDeviceWaitIdle(device);

    // destroy resources
    TrashBuffer(cb, cbmem);
    TrashImage(img, imgmem, imgview);
    TrashDescSet(descset);

    vkDestroyDescriptorPool(device, descpool, NULL);

    return 0;
  }
};

REGISTER_TEST();
