/* Copyright (c) 2018-2026 The Khronos Group Inc.
 * Copyright (c) 2018-2026 Valve Corporation
 * Copyright (c) 2018-2026 LunarG, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#pragma once

#include "external/vma/vma.h"

#include <typeinfo>
#include <vector>
#include "containers/custom_containers.h"
#include "containers/range.h"

struct Location;
namespace gpuav {
class Validator;

namespace vko {

class DescriptorSetManager {
  public:
    DescriptorSetManager(VkDevice device, uint32_t num_bindings_in_set);
    ~DescriptorSetManager();

    VkResult GetDescriptorSet(VkDescriptorPool *out_desc_pool, VkDescriptorSetLayout ds_layout, VkDescriptorSet *out_desc_sets);
    VkResult GetDescriptorSets(uint32_t count, VkDescriptorPool *out_pool, VkDescriptorSetLayout ds_layout,
                               std::vector<VkDescriptorSet> *out_desc_sets);
    void PutBackDescriptorSet(VkDescriptorPool desc_pool, VkDescriptorSet desc_set);

  private:
    struct PoolTracker {
        uint32_t size;
        uint32_t used;
    };
    VkDevice device;
    uint32_t num_bindings_in_set;
    vvl::unordered_map<VkDescriptorPool, PoolTracker> desc_pool_map_;
    mutable std::mutex lock_;
};

class Buffer {
  public:
    explicit Buffer(Validator &gpuav) : gpuav(gpuav) {}

    // Warps VMA calls to simplify error reporting.
    // No error propagation, but if hitting a VMA error, GPU-AV is likely not going to recover anyway.

    [[nodiscard]] void *GetMappedPtr() const;
    void FlushAllocation(VkDeviceSize offset = 0, VkDeviceSize size = VK_WHOLE_SIZE) const;
    void InvalidateAllocation(VkDeviceSize offset = 0, VkDeviceSize size = VK_WHOLE_SIZE) const;

    [[nodiscard]] VkResult Create(const VkBufferCreateInfo* buffer_create_info,
                                  const VmaAllocationCreateInfo* allocation_create_info);
    void Destroy();

    bool IsDestroyed() const { return buffer == VK_NULL_HANDLE; }
    const VkBuffer &VkHandle() const { return buffer; }
    const VmaAllocation &Allocation() const { return allocation; }
    VkDeviceAddress Address() const { return device_address; }
    VkDeviceSize Size() const { return size; }
    void Clear() const;

  private:
    const Validator &gpuav;
    VkBuffer buffer = VK_NULL_HANDLE;
    VmaAllocation allocation = VK_NULL_HANDLE;
    // If buffer was not created with VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT then this will not be zero
    VkDeviceAddress device_address = 0;
    VkDeviceSize size = 0;
    void *mapped_ptr = nullptr;
};

struct BufferRange {
    VkBuffer buffer = VK_NULL_HANDLE;
    VkDeviceSize offset = 0;
    VkDeviceSize size = 0;
    void *offset_mapped_ptr = nullptr;
    VkDeviceAddress offset_address = 0;
    VmaAllocation vma_alloc = VK_NULL_HANDLE;  // Todo: get rid of this once host cached allocation are removed

    VkDescriptorBufferInfo GetDescriptorBufferInfo() const { return {buffer, offset, size}; }
    void Clear() const;
};

void CmdSynchronizedCopyBufferRange(VkCommandBuffer cb, const vko::BufferRange &dst, const vko::BufferRange &src);

// Register/Create and register GPU resources, all to be destroyed upon a call to DestroyResources
class GpuResourcesManager {
  public:
    explicit GpuResourcesManager(Validator &gpuav, bool thread_safe_buffer_caches);

    VkDescriptorSet GetManagedDescriptorSet(VkDescriptorSetLayout desc_set_layout);

    vko::BufferRange GetHostCoherentBufferRange(VkDeviceSize size);
    void ReturnHostCoherentBufferRange(const vko::BufferRange& buffer_range);
    vko::BufferRange GetHostCachedBufferRange(VkDeviceSize size);
    void FlushAllocation(const vko::BufferRange &buffer_range);
    void InvalidateAllocation(const vko::BufferRange &buffer_range);
    vko::BufferRange GetDeviceLocalBufferRange(VkDeviceSize size);
    void ReturnDeviceLocalBufferRange(const vko::BufferRange &buffer_range);
    vko::BufferRange GetDeviceLocalIndirectBufferRange(VkDeviceSize size);
    vko::BufferRange GetStagingBufferRange(VkDeviceSize size);

    void ReturnResources();
    void DestroyResources();

    Validator &gpuav_;

  private:
    struct CachedDescriptor {
        VkDescriptorPool desc_pool = VK_NULL_HANDLE;
        VkDescriptorSet desc_set = VK_NULL_HANDLE;
    };
    struct LayoutToSets {
        VkDescriptorSetLayout desc_set_layout = VK_NULL_HANDLE;
        std::vector<CachedDescriptor> cached_descriptors;
        size_t first_available_desc_set = 0;
    };
    std::vector<LayoutToSets> cache_layouts_to_sets_;

    // Set "thread_safe" to true if the caches needs to be.
    class BufferCache {
      public:
        BufferCache(bool thread_safe) : thread_safe_(thread_safe) {}
        void Create(VkBufferUsageFlags buffer_usage_flags, const VmaAllocationCreateInfo allocation_ci);
        vko::BufferRange GetBufferRange(Validator &gpuav, VkDeviceSize byte_size, VkDeviceSize alignment,
                                        VkDeviceSize min_buffer_block_byte_size = 0);
        ~BufferCache();
        void ReturnBufferRange(const vko::BufferRange &buffer_range);
        void ReturnBuffers();
        void DestroyBuffers();

      private:
        bool thread_safe_ = false;
        VkBufferUsageFlags buffer_usage_flags_{};
        VmaAllocationCreateInfo allocation_ci_{};

        struct CachedBufferBlock {
            vko::Buffer buffer;
            vvl::range<VkDeviceAddress> total_range;
            vvl::range<VkDeviceAddress> used_range;
        };
        mutable std::mutex mtx;
        std::vector<CachedBufferBlock> cached_buffers_blocks_{};
        VkDeviceSize total_available_byte_size_ = 0;
        size_t next_avail_buffer_pos_hint_ = 0;
    };

    // One cache per buffer type: having them mixed in just one would make cache lookups worse
    struct BufferCaches {
        BufferCaches(bool thread_safe)
            : host_coherent(thread_safe),
              host_cached(thread_safe),
              device_local(thread_safe),
              device_local_indirect(thread_safe),
              staging(thread_safe) {}

        // Will have HOST_VISIBLE and HOST_COHERENT (may be HOST_CACHED)
        BufferCache host_coherent;
        // Will have HOST_VISIBLE and HOST_CACHED (may be HOST_COHERENT)
        BufferCache host_cached;
        // Will have DEVICE_LOCAL
        BufferCache device_local;
        // Will have DEVICE_LOCAL with VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT
        BufferCache device_local_indirect;
        // Used to transfer from HOST_VISIBLE to DEVICE_LOCAL buffers
        BufferCache staging;

        void ReturnBuffers() {
            host_coherent.ReturnBuffers();
            host_cached.ReturnBuffers();
            device_local.ReturnBuffers();
            device_local_indirect.ReturnBuffers();
            staging.ReturnBuffers();
        }

        void DestroyBuffers() {
            host_coherent.DestroyBuffers();
            host_cached.DestroyBuffers();
            device_local.DestroyBuffers();
            device_local_indirect.DestroyBuffers();
            staging.DestroyBuffers();
        }
    } buffer_caches_;
};

class StagingBuffer {
  public:
    static bool CanDeviceEverStage(Validator &gpuav);
    StagingBuffer(GpuResourcesManager &gpu_resources_manager, VkDeviceSize size, VkCommandBuffer cb);
    void CmdCopyDeviceToHost(VkCommandBuffer cb) const;
    void CmdCopyHostToDevice(VkCommandBuffer cb) const;
    const BufferRange &GetBufferRange() const { return device_buffer_range; }
    void *GetHostBufferPtr() {
        gpu_resources_manager.InvalidateAllocation(host_buffer_range);
        return host_buffer_range.offset_mapped_ptr;
    }

  private:
    BufferRange host_buffer_range = {};
    BufferRange device_buffer_range = {};
    GpuResourcesManager &gpu_resources_manager;
    VkMemoryPropertyFlags device_buffer_mem_prop_flags = {};
};

// Used to allocate and submit GPU-AV's own command buffers
class CommandPool {
  public:
    CommandPool(Validator &gpuav, uint32_t queue_family_i, const Location &loc);
    ~CommandPool();
    // Returned command buffer is ready to be used,
    // corresponding fence has been waited upon.
    std::pair<VkCommandBuffer, VkFence> GetCommandBuffer();

  private:
    Validator &gpuav_;
    VkCommandPool cmd_pool_ = VK_NULL_HANDLE;
    std::vector<VkCommandBuffer> cmd_buffers_{};
    std::vector<VkFence> fences_{};
    uint32_t cmd_buffer_ring_head_ = 0;
};

// Cache a single object of type T. Key is *only* based on typeid(T)
// Set "thread_safe" to true if the caches needs to be.
template <bool thread_safe>
class SharedResourcesCache {
  public:
    // Try get an object, returns null if not found
    template <typename T>
    T *TryGet() {
        std::unique_lock<std::mutex> lock(mtx, std::defer_lock);
        if (thread_safe) {
            lock.lock();
        }

        auto entry = shared_validation_resources_map_.find(typeid(T));
        if (entry == shared_validation_resources_map_.cend()) {
            return nullptr;
        }
        T *t = reinterpret_cast<T *>(entry->second.first);
        return t;
    }
    template <typename T>
    const T *TryGet() const {
        std::unique_lock<std::mutex> lock(mtx, std::defer_lock);
        if (thread_safe) {
            lock.lock();
        }

        auto entry = shared_validation_resources_map_.find(typeid(T));
        if (entry == shared_validation_resources_map_.cend()) {
            return nullptr;
        }
        const T *t = reinterpret_cast<const T *>(entry->second.first);
        return t;
    }

    // Get an object, assuming it has been created
    template <typename T>
    T &Get() {
        T *t = TryGet<T>();
        assert(t);
        return *t;
    }
    template <typename T>
    const T &Get() const {
        const T *t = TryGet<T>();
        assert(t);
        return *t;
    }

    // First call to GetOrCreate<T> will create the object, subsequent calls will retrieve the cached entry.
    // /!\ The cache key is only based on the type T, not on the passed parameters
    // => Successive calls to GetOrCreate<T> with different parameters will NOT give different objects,
    // only the entry cached upon the first call to Get<T> will be retrieved
    template <typename T, class... ConstructorTypes>
    T &GetOrCreate(ConstructorTypes &&...args) {
        T *t = TryGet<T>();
        if (t) {
            return *t;
        }
        std::unique_lock<std::mutex> lock(mtx, std::defer_lock);
        if (thread_safe) {
            lock.lock();
        }
        auto entry =
            shared_validation_resources_map_.insert({typeid(T), {new T(std::forward<ConstructorTypes>(args)...), [](void *ptr) {
                                                                     auto obj = static_cast<T *>(ptr);
                                                                     delete obj;
                                                                 }}});
        return *static_cast<T *>(entry.first->second.first);
    }

    void Clear() {
        for (auto &[key, value] : shared_validation_resources_map_) {
            auto &[object, destructor] = value;
            destructor(object);
        }
        shared_validation_resources_map_.clear();
    }

  private:
    using TypeInfoRef = std::reference_wrapper<const std::type_info>;
    struct Hasher {
        std::size_t operator()(TypeInfoRef code) const { return code.get().hash_code(); }
    };
    struct EqualTo {
        bool operator()(TypeInfoRef lhs, TypeInfoRef rhs) const { return lhs.get() == rhs.get(); }
    };
    mutable std::mutex mtx;
    vvl::unordered_map<TypeInfoRef, std::pair<void * /*object*/, void (*)(void *) /*object destructor*/>, Hasher, EqualTo>
        shared_validation_resources_map_;
};

}  // namespace vko

}  // namespace gpuav
