/* Copyright (c) 2024-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.
 */

#include <vulkan/vulkan_core.h>
#include "descriptor_checks_classic.h"
#include "drawdispatch/drawdispatch_vuids.h"
#include "gpuav/core/gpuav.h"
#include "gpuav/resources/gpuav_shader_resources.h"
#include "gpuav/resources/gpuav_state_trackers.h"
#include "gpuav/shaders/gpuav_error_codes.h"
#include "gpuav/shaders/gpuav_error_header.h"
#include "gpuav/shaders/gpuav_shaders_constants.h"

namespace gpuav {

// Returns the number of bytes to hold 32 bit aligned array of bits.
static uint32_t BitBufferSize(uint32_t num_bits) {
    static constexpr uint32_t kBitsPerWord = 32;
    return (((num_bits + (kBitsPerWord - 1)) & ~(kBitsPerWord - 1)) / kBitsPerWord) * sizeof(uint32_t);
}

DescriptorIdPool::DescriptorIdPool(Validator& gpuav, uint32_t max_descriptors) : max_descriptors_(max_descriptors), buffer_(gpuav) {
    // If max_descriptors_ is 0, GPU-AV aborted during vkCreateDevice(). We still need to
    // support calls into this class as no-ops if this happens.
    if (max_descriptors_ == 0) {
        return;
    }

    VkBufferCreateInfo buffer_info = vku::InitStruct<VkBufferCreateInfo>();
    buffer_info.size = BitBufferSize(max_descriptors_ + 1);  // add extra entry since 0 is the invalid id.
    buffer_info.usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT;

    VmaAllocationCreateInfo alloc_info{};
    alloc_info.requiredFlags = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT;
    const VkResult result = buffer_.Create(&buffer_info, &alloc_info);
    if (result != VK_SUCCESS) {
        return;
    }

    gpu_id_pool_state_ = (uint32_t*)buffer_.GetMappedPtr();
    memset(gpu_id_pool_state_, 0, static_cast<size_t>(buffer_info.size));
}

DescriptorIdPool::~DescriptorIdPool() {
    if (max_descriptors_ > 0) {
        buffer_.Destroy();
        gpu_id_pool_state_ = nullptr;
    }
}

DescriptorId DescriptorIdPool::NextId(const VulkanTypedHandle& handle) {
    if (max_descriptors_ == 0) {
        return 0;
    }
    DescriptorId result;

    // NOTE: valid ids are in the range [1, max_descriptors_] (inclusive)
    // 0 is the invalid id.
    std::lock_guard guard(lock_);
    if (alloc_map_.size() >= max_descriptors_) {
        return 0;
    }
    do {
        result = next_id_++;
        if (next_id_ > max_descriptors_) {
            next_id_ = 1;
        }
    } while (alloc_map_.count(result) > 0);
    alloc_map_[result] = handle;
    gpu_id_pool_state_[result / 32] |= 1u << (result & 31);
    return result;
}

void DescriptorIdPool::DeleteId(DescriptorId id) {
    if (max_descriptors_ > 0) {
        std::lock_guard guard(lock_);
        // Note: We don't mess with next_id_ here because ids should be assigned in LRU order.
        gpu_id_pool_state_[id / 32] &= ~(1u << (id & 31));
        alloc_map_.erase(id);
    }
}

struct DescriptorChecksClassicCbState {
    vko::BufferRange last_bound_desc_sets_ssbo;
};

void DescriptorChecksClassicOnFinishDeviceSetup(Validator& gpuav) {
    if (!gpuav.gpuav_settings.shader_instrumentation.descriptor_checks) {
        gpuav.shared_resources_cache.GetOrCreate<DescriptorIdPool>(gpuav, 0);
        return;
    }

    VkPhysicalDeviceDescriptorIndexingProperties desc_indexing_props = vku::InitStructHelper();
    VkPhysicalDeviceProperties2 props2 = vku::InitStructHelper(&desc_indexing_props);
    DispatchGetPhysicalDeviceProperties2Helper(gpuav.api_version, gpuav.physical_device, &props2);

    uint32_t num_descs = desc_indexing_props.maxUpdateAfterBindDescriptorsInAllPools;
    if (num_descs == 0 || num_descs > glsl::kDebugInputBindlessMaxDescriptors) {
        num_descs = glsl::kDebugInputBindlessMaxDescriptors;
    }

    gpuav.shared_resources_cache.GetOrCreate<DescriptorIdPool>(gpuav, num_descs);
}

void RegisterDescriptorChecksClassicValidation(Validator& gpuav, CommandBufferSubState& cb) {
    if (!gpuav.gpuav_settings.shader_instrumentation.descriptor_checks) {
        return;
    }

    cb.on_instrumentation_error_logger_register_functions.emplace_back([](Validator& gpuav, CommandBufferSubState& cb,
                                                                          const LastBound&) {
        // We want to grab the last (current) element in descriptor_binding_commands, but as a std::vector, the reference might be
        // garbage later, so just hold the index for later. It is possible to have no descriptor sets bound, for example if using
        // push constants.
        uint32_t descriptor_binding_index = vvl::kNoIndex32;
        DescriptorSetBindings* desc_set_bindings = cb.shared_resources_cache.TryGet<DescriptorSetBindings>();
        if (desc_set_bindings && !desc_set_bindings->descriptor_set_binding_commands.empty()) {
            descriptor_binding_index = uint32_t(desc_set_bindings->descriptor_set_binding_commands.size() - 1);
        }

        CommandBufferSubState::InstrumentationErrorLogger inst_error_logger = [&cb, descriptor_binding_index](
                                                                                  Validator& gpuav, const Location& loc,
                                                                                  const uint32_t* error_record,
                                                                                  const InstrumentedShader*,
                                                                                  std::string& out_error_msg,
                                                                                  std::string& out_vuid_msg) {
            using namespace glsl;

            bool error_found = false;
            if (GetErrorGroup(error_record) != kErrorGroup_InstDescriptorIndexingOOB) {
                return error_found;
            }

            std::ostringstream strm;

            if (descriptor_binding_index == vvl::kNoIndex32) {
                assert(false);  // This means we have hit a situtation where there are no descriptors bound
                return false;
            }
            const DescriptorSetBindings& desc_set_bindings = cb.shared_resources_cache.Get<DescriptorSetBindings>();
            const auto& descriptor_sets =
                desc_set_bindings.descriptor_set_binding_commands[descriptor_binding_index].bound_descriptor_sets;

            // Currently we only encode the descriptor index here and save the binding in a parameter slot
            // The issue becomes if the user has kErrorSubCode_DescriptorIndexing_Bounds then we can't back track to the exact
            // binding because they have gone over it
            const uint32_t encoded_set_index = error_record[kInst_LogError_ParameterOffset_0];
            const uint32_t set_num = encoded_set_index >> kInst_DescriptorIndexing_SetShift;
            const uint32_t descriptor_index = encoded_set_index & kInst_DescriptorIndexing_IndexMask;
            const uint32_t binding_num = error_record[kInst_LogError_ParameterOffset_2];

            const uint32_t array_length = error_record[kInst_LogError_ParameterOffset_1];

            const uint32_t error_sub_code = GetSubError(error_record);
            switch (error_sub_code) {
                case kErrorSubCode_DescriptorIndexing_Bounds: {
                    strm << "(set = " << set_num << ", binding = " << binding_num << ") Index of " << descriptor_index
                         << " used to index descriptor array of length " << array_length << ".";
                    out_vuid_msg = vvl::CreateActionVuid(loc.function, vvl::ActionVUID::DESCRIPTOR_INDEX_OOB_10068);
                    error_found = true;
                } break;

                case kErrorSubCode_DescriptorIndexing_Uninitialized: {
                    const auto& dsl = descriptor_sets[set_num]->Layout();
                    strm << "(set = " << set_num << ", binding = " << binding_num << ") Descriptor index " << descriptor_index
                         << " is uninitialized.";

                    if (descriptor_index == 0 && array_length == 1) {
                        strm << " (There is no array, but descriptor is viewed as having an array of length 1)";
                    }

                    const VkDescriptorBindingFlags binding_flags = dsl.GetDescriptorBindingFlagsFromBinding(binding_num);
                    if (binding_flags & VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT) {
                        strm << " VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT was used and the original descriptorCount ("
                             << dsl.GetDescriptorCountFromBinding(binding_num)
                             << ") could have been reduced during AllocateDescriptorSets.";
                    } else if (gpuav.enabled_features.nullDescriptor) {
                        strm << " nullDescriptor feature is on, but vkUpdateDescriptorSets was not called with VK_NULL_HANDLE for "
                                "this "
                                "descriptor.";
                    }

                    out_vuid_msg = vvl::CreateActionVuid(loc.function, vvl::ActionVUID::INVALID_DESCRIPTOR_08114);
                    error_found = true;
                } break;

                case kErrorSubCode_DescriptorIndexing_Destroyed: {
                    strm << "(set = " << set_num << ", binding = " << binding_num << ") Descriptor index " << descriptor_index
                         << " references a resource that was destroyed.";

                    if (descriptor_index == 0 && array_length == 1) {
                        strm << " (There is no array, but descriptor is viewed as having an array of length 1)";
                    }

                    out_vuid_msg = "UNASSIGNED-Descriptor destroyed";
                    error_found = true;
                } break;
            }
            out_error_msg += strm.str();
            return error_found;
        };

        return inst_error_logger;
    });

    cb.on_instrumentation_error_logger_register_functions.emplace_back([](Validator& gpuav, CommandBufferSubState& cb,
                                                                          const LastBound& last_bound) {
        // We want to grab the last (current) element in descriptor_binding_commands, but as a std::vector, the reference might be
        // garbage later, so just hold the index for later. It is possible to have no descriptor sets bound, for example if using
        // push constants.
        uint32_t descriptor_binding_index = vvl::kNoIndex32;
        DescriptorSetBindings* desc_set_bindings = cb.shared_resources_cache.TryGet<DescriptorSetBindings>();
        if (desc_set_bindings && !desc_set_bindings->descriptor_set_binding_commands.empty()) {
            descriptor_binding_index = uint32_t(desc_set_bindings->descriptor_set_binding_commands.size() - 1);
        }

        bool uses_shader_object = last_bound.pipeline_state == nullptr;

        CommandBufferSubState::InstrumentationErrorLogger inst_error_logger = [&cb, descriptor_binding_index, uses_shader_object](
                                                                                  Validator& gpuav, const Location& loc,
                                                                                  const uint32_t* error_record,
                                                                                  const InstrumentedShader*,
                                                                                  std::string& out_error_msg,
                                                                                  std::string& out_vuid_msg) {
            using namespace glsl;
            bool error_found = false;
            if (GetErrorGroup(error_record) != kErrorGroup_InstDescriptorClass) {
                return error_found;
            }
            error_found = true;
            std::ostringstream strm;

            if (descriptor_binding_index == vvl::kNoIndex32) {
                assert(false);  // This means we have hit a situtation where there are no descriptors bound
                return false;
            }
            const DescriptorSetBindings& desc_set_bindings = cb.shared_resources_cache.Get<DescriptorSetBindings>();
            const auto& descriptor_sets =
                desc_set_bindings.descriptor_set_binding_commands[descriptor_binding_index].bound_descriptor_sets;

            const uint32_t encoded_set_index = error_record[kInst_LogError_ParameterOffset_0];
            const uint32_t set_num = encoded_set_index >> kInst_DescriptorIndexing_SetShift;
            const uint32_t global_descriptor_index = encoded_set_index & kInst_DescriptorIndexing_IndexMask;

            const auto descriptor_set_state = descriptor_sets[set_num];
            auto [binding_num, desc_index] = descriptor_set_state->GetBindingAndIndex(global_descriptor_index);

            const auto* binding_state = descriptor_set_state->GetBinding(binding_num);

            strm << "(set = " << set_num << ", binding = " << binding_num << ", index " << desc_index << ") ";

            const uint32_t error_sub_code = GetSubError(error_record);
            switch (error_sub_code) {
                case kErrorSubCode_DescriptorClass_GeneralBufferBounds:
                case kErrorSubCode_DescriptorClass_GeneralBufferCoopMatBounds: {
                    if (binding_state->descriptor_class != vvl::DescriptorClass::GeneralBuffer) {
                        assert(false);
                        return false;
                    }
                    const vvl::Buffer* buffer_state =
                        static_cast<const vvl::BufferBinding*>(binding_state)->descriptors[desc_index].GetBufferState();
                    if (buffer_state) {
                        const uint32_t byte_offset = error_record[kInst_LogError_ParameterOffset_1];
                        const uint32_t resource_size = error_record[kInst_LogError_ParameterOffset_2];
                        strm << " access out of bounds. The descriptor buffer (" << gpuav.FormatHandle(buffer_state->Handle())
                             << ") size is " << buffer_state->GetSize() << " bytes, " << resource_size
                             << " bytes were bound, and the highest out of bounds access was at [" << byte_offset << "] bytes";
                    } else {
                        // This will only get called when using nullDescriptor without bindless
                        strm << "Trying to access a null descriptor, but vkUpdateDescriptorSets was not called with VK_NULL_HANDLE "
                                "for "
                                "this descriptor. ";
                    }
                    if (error_sub_code == kErrorSubCode_DescriptorClass_GeneralBufferCoopMatBounds) {
                        strm << "\nFor VK_KHR_cooperative_matrix this is invalid unless cooperativeMatrixRobustBufferAccess is "
                                "enabled.";
                    }

                    vvl::ActionVUID action_vuid = vvl::ActionVUID::UNKNOWN;
                    if (binding_state->type == VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER ||
                        binding_state->type == VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC) {
                        action_vuid = uses_shader_object ? vvl::ActionVUID::UNIFORM_ACCESS_OOB_08612
                                                         : vvl::ActionVUID::UNIFORM_ACCESS_OOB_06935;
                    } else {
                        action_vuid = uses_shader_object ? vvl::ActionVUID::STORAGE_ACCESS_OOB_08613
                                                         : vvl::ActionVUID::STORAGE_ACCESS_OOB_06936;
                    }
                    out_vuid_msg = vvl::CreateActionVuid(loc.function, action_vuid);
                } break;

                case kErrorSubCode_DescriptorClass_TexelBufferBounds: {
                    if (binding_state->descriptor_class != vvl::DescriptorClass::TexelBuffer) {
                        assert(false);
                        return false;
                    }

                    const vvl::BufferView* buffer_view_state =
                        static_cast<const vvl::TexelBinding*>(binding_state)->descriptors[desc_index].GetBufferViewState();
                    if (buffer_view_state) {
                        const uint32_t byte_offset = error_record[kInst_LogError_ParameterOffset_1];
                        const uint32_t resource_size = error_record[kInst_LogError_ParameterOffset_2];

                        strm << " access out of bounds. The descriptor texel buffer ("
                             << gpuav.FormatHandle(buffer_view_state->Handle()) << ") size is " << resource_size
                             << " texels and the highest out of bounds access was at [" << byte_offset << "] bytes";
                    } else {
                        // This will only get called when using nullDescriptor without bindless
                        strm << "Trying to access a null descriptor, but vkUpdateDescriptorSets was not called with VK_NULL_HANDLE "
                                "for "
                                "this descriptor. ";
                    }

                    // https://gitlab.khronos.org/vulkan/vulkan/-/issues/3977
                    out_vuid_msg = "UNASSIGNED-Descriptor Texel Buffer texel out of bounds";
                } break;

                default:
                    error_found = false;
                    assert(false);  // other OOB checks are not implemented yet
            }

            out_error_msg += strm.str();
            return error_found;
        };

        return inst_error_logger;
    });

    DescriptorSetBindings& desc_set_bindings = cb.shared_resources_cache.GetOrCreate<DescriptorSetBindings>();

    desc_set_bindings.on_update_bound_descriptor_sets.emplace_back([](Validator& gpuav, CommandBufferSubState& cb,
                                                                      DescriptorSetBindings::BindingCommand& desc_binding_cmd) {
        DescriptorChecksClassicCbState& dc_cb_state = cb.shared_resources_cache.GetOrCreate<DescriptorChecksClassicCbState>();
        dc_cb_state.last_bound_desc_sets_ssbo =
            cb.gpu_resources_manager.GetHostCoherentBufferRange(sizeof(glsl::BoundDescriptorSetsSSBO));
        dc_cb_state.last_bound_desc_sets_ssbo.Clear();
        auto desc_sets_ssbo = static_cast<glsl::BoundDescriptorSetsSSBO*>(dc_cb_state.last_bound_desc_sets_ssbo.offset_mapped_ptr);
        desc_sets_ssbo->descriptor_init_status = gpuav.shared_resources_cache.Get<DescriptorIdPool>().GetDeviceAddress();

        for (size_t bound_ds_i = 0; bound_ds_i < desc_binding_cmd.bound_descriptor_sets.size(); ++bound_ds_i) {
            auto& bound_ds = desc_binding_cmd.bound_descriptor_sets[bound_ds_i];
            // Account for gaps in descriptor sets bindings
            if (!bound_ds) {
                continue;
            }

            if (bound_ds->IsUpdateAfterBind()) {
                continue;
            }
            desc_sets_ssbo->descriptor_encodings[bound_ds_i] = SubState(*bound_ds).GetDescriptorEncodingsAddress(gpuav);
        }

        desc_binding_cmd.bound_desc_sets_ssbo = dc_cb_state.last_bound_desc_sets_ssbo;
    });

    cb.on_instrumentation_common_desc_update_functions.emplace_back(
        [dummy_buffer_range = vko::BufferRange{}](CommandBufferSubState& cb, const LastBound&, const Location&,
                                                  CommonDescriptorUpdate& out_update) mutable {
            DescriptorChecksClassicCbState* dc_cb_state = cb.shared_resources_cache.TryGet<DescriptorChecksClassicCbState>();
            if (dc_cb_state) {
                const vko::BufferRange& buffer_range = dc_cb_state->last_bound_desc_sets_ssbo;
                out_update.buffer = buffer_range.buffer;
                out_update.offset = buffer_range.offset;
                out_update.range = buffer_range.size;
                out_update.address = buffer_range.offset_address;
            } else {
                // TODO - This will always hit the case for Buffer/Heap mode, this is wrong and just an issue with the fact
                // on_update_bound_descriptor_sets is never called

                // [For Classic mode] If no descriptor set was bound in command buffer, we still need "something" to be in the slot
                // or else it will be marked as invalid for not being updated
                if (dummy_buffer_range.buffer == VK_NULL_HANDLE) {
                    // Caputre the dummy_buffer_range so if found multiple time, only allocate it once
                    dummy_buffer_range = cb.gpu_resources_manager.GetDeviceLocalBufferRange(64);
                }
                out_update.buffer = dummy_buffer_range.buffer;
                out_update.offset = dummy_buffer_range.offset;
                out_update.range = dummy_buffer_range.size;
                out_update.address = dummy_buffer_range.offset_address;
            }

            out_update.binding = glsl::kBindingInstDescriptorIndexingOOB;
        });

    // For every descriptor binding command, and for each set bound by this command, update a GPU buffer holding its
    // corresponding descriptor states
    cb.on_pre_cb_submission_functions.emplace_back([](Validator& gpuav, CommandBufferSubState& cb, VkCommandBuffer) {
        DescriptorSetBindings& desc_set_bindings = cb.shared_resources_cache.Get<DescriptorSetBindings>();
        for (DescriptorSetBindings::BindingCommand& desc_binding_cmd : desc_set_bindings.descriptor_set_binding_commands) {
            auto desc_sets_ssbo_ptr =
                static_cast<glsl::BoundDescriptorSetsSSBO*>(desc_binding_cmd.bound_desc_sets_ssbo.offset_mapped_ptr);
            for (size_t bound_ds_i = 0; bound_ds_i < desc_binding_cmd.bound_descriptor_sets.size(); ++bound_ds_i) {
                auto& bound_ds = desc_binding_cmd.bound_descriptor_sets[bound_ds_i];
                // Account for gaps in descriptor sets bindings
                if (!bound_ds) {
                    continue;
                }

                DescriptorSetSubState& desc_set_state = SubState(*bound_ds);
                desc_sets_ssbo_ptr->descriptor_encodings[bound_ds_i] = desc_set_state.GetDescriptorEncodingsAddress(gpuav);
            }
        }
    });
}

}  // namespace gpuav
