#pragma once

#include "rx/MemoryTable.hpp"
#include "rx/die.hpp"

#include <cstddef>
#include <cstdint>
#include <cstdlib>
#include <mutex>
#include <span>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
#include <vulkan/vulkan.h>
#include <vulkan/vulkan_core.h>

#define VK_VERIFY(...)                                                         \
  if (VkResult _ = (__VA_ARGS__); _ != VK_SUCCESS) {                           \
    ::vk::verifyFailed(_, #__VA_ARGS__);                                       \
  }

inline constexpr auto operator<=>(const VkExtent3D &lhs,
                                  const VkExtent3D &rhs) {
  if (auto res = lhs.width <=> rhs.width; res != 0) {
    return res;
  }
  if (auto res = lhs.height <=> rhs.height; res != 0) {
    return res;
  }
  return lhs.depth <=> rhs.depth;
}

inline constexpr bool operator==(const VkExtent3D &lhs, const VkExtent3D &rhs) {
  return lhs.width == rhs.width && lhs.height == rhs.height &&
         lhs.depth == rhs.depth;
}

namespace vk {
void verifyFailed(VkResult result, const char *message);

struct Context {
  VkInstance instance = VK_NULL_HANDLE;
  VkPhysicalDeviceMemoryProperties physicalMemoryProperties;
  VkAllocationCallbacks *allocator = nullptr;
  VkPhysicalDevice physicalDevice = VK_NULL_HANDLE;
  VkDevice device = VK_NULL_HANDLE;
  std::vector<std::string> deviceExtensions;
  std::vector<std::pair<VkQueue, unsigned>> computeQueues;
  std::vector<std::pair<VkQueue, unsigned>> graphicsQueues;
  VkQueue presentQueue = VK_NULL_HANDLE;
  unsigned presentQueueFamily{};

  VkSwapchainKHR swapchain = VK_NULL_HANDLE;
  VkSurfaceKHR surface = VK_NULL_HANDLE;
  VkExtent2D swapchainExtent{};
  std::vector<VkImage> swapchainImages;
  VkFormat swapchainColorFormat = VK_FORMAT_B8G8R8A8_UNORM;
  VkColorSpaceKHR swapchainColorSpace = VK_COLOR_SPACE_SRGB_NONLINEAR_KHR;
  std::vector<VkImageView> swapchainImageViews;
  VkSemaphore presentCompleteSemaphore = VK_NULL_HANDLE;
  VkSemaphore renderCompleteSemaphore = VK_NULL_HANDLE;
  VkPhysicalDeviceDescriptorBufferPropertiesEXT descriptorBufferProps;

  bool supportsBarycentric = false;
  bool supportsInt8 = false;
  bool supportsInt64Atomics = false;
  bool supportsNonSemanticInfo = false;

  Context() = default;
  Context(const Context &) = delete;
  Context(Context &&other) noexcept { other.swap(*this); }
  Context &operator=(Context &&other) noexcept {
    other.swap(*this);
    return *this;
  }

  ~Context() {
    if (instance == VK_NULL_HANDLE) {
      return;
    }

    for (auto imageView : swapchainImageViews) {
      vkDestroyImageView(device, imageView, allocator);
    }

    if (swapchain != VK_NULL_HANDLE) {
      vkDestroySwapchainKHR(device, swapchain, allocator);
    }

    if (presentCompleteSemaphore != VK_NULL_HANDLE) {
      vkDestroySemaphore(device, presentCompleteSemaphore, allocator);
    }

    if (renderCompleteSemaphore != VK_NULL_HANDLE) {
      vkDestroySemaphore(device, renderCompleteSemaphore, allocator);
    }

    if (device != VK_NULL_HANDLE) {
      vkDestroyDevice(device, allocator);
    }

    if (surface != VK_NULL_HANDLE) {
      vkDestroySurfaceKHR(instance, surface, allocator);
    }

    if (instance != VK_NULL_HANDLE) {
      vkDestroyInstance(instance, allocator);
    }
  }

  void swap(Context &other) noexcept {
    std::swap(instance, other.instance);
    std::swap(physicalMemoryProperties, other.physicalMemoryProperties);
    std::swap(allocator, other.allocator);
    std::swap(device, other.device);
    std::swap(physicalDevice, other.physicalDevice);
    std::swap(deviceExtensions, other.deviceExtensions);
    std::swap(computeQueues, other.computeQueues);
    std::swap(graphicsQueues, other.graphicsQueues);
    std::swap(presentQueue, other.presentQueue);
    std::swap(presentQueueFamily, other.presentQueueFamily);

    std::swap(swapchain, other.swapchain);
    std::swap(swapchainExtent, other.swapchainExtent);
    std::swap(swapchainImages, other.swapchainImages);
    std::swap(swapchainColorFormat, other.swapchainColorFormat);
    std::swap(swapchainColorSpace, other.swapchainColorSpace);
    std::swap(swapchainImageViews, other.swapchainImageViews);
  }

  bool hasDeviceExtension(std::string_view ext);
  void createSwapchain();
  void recreateSwapchain();
  void createDevice(VkSurfaceKHR surface, int gpuIndex,
                    std::vector<const char *> requiredExtensions,
                    std::vector<const char *> optionalExtensions);

  static Context create(std::vector<const char *> requiredLayers,
                        std::vector<const char *> optionalLayers,
                        std::vector<const char *> requiredExtensions,
                        std::vector<const char *> optionalExtensions);

  std::uint32_t findPhysicalMemoryTypeIndex(std::uint32_t typeBits,
                                            VkMemoryPropertyFlags properties);
};

extern Context *context;

class DeviceMemory {
  VkDeviceMemory mDeviceMemory = VK_NULL_HANDLE;
  VkDeviceSize mSize = 0;
  unsigned mMemoryTypeIndex = 0;

public:
  DeviceMemory(DeviceMemory &) = delete;
  DeviceMemory(DeviceMemory &&other) noexcept { *this = std::move(other); }
  DeviceMemory() = default;

  ~DeviceMemory() {
    if (mDeviceMemory != nullptr) {
      vkFreeMemory(context->device, mDeviceMemory, context->allocator);
    }
    mDeviceMemory = nullptr;
  }

  DeviceMemory &operator=(DeviceMemory &&other) noexcept {
    std::swap(mDeviceMemory, other.mDeviceMemory);
    std::swap(mSize, other.mSize);
    std::swap(mMemoryTypeIndex, other.mMemoryTypeIndex);
    return *this;
  }

  [[nodiscard]] VkDeviceMemory getHandle() const { return mDeviceMemory; }
  [[nodiscard]] VkDeviceSize getSize() const { return mSize; }
  [[nodiscard]] unsigned getMemoryTypeIndex() const { return mMemoryTypeIndex; }

  static DeviceMemory AllocateFromType(std::size_t size,
                                       unsigned memoryTypeIndex) {
    VkMemoryAllocateFlagsInfo flags{
        .sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_FLAGS_INFO,
        .flags = VK_MEMORY_ALLOCATE_DEVICE_ADDRESS_BIT,
    };

    VkMemoryAllocateInfo allocInfo{
        .sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO,
        .pNext = &flags,
        .allocationSize = size,
        .memoryTypeIndex = memoryTypeIndex,
    };

    DeviceMemory result;
    VK_VERIFY(vkAllocateMemory(context->device, &allocInfo, context->allocator,
                               &result.mDeviceMemory));
    result.mSize = size;
    result.mMemoryTypeIndex = memoryTypeIndex;
    return result;
  }

  static DeviceMemory Allocate(std::size_t size, unsigned memoryTypeBits,
                               VkMemoryPropertyFlags properties) {
    return AllocateFromType(
        size, context->findPhysicalMemoryTypeIndex(memoryTypeBits, properties));
  }

  static DeviceMemory Allocate(VkMemoryRequirements requirements,
                               VkMemoryPropertyFlags properties) {
    return AllocateFromType(requirements.size,
                            context->findPhysicalMemoryTypeIndex(
                                requirements.memoryTypeBits, properties));
  }

  static DeviceMemory CreateExternalFd(int fd, std::size_t size,
                                       unsigned memoryTypeIndex) {
    VkImportMemoryFdInfoKHR importMemoryInfo{
        VK_STRUCTURE_TYPE_IMPORT_MEMORY_FD_INFO_KHR,
        nullptr,
        VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT,
        fd,
    };

    VkMemoryAllocateInfo allocInfo{
        .sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO,
        .pNext = &importMemoryInfo,
        .allocationSize = size,
        .memoryTypeIndex = memoryTypeIndex,
    };

    DeviceMemory result;
    VK_VERIFY(vkAllocateMemory(context->device, &allocInfo, context->allocator,
                               &result.mDeviceMemory));
    result.mSize = size;
    result.mMemoryTypeIndex = memoryTypeIndex;
    return result;
  }
  static DeviceMemory
  CreateExternalHostMemory(void *hostPointer, std::size_t size,
                           VkMemoryPropertyFlags properties) {
    VkMemoryHostPointerPropertiesEXT hostPointerProperties = {
        .sType = VK_STRUCTURE_TYPE_MEMORY_HOST_POINTER_PROPERTIES_EXT};

    auto vkGetMemoryHostPointerPropertiesEXT =
        (PFN_vkGetMemoryHostPointerPropertiesEXT)vkGetDeviceProcAddr(
            context->device, "vkGetMemoryHostPointerPropertiesEXT");

    VK_VERIFY(vkGetMemoryHostPointerPropertiesEXT(
        context->device, VK_EXTERNAL_MEMORY_HANDLE_TYPE_HOST_ALLOCATION_BIT_EXT,
        hostPointer, &hostPointerProperties));

    auto memoryTypeBits = hostPointerProperties.memoryTypeBits;

    VkImportMemoryHostPointerInfoEXT importMemoryInfo = {
        VK_STRUCTURE_TYPE_IMPORT_MEMORY_HOST_POINTER_INFO_EXT,
        nullptr,
        VK_EXTERNAL_MEMORY_HANDLE_TYPE_HOST_ALLOCATION_BIT_EXT,
        hostPointer,
    };

    auto memoryTypeIndex =
        context->findPhysicalMemoryTypeIndex(memoryTypeBits, properties);

    VkMemoryAllocateInfo allocInfo{
        .sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO,
        .pNext = &importMemoryInfo,
        .allocationSize = size,
        .memoryTypeIndex = memoryTypeIndex,
    };

    DeviceMemory result;
    VK_VERIFY(vkAllocateMemory(context->device, &allocInfo, context->allocator,
                               &result.mDeviceMemory));
    result.mSize = size;
    result.mMemoryTypeIndex = memoryTypeIndex;
    return result;
  }

  void *map(VkDeviceSize offset, VkDeviceSize size) {
    void *result = 0;
    VK_VERIFY(
        vkMapMemory(context->device, mDeviceMemory, offset, size, 0, &result));
    return result;
  }

  void unmap() { vkUnmapMemory(context->device, mDeviceMemory); }
};

struct DeviceMemoryRef {
  VkDeviceMemory deviceMemory = VK_NULL_HANDLE;
  VkDeviceSize offset = 0;
  VkDeviceSize size = 0;
  void *data = nullptr;
  void *allocator = nullptr;

  void (*release)(DeviceMemoryRef &memoryRef) = nullptr;
};

class MemoryResource {
  DeviceMemory mMemory;
  char *mData = nullptr;
  rx::MemoryAreaTable<> table;
  // const char *debugName = "<unknown>";

  std::mutex mMtx;

public:
  MemoryResource() = default;
  ~MemoryResource() { clear(); }

  void clear() {
    if (mMemory.getHandle() != nullptr && mData != nullptr) {
      vkUnmapMemory(context->device, mMemory.getHandle());
    }
  }

  void free() {
    clear();
    mMemory = {};
  }

  void initFromHost(void *data, std::size_t size) {
    assert(mMemory.getHandle() == nullptr);
    auto properties = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT |
                      VK_MEMORY_PROPERTY_HOST_COHERENT_BIT;
    mMemory = DeviceMemory::CreateExternalHostMemory(data, size, properties);
    table.map(0, size);
    // debugName = "direct";
  }

  void initHostVisible(std::size_t size) {
    assert(mMemory.getHandle() == nullptr);
    auto properties = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT |
                      VK_MEMORY_PROPERTY_HOST_COHERENT_BIT;
    auto memory = DeviceMemory::Allocate(size, ~0, properties);

    void *data = nullptr;
    VK_VERIFY(
        vkMapMemory(context->device, memory.getHandle(), 0, size, 0, &data));

    mMemory = std::move(memory);
    table.map(0, size);
    mData = reinterpret_cast<char *>(data);
    // debugName = "host";
  }

  void initDeviceLocal(std::size_t size) {
    assert(mMemory.getHandle() == nullptr);
    auto properties = VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT;

    mMemory = DeviceMemory::Allocate(size, ~0, properties);
    table.map(0, size);
    // debugName = "local";
  }

  DeviceMemoryRef allocate(VkMemoryRequirements requirements) {
    if ((requirements.memoryTypeBits & (1 << mMemory.getMemoryTypeIndex())) ==
        0) {
      std::abort();
    }

    std::lock_guard lock(mMtx);

    for (auto elem : table) {
      auto offset = (elem.beginAddress + requirements.alignment - 1) &
                    ~(requirements.alignment - 1);

      if (offset >= elem.endAddress) {
        continue;
      }

      auto blockSize = elem.endAddress - offset;

      if (blockSize < requirements.size) {
        continue;
      }

      // if (debugName == std::string_view{"local"}) {
      // std::printf("memory: allocation %s memory %lx-%lx\n", debugName,
      // offset,
      //             offset + requirements.size);
      // }

      table.unmap(offset, offset + requirements.size);
      return {.deviceMemory = mMemory.getHandle(),
              .offset = offset,
              .size = requirements.size,
              .data = mData,
              .allocator = this,
              .release = [](DeviceMemoryRef &memoryRef) {
                auto self =
                    reinterpret_cast<MemoryResource *>(memoryRef.allocator);
                self->deallocate(memoryRef);
              }};
    }

    std::abort();
  }

  void deallocate(DeviceMemoryRef memory) {
    std::lock_guard lock(mMtx);
    table.map(memory.offset, memory.offset + memory.size);
  }

  void dump() {
    std::lock_guard lock(mMtx);

    for (auto elem : table) {
      std::fprintf(stderr, "%zu - %zu\n", elem.beginAddress, elem.endAddress);
    }
  }

  DeviceMemoryRef getFromOffset(std::uint64_t offset, std::size_t size) {
    return {mMemory.getHandle(), offset, size, nullptr, nullptr, nullptr};
  }

  explicit operator bool() const { return mMemory.getHandle() != nullptr; }
};

struct Semaphore {
  VkSemaphore mSemaphore = VK_NULL_HANDLE;

public:
  Semaphore(const Semaphore &) = delete;

  Semaphore() = default;
  Semaphore(Semaphore &&other) noexcept { *this = std::move(other); }

  Semaphore &operator=(Semaphore &&other) noexcept {
    std::swap(mSemaphore, other.mSemaphore);
    return *this;
  }

  ~Semaphore() {
    if (mSemaphore != VK_NULL_HANDLE) {
      vkDestroySemaphore(context->device, mSemaphore, nullptr);
    }
  }

  static Semaphore Create(std::uint64_t initialValue = 0) {
    VkSemaphoreTypeCreateInfo typeCreateInfo = {
        VK_STRUCTURE_TYPE_SEMAPHORE_TYPE_CREATE_INFO, nullptr,
        VK_SEMAPHORE_TYPE_TIMELINE, initialValue};

    VkSemaphoreCreateInfo createInfo = {VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO,
                                        &typeCreateInfo, 0};

    Semaphore result;
    VK_VERIFY(vkCreateSemaphore(context->device, &createInfo, nullptr,
                                &result.mSemaphore));
    return result;
  }

  VkResult wait(std::uint64_t value, uint64_t timeout) const {
    VkSemaphoreWaitInfo waitInfo = {VK_STRUCTURE_TYPE_SEMAPHORE_WAIT_INFO,
                                    nullptr,
                                    VK_SEMAPHORE_WAIT_ANY_BIT,
                                    1,
                                    &mSemaphore,
                                    &value};

    return vkWaitSemaphores(context->device, &waitInfo, timeout);
  }

  void signal(std::uint64_t value) {
    VkSemaphoreSignalInfo signalInfo = {VK_STRUCTURE_TYPE_SEMAPHORE_SIGNAL_INFO,
                                        nullptr, mSemaphore, value};

    VK_VERIFY(vkSignalSemaphore(context->device, &signalInfo));
  }

  [[gnu::used, nodiscard]] std::uint64_t getCounterValue() const {
    std::uint64_t result = 0;
    VK_VERIFY(vkGetSemaphoreCounterValue(context->device, mSemaphore, &result));
    return result;
  }

  VkSemaphore getHandle() const { return mSemaphore; }

  bool operator==(std::nullptr_t) const { return mSemaphore == nullptr; }
  bool operator!=(std::nullptr_t) const { return mSemaphore != nullptr; }
};

struct BinSemaphore {
  VkSemaphore mSemaphore = VK_NULL_HANDLE;

public:
  BinSemaphore(const BinSemaphore &) = delete;

  BinSemaphore() = default;
  BinSemaphore(BinSemaphore &&other) noexcept { *this = std::move(other); }

  BinSemaphore &operator=(BinSemaphore &&other) noexcept {
    std::swap(mSemaphore, other.mSemaphore);
    return *this;
  }

  ~BinSemaphore() {
    if (mSemaphore != VK_NULL_HANDLE) {
      vkDestroySemaphore(context->device, mSemaphore, nullptr);
    }
  }

  static BinSemaphore Create() {
    VkSemaphoreTypeCreateInfo typeCreateInfo = {
        VK_STRUCTURE_TYPE_SEMAPHORE_TYPE_CREATE_INFO, nullptr,
        VK_SEMAPHORE_TYPE_BINARY, 0};

    VkSemaphoreCreateInfo createInfo = {VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO,
                                        &typeCreateInfo, 0};

    BinSemaphore result;
    VK_VERIFY(vkCreateSemaphore(context->device, &createInfo, nullptr,
                                &result.mSemaphore));
    return result;
  }

  [[nodiscard]] VkSemaphore getHandle() const { return mSemaphore; }

  bool operator==(std::nullptr_t) const { return mSemaphore == nullptr; }
};

struct Fence {
  VkFence mFence = VK_NULL_HANDLE;

public:
  Fence(const Fence &) = delete;

  Fence() = default;
  Fence(Fence &&other) noexcept { *this = std::move(other); }

  Fence &operator=(Fence &&other) noexcept {
    std::swap(mFence, other.mFence);
    return *this;
  }

  ~Fence() {
    if (mFence != VK_NULL_HANDLE) {
      vkDestroyFence(context->device, mFence, nullptr);
    }
  }

  static Fence Create() {
    VkFenceCreateInfo fenceCreateInfo = {VK_STRUCTURE_TYPE_FENCE_CREATE_INFO,
                                         nullptr, 0};
    Fence result;
    VK_VERIFY(vkCreateFence(context->device, &fenceCreateInfo, nullptr,
                            &result.mFence));
    return result;
  }

  void wait() const {
    VK_VERIFY(vkWaitForFences(context->device, 1, &mFence, 1, UINT64_MAX));
  }

  [[nodiscard]] bool isComplete() const {
    return vkGetFenceStatus(context->device, mFence) == VK_SUCCESS;
  }

  void reset() { vkResetFences(context->device, 1, &mFence); }

  VkFence getHandle() const { return mFence; }

  bool operator==(std::nullptr_t) const { return mFence == nullptr; }
};

struct CommandBuffer {
  VkCommandBuffer mCmdBuffer = VK_NULL_HANDLE;

public:
  CommandBuffer(const CommandBuffer &) = delete;

  CommandBuffer() = default;
  CommandBuffer(CommandBuffer &&other) noexcept { *this = std::move(other); }

  CommandBuffer &operator=(CommandBuffer &&other) noexcept {
    std::swap(mCmdBuffer, other.mCmdBuffer);
    return *this;
  }

  CommandBuffer(VkCommandPool commandPool,
                VkCommandBufferLevel level = VK_COMMAND_BUFFER_LEVEL_PRIMARY,
                VkCommandBufferUsageFlags flags = {}) {
    VkCommandBufferAllocateInfo allocInfo{};
    allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
    allocInfo.level = level;
    allocInfo.commandPool = commandPool;
    allocInfo.commandBufferCount = 1;

    vkAllocateCommandBuffers(context->device, &allocInfo, &mCmdBuffer);

    VkCommandBufferBeginInfo beginInfo{};
    beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
    beginInfo.flags = flags;

    vkBeginCommandBuffer(mCmdBuffer, &beginInfo);
  }

  VkCommandBuffer getHandle() const { return mCmdBuffer; }

  operator VkCommandBuffer() const { return mCmdBuffer; }

  void end() { vkEndCommandBuffer(mCmdBuffer); }

  bool operator==(std::nullptr_t) const { return mCmdBuffer == nullptr; }
};

class CommandPool {
  VkCommandPool mHandle = VK_NULL_HANDLE;

public:
  CommandPool(const CommandPool &) = delete;

  CommandPool() = default;
  CommandPool(CommandPool &&other) noexcept { *this = std::move(other); }
  ~CommandPool() {
    if (mHandle != nullptr) {
      vkDestroyCommandPool(context->device, mHandle, context->allocator);
    }
  }

  CommandPool &operator=(CommandPool &&other) noexcept {
    std::swap(mHandle, other.mHandle);
    return *this;
  }

  static CommandPool Create(uint32_t queueFamilyIndex,
                            VkCommandPoolCreateFlags flags = 0) {
    VkCommandPoolCreateInfo info{
        .sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO,
        .flags = flags,
        .queueFamilyIndex = queueFamilyIndex,
    };

    CommandPool result;
    VK_VERIFY(vkCreateCommandPool(context->device, &info, context->allocator,
                                  &result.mHandle));
    return result;
  }

  CommandBuffer createOneTimeSubmitBuffer() {
    return createPrimaryBuffer(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT);
  }

  CommandBuffer createPrimaryBuffer(VkCommandBufferUsageFlags flags) {
    return {mHandle, VK_COMMAND_BUFFER_LEVEL_PRIMARY, flags};
  }

  operator VkCommandPool() const { return mHandle; }
  [[nodiscard]] VkCommandPool getHandle() const { return mHandle; }

  bool operator==(std::nullptr_t) const { return mHandle == nullptr; }
};

class CommandPoolRef {
  VkCommandPool mHandle = VK_NULL_HANDLE;

public:
  CommandPoolRef() = default;
  CommandPoolRef(VkCommandPool handle) : mHandle(handle) {}
  CommandPoolRef(const CommandPool &pool) : mHandle(pool.getHandle()) {}

  CommandBuffer createOneTimeSubmitBuffer() {
    return createPrimaryBuffer(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT);
  }

  CommandBuffer createPrimaryBuffer(VkCommandBufferUsageFlags flags) {
    return CommandBuffer(mHandle, VK_COMMAND_BUFFER_LEVEL_PRIMARY, flags);
  }

  VkCommandPool getHandle() const { return mHandle; }
  operator VkCommandPool() const { return mHandle; }

  bool operator==(std::nullptr_t) const { return mHandle == nullptr; }
};

class Buffer {
  VkBuffer mBuffer = VK_NULL_HANDLE;
  VkDeviceAddress mAddress{};
  DeviceMemoryRef mMemory;

public:
  Buffer(const Buffer &) = delete;

  Buffer() = default;
  Buffer(Buffer &&other) noexcept { *this = std::move(other); }
  ~Buffer() {
    if (mBuffer != nullptr) {
      vkDestroyBuffer(context->device, mBuffer, context->allocator);

      if (mMemory.release != nullptr) {
        mMemory.release(mMemory);
      }
    }
  }

  Buffer &operator=(Buffer &&other) noexcept {
    std::swap(mBuffer, other.mBuffer);
    std::swap(mAddress, other.mAddress);
    std::swap(mMemory, other.mMemory);
    return *this;
  }

  Buffer(std::size_t size, VkBufferUsageFlags usage,
         VkBufferCreateFlags flags = 0,
         VkSharingMode sharingMode = VK_SHARING_MODE_EXCLUSIVE,
         std::span<const std::uint32_t> queueFamilyIndices = {}) {
    VkBufferCreateInfo bufferInfo{
        .sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO,
        .flags = flags,
        .size = size,
        .usage = usage | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT,
        .sharingMode = sharingMode,
        .queueFamilyIndexCount =
            static_cast<std::uint32_t>(queueFamilyIndices.size()),
        .pQueueFamilyIndices = queueFamilyIndices.data(),
    };

    VK_VERIFY(vkCreateBuffer(context->device, &bufferInfo, context->allocator,
                             &mBuffer));
  }

  operator VkBuffer() const { return mBuffer; }

  [[nodiscard]] std::byte *getData() const {
    rx::dieIf(mMemory.data == nullptr,
              "unexpected Buffer::getData call with device local memory");
    return reinterpret_cast<std::byte *>(mMemory.data) + mMemory.offset;
  }

  static Buffer
  CreateExternal(std::size_t size, VkBufferUsageFlags usage,
                 VkBufferCreateFlags flags = 0,
                 VkSharingMode sharingMode = VK_SHARING_MODE_EXCLUSIVE,
                 std::span<const std::uint32_t> queueFamilyIndices = {}) {
    VkExternalMemoryBufferCreateInfo info{
        VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_BUFFER_CREATE_INFO, nullptr,
        VK_EXTERNAL_MEMORY_HANDLE_TYPE_HOST_ALLOCATION_BIT_EXT};

    VkBufferCreateInfo bufferInfo{};
    bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
    bufferInfo.pNext = &info;
    bufferInfo.flags = flags;
    bufferInfo.size = size;
    bufferInfo.usage = usage;
    bufferInfo.sharingMode = sharingMode;
    bufferInfo.queueFamilyIndexCount = queueFamilyIndices.size();
    bufferInfo.pQueueFamilyIndices = queueFamilyIndices.data();

    Buffer result;

    VK_VERIFY(vkCreateBuffer(context->device, &bufferInfo, context->allocator,
                             &result.mBuffer));

    return result;
  }

  static Buffer
  Allocate(MemoryResource &pool, std::size_t size, VkBufferUsageFlags usage = 0,
           VkBufferCreateFlags flags = 0,
           VkSharingMode sharingMode = VK_SHARING_MODE_EXCLUSIVE,
           std::span<const std::uint32_t> queueFamilyIndices = {}) {
    Buffer result(size, usage, flags, sharingMode, queueFamilyIndices);
    result.allocateAndBind(pool);
    return result;
  }

  [[nodiscard]] VkDeviceAddress getAddress() const { return mAddress; }
  [[nodiscard]] VkBuffer getHandle() const { return mBuffer; }
  [[nodiscard]] VkBuffer release() { return std::exchange(mBuffer, nullptr); }

  [[nodiscard]] VkMemoryRequirements getMemoryRequirements() const {
    VkMemoryRequirements requirements{};
    vkGetBufferMemoryRequirements(context->device, mBuffer, &requirements);
    return requirements;
  }

  void allocateAndBind(MemoryResource &pool) {
    auto memory = pool.allocate(getMemoryRequirements());
    bindMemory(memory);
  }

  void bindMemory(DeviceMemoryRef memory) {
    VK_VERIFY(vkBindBufferMemory(context->device, mBuffer, memory.deviceMemory,
                                 memory.offset));
    mMemory = memory;

    VkBufferDeviceAddressInfo addressInfo{
        .sType = VK_STRUCTURE_TYPE_BUFFER_DEVICE_ADDRESS_INFO,
        .buffer = getHandle(),
    };

    mAddress = vkGetBufferDeviceAddress(vk::context->device, &addressInfo);
  }

  void copyTo(VkCommandBuffer cmdBuffer, VkBuffer dstBuffer,
              std::span<const VkBufferCopy> regions) {
    vkCmdCopyBuffer(cmdBuffer, mBuffer, dstBuffer, regions.size(),
                    regions.data());

    VkDependencyInfo depInfo = {.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO};
    vkCmdPipelineBarrier2(cmdBuffer, &depInfo);
  }

  [[nodiscard]] const DeviceMemoryRef &getMemory() const { return mMemory; }
  bool operator==(std::nullptr_t) const { return mBuffer == nullptr; }
  bool operator!=(std::nullptr_t) const { return mBuffer != nullptr; }
};

class Image {
  VkImage mImage = VK_NULL_HANDLE;
  VkImageType mImageType{};
  VkFormat mFormat = {};
  VkImageAspectFlags mAspects = {};
  VkExtent3D mExtent{};
  unsigned mMipLevels = 0;
  unsigned mArrayLayers = 0;
  VkSampleCountFlagBits mSamples = {};
  DeviceMemoryRef mMemory;

public:
  Image(const Image &) = delete;

  Image() = default;
  Image(Image &&other) noexcept { *this = std::move(other); }

  ~Image() {
    if (mImage != VK_NULL_HANDLE) {
      vkDestroyImage(context->device, mImage, context->allocator);

      if (mMemory.release != nullptr) {
        mMemory.release(mMemory);
      }
    }
  }

  Image &operator=(Image &&other) noexcept {
    std::swap(mImage, other.mImage);
    std::swap(mImageType, other.mImageType);
    std::swap(mFormat, other.mFormat);
    std::swap(mAspects, other.mAspects);
    std::swap(mExtent, other.mExtent);
    std::swap(mMipLevels, other.mMipLevels);
    std::swap(mArrayLayers, other.mArrayLayers);
    std::swap(mSamples, other.mSamples);
    std::swap(mMemory, other.mMemory);
    return *this;
  }

  Image(VkImageType type, VkExtent3D extent, uint32_t mipLevels,
        uint32_t arrayLayers, VkSampleCountFlagBits samples, VkFormat format,
        VkImageUsageFlags usage, VkImageTiling tiling = VK_IMAGE_TILING_OPTIMAL,
        VkSharingMode sharingMode = VK_SHARING_MODE_EXCLUSIVE,
        VkImageLayout initialLayout = VK_IMAGE_LAYOUT_UNDEFINED) {
    VkImageCreateInfo imageInfo{
        .sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO,
        .imageType = type,
        .format = format,
        .extent = extent,
        .mipLevels = mipLevels,
        .arrayLayers = arrayLayers,
        .samples = samples,
        .tiling = tiling,
        .usage = usage,
        .sharingMode = sharingMode,
        .initialLayout = initialLayout,
    };

    mImageType = type;
    mFormat = format;
    mExtent = extent;
    mMipLevels = mipLevels;
    mArrayLayers = arrayLayers;
    mSamples = samples;

    if (usage & VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT) {
      mAspects |= VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT;
    } else {
      mAspects |= VK_IMAGE_ASPECT_COLOR_BIT;
    }

    VK_VERIFY(vkCreateImage(context->device, &imageInfo, nullptr, &mImage));
  }

  operator VkImage() const { return mImage; }

  static Image
  Allocate(MemoryResource &pool, VkImageType type, VkExtent3D extent,
           uint32_t mipLevels, uint32_t arrayLayers, VkFormat format,
           VkSampleCountFlagBits samples, VkImageUsageFlags usage,
           VkImageTiling tiling = VK_IMAGE_TILING_OPTIMAL,
           VkSharingMode sharingMode = VK_SHARING_MODE_EXCLUSIVE,
           VkImageLayout initialLayout = VK_IMAGE_LAYOUT_UNDEFINED) {

    Image result(type, extent, mipLevels, arrayLayers, samples, format, usage,
                 tiling, sharingMode, initialLayout);

    result.allocateAndBind(pool);
    return result;
  }

  [[nodiscard]] VkExtent3D getExtent() const { return mExtent; }
  [[nodiscard]] VkImageType getImageType() const { return mImageType; }
  [[nodiscard]] VkFormat getFormat() const { return mFormat; }
  [[nodiscard]] VkImageAspectFlags getAspects() const { return mAspects; }
  [[nodiscard]] std::uint32_t getWidth() const { return getExtent().width; }
  [[nodiscard]] std::uint32_t getHeight() const { return getExtent().height; }
  [[nodiscard]] std::uint32_t getDepth() const { return getExtent().depth; }
  [[nodiscard]] std::uint32_t getArrayLayers() const { return mArrayLayers; }
  [[nodiscard]] std::uint32_t getMipLevels() const { return mMipLevels; }
  [[nodiscard]] VkSampleCountFlagBits getSamples() const { return mSamples; }

  [[nodiscard]] VkImage getHandle() const { return mImage; }
  [[nodiscard]] VkImage release() { return std::exchange(mImage, nullptr); }

  [[nodiscard]] VkMemoryRequirements getMemoryRequirements() const {
    VkMemoryRequirements requirements{};
    vkGetImageMemoryRequirements(context->device, mImage, &requirements);
    return requirements;
  }

  void allocateAndBind(MemoryResource &pool) {
    auto memory = pool.allocate(getMemoryRequirements());
    bindMemory(memory);
  }

  void bindMemory(DeviceMemoryRef memory) {
    VK_VERIFY(vkBindImageMemory(context->device, mImage, memory.deviceMemory,
                                memory.offset));
    mMemory = memory;
  }

  const DeviceMemoryRef &getMemory() const { return mMemory; }
};

struct ImageView {
  VkImageView mHandle = VK_NULL_HANDLE;
  VkImageViewType mType{};
  VkFormat mFormat{};
  VkImageSubresourceRange mSubresourceRange;

public:
  ImageView(const ImageView &) = delete;

  ImageView() = default;
  ImageView(ImageView &&other) noexcept { *this = std::move(other); }

  ~ImageView() {
    if (mHandle != nullptr) {
      vkDestroyImageView(context->device, mHandle, context->allocator);
    }
  }

  ImageView &operator=(ImageView &&other) noexcept {
    std::swap(mHandle, other.mHandle);
    std::swap(mType, other.mType);
    std::swap(mFormat, other.mFormat);
    std::swap(mSubresourceRange, other.mSubresourceRange);
    return *this;
  }

  ImageView(VkImageViewType type, VkImage image, VkFormat format,
            VkComponentMapping components,
            VkImageSubresourceRange subresourceRange) {
    VkImageViewCreateInfo imageInfo{
        .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,
        .flags = 0,
        .image = image,
        .viewType = type,
        .format = format,
        .components = components,
        .subresourceRange = subresourceRange,
    };

    VK_VERIFY(vkCreateImageView(context->device, &imageInfo, context->allocator,
                                &mHandle));
  }

  [[nodiscard]] VkImageView getHandle() const { return mHandle; }

  [[nodiscard]] VkImageView release() {
    return std::exchange(mHandle, nullptr);
  }
};

vk::MemoryResource &getHostVisibleMemory();
vk::MemoryResource &getDeviceLocalMemory();

VkResult CreateShadersEXT(VkDevice device, uint32_t createInfoCount,
                          const VkShaderCreateInfoEXT *pCreateInfos,
                          const VkAllocationCallbacks *pAllocator,
                          VkShaderEXT *pShaders);

void DestroyShaderEXT(VkDevice device, VkShaderEXT shader,
                      const VkAllocationCallbacks *pAllocator);

void CmdBindShadersEXT(VkCommandBuffer commandBuffer, uint32_t stageCount,
                       const VkShaderStageFlagBits *pStages,
                       const VkShaderEXT *pShaders);
void CmdSetColorBlendEnableEXT(VkCommandBuffer commandBuffer,
                               uint32_t firstAttachment,
                               uint32_t attachmentCount,
                               const VkBool32 *pColorBlendEnables);
void CmdSetColorBlendEquationEXT(
    VkCommandBuffer commandBuffer, uint32_t firstAttachment,
    uint32_t attachmentCount,
    const VkColorBlendEquationEXT *pColorBlendEquations);

void CmdSetDepthClampEnableEXT(VkCommandBuffer commandBuffer,
                               VkBool32 depthClampEnable);
void CmdSetLogicOpEXT(VkCommandBuffer commandBuffer, VkLogicOp logicOp);
void CmdSetPolygonModeEXT(VkCommandBuffer commandBuffer,
                          VkPolygonMode polygonMode);
void CmdSetAlphaToOneEnableEXT(VkCommandBuffer commandBuffer,
                               VkBool32 alphaToOneEnable);
void CmdSetLogicOpEnableEXT(VkCommandBuffer commandBuffer,
                            VkBool32 logicOpEnable);
void CmdSetRasterizationSamplesEXT(VkCommandBuffer commandBuffer,
                                   VkSampleCountFlagBits rasterizationSamples);
void CmdSetSampleMaskEXT(VkCommandBuffer commandBuffer,
                         VkSampleCountFlagBits samples,
                         const VkSampleMask *pSampleMask);
void CmdSetTessellationDomainOriginEXT(VkCommandBuffer commandBuffer,
                                       VkTessellationDomainOrigin domainOrigin);
void CmdSetAlphaToCoverageEnableEXT(VkCommandBuffer commandBuffer,
                                    VkBool32 alphaToCoverageEnable);
void CmdSetVertexInputEXT(
    VkCommandBuffer commandBuffer, uint32_t vertexBindingDescriptionCount,
    const VkVertexInputBindingDescription2EXT *pVertexBindingDescriptions,
    uint32_t vertexAttributeDescriptionCount,
    const VkVertexInputAttributeDescription2EXT *pVertexAttributeDescriptions);
void CmdSetColorWriteMaskEXT(VkCommandBuffer commandBuffer,
                             uint32_t firstAttachment, uint32_t attachmentCount,
                             const VkColorComponentFlags *pColorWriteMasks);

void GetDescriptorSetLayoutSizeEXT(VkDevice device,
                                   VkDescriptorSetLayout layout,
                                   VkDeviceSize *pLayoutSizeInBytes);

void GetDescriptorSetLayoutBindingOffsetEXT(VkDevice device,
                                            VkDescriptorSetLayout layout,
                                            uint32_t binding,
                                            VkDeviceSize *pOffset);
void GetDescriptorEXT(VkDevice device,
                      const VkDescriptorGetInfoEXT *pDescriptorInfo,
                      size_t dataSize, void *pDescriptor);
void CmdBindDescriptorBuffersEXT(
    VkCommandBuffer commandBuffer, uint32_t bufferCount,
    const VkDescriptorBufferBindingInfoEXT *pBindingInfos);

void CmdSetDescriptorBufferOffsetsEXT(VkCommandBuffer commandBuffer,
                                      VkPipelineBindPoint pipelineBindPoint,
                                      VkPipelineLayout layout,
                                      uint32_t firstSet, uint32_t setCount,
                                      const uint32_t *pBufferIndices,
                                      const VkDeviceSize *pOffsets);

void CmdBindDescriptorBufferEmbeddedSamplersEXT(
    VkCommandBuffer commandBuffer, VkPipelineBindPoint pipelineBindPoint,
    VkPipelineLayout layout, uint32_t set);

VkResult CreateDebugUtilsMessengerEXT(
    VkInstance instance, const VkDebugUtilsMessengerCreateInfoEXT *pCreateInfo,
    const VkAllocationCallbacks *pAllocator,
    VkDebugUtilsMessengerEXT *pMessenger);
void DestroyDebugUtilsMessengerEXT(VkInstance instance,
                                   VkDebugUtilsMessengerEXT messenger,
                                   const VkAllocationCallbacks *pAllocator);
} // namespace vk
