/* 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 "debug_printf_pass.h"
#include "generated/spirv_grammar_helper.h"
#include "module.h"
#include "gpuav/shaders/gpuav_error_header.h"
#include <spirv/unified1/NonSemanticDebugPrintf.h>
#include <cstdint>
#include <cstring>
#include <iostream>
#include <spirv/unified1/spirv.hpp>
#include <string>

#include "generated/device_features.h"

namespace gpuav {
namespace spirv {

// All functions are a list of uint32_t
// The difference is just how many are passed in
uint32_t DebugPrintfPass::GetLinkFunctionId(uint32_t argument_count) {
    if (auto it = function_id_map_.find(argument_count); it != function_id_map_.end()) {
        return it->second;
    }

    const uint32_t link_function_id = module_.TakeNextId();
    function_id_map_[argument_count] = link_function_id;

    return link_function_id;
}

bool DebugPrintfPass::RequiresInstrumentation(const Instruction& inst, InstructionMeta& meta) {
    if (inst.Opcode() == spv::OpExtInst && inst.Word(3) == ext_import_id_ && inst.Word(4) == NonSemanticDebugPrintfDebugPrintf) {
        meta.target_instruction = &inst;
        return true;
    }
    return false;
}

// Takes the various arguments and casts them to a valid uint32_t to be passed as a parameter in the function
void DebugPrintfPass::CreateFunctionParams(uint32_t argument_id, const Type& argument_type, std::vector<uint32_t>& params,
                                           BasicBlock& block, InstructionIt* inst_it, ParamMeta& p_meta) {
    const Type& uint32_type = type_manager_.GetTypeInt(32, false);
    const uint32_t uint32_type_id = uint32_type.Id();

    switch (argument_type.spv_type_) {
        case SpvType::kVector: {
            const uint32_t component_count = argument_type.meta_.vector.component_count;
            const uint32_t component_type_id = argument_type.inst_.Word(2);
            const Type* component_type = type_manager_.FindTypeById(component_type_id);
            assert(component_type);
            for (uint32_t i = 0; i < component_count; i++) {
                const uint32_t extract_id = module_.TakeNextId();
                block.CreateInstruction(spv::OpCompositeExtract, {component_type_id, extract_id, argument_id, i}, inst_it);
                CreateFunctionParams(extract_id, *component_type, params, block, inst_it, p_meta);
            }
            break;
        }

        case SpvType::kInt: {
            const uint32_t width = argument_type.meta_.scalar.bit_width;

            // first thing is to get any signed to unsigned via bitcast
            const bool is_signed = argument_type.meta_.scalar.is_signed;
            uint32_t incoming_id = argument_id;
            if (is_signed) {
                const uint32_t bitcast_id = module_.TakeNextId();
                const uint32_t unsigned_type_id = type_manager_.GetTypeInt(width, false).Id();
                block.CreateInstruction(spv::OpBitcast, {unsigned_type_id, bitcast_id, argument_id}, inst_it);
                incoming_id = bitcast_id;

                if (width == 8) {
                    if (p_meta.expanded_parameter_count > 31) {
                        module_.InternalWarning("DEBUG-PRINTF-SIGNED-8-MASK",
                                                "More than 32 expanded parameters, can't properly detect 8-bit signed ints [Simple "
                                                "fix is to turn long printf() into 2 shorter printf() calls]");
                    } else {
                        p_meta.signed_8_bitmask |= 1 << p_meta.expanded_parameter_count;
                    }
                } else if (width == 16) {
                    if (p_meta.expanded_parameter_count > 31) {
                        module_.InternalWarning("DEBUG-PRINTF-SIGNED-16-MASK",
                                                "More than 32 expanded parameters, can't properly detect 16-bit signed ints "
                                                "[Simple fix is to turn long printf() into 2 shorter printf() calls]");
                    } else {
                        p_meta.signed_16_bitmask |= 1 << p_meta.expanded_parameter_count;
                    }
                }
            }

            if (width == 8 || width == 16) {
                const uint32_t uconvert_id = module_.TakeNextId();
                block.CreateInstruction(spv::OpUConvert, {uint32_type_id, uconvert_id, incoming_id}, inst_it);
                params.push_back(uconvert_id);
                p_meta.expanded_parameter_count++;
            } else if (width == 32) {
                params.push_back(incoming_id);
                p_meta.expanded_parameter_count++;
            } else if (width == 64) {
                const uint32_t uconvert_high_id = module_.TakeNextId();
                block.CreateInstruction(spv::OpUConvert, {uint32_type_id, uconvert_high_id, incoming_id}, inst_it);
                params.push_back(uconvert_high_id);
                p_meta.expanded_parameter_count++;

                const uint32_t uint64_type_id = type_manager_.GetTypeInt(64, false).Id();
                const uint32_t shift_right_id = module_.TakeNextId();
                const uint32_t constant_32_id = type_manager_.GetConstantUInt32(32).Id();
                block.CreateInstruction(spv::OpShiftRightLogical, {uint64_type_id, shift_right_id, incoming_id, constant_32_id},
                                        inst_it);

                const uint32_t uconvert_low_id = module_.TakeNextId();
                block.CreateInstruction(spv::OpUConvert, {uint32_type_id, uconvert_low_id, shift_right_id}, inst_it);
                params.push_back(uconvert_low_id);
            } else {
                module_.InternalError(Name(), "CreateFunctionParams has unsupported for int width");
            }
            break;
        }

        case SpvType::kFloat: {
            const uint32_t width = argument_type.meta_.scalar.bit_width;
            if (width == 16) {
                const uint32_t float32_type_id = type_manager_.GetTypeFloat(32).Id();
                const uint32_t fconvert_id = module_.TakeNextId();
                block.CreateInstruction(spv::OpFConvert, {float32_type_id, fconvert_id, argument_id}, inst_it);

                const uint32_t bitcast_id = module_.TakeNextId();
                block.CreateInstruction(spv::OpBitcast, {uint32_type_id, bitcast_id, fconvert_id}, inst_it);
                params.push_back(bitcast_id);
                p_meta.expanded_parameter_count++;
            } else if (width == 32) {
                const uint32_t bitcast_id = module_.TakeNextId();
                block.CreateInstruction(spv::OpBitcast, {uint32_type_id, bitcast_id, argument_id}, inst_it);
                params.push_back(bitcast_id);
                p_meta.expanded_parameter_count++;
            } else if (width == 64) {
                if (p_meta.expanded_parameter_count > 31) {
                    // It is very unlikely to hit this
                    module_.InternalWarning("DEBUG-PRINTF-DOUBLE-MASK",
                                            "More than 32 expanded parameters, can't properly detect 64-bit float [Simple fix is "
                                            "to turn long printf() into 2 shorter printf() calls]");
                } else {
                    p_meta.double_bitmask |= 1 << p_meta.expanded_parameter_count;
                }

                if (!module_.settings_.enabled_features->shaderInt64) {
                    module_.InternalError(
                        "DEBUG-PRINTF-INT64-SUPPORT",
                        "shaderInt64 feature is not supported, but is required to cast a 64-bit float to a 64-bit int "
                        "when writing to the output buffer");
                }
                module_.AddCapability(spv::CapabilityInt64);

                const uint32_t uint64_type_id = type_manager_.GetTypeInt(64, false).Id();
                const uint32_t bitcast_id = module_.TakeNextId();
                block.CreateInstruction(spv::OpBitcast, {uint64_type_id, bitcast_id, argument_id}, inst_it);

                const uint32_t uconvert_high_id = module_.TakeNextId();
                block.CreateInstruction(spv::OpUConvert, {uint32_type_id, uconvert_high_id, bitcast_id}, inst_it);
                params.push_back(uconvert_high_id);
                p_meta.expanded_parameter_count++;

                const uint32_t shift_right_id = module_.TakeNextId();
                const uint32_t constant_32_id = type_manager_.GetConstantUInt32(32).Id();
                block.CreateInstruction(spv::OpShiftRightLogical, {uint64_type_id, shift_right_id, bitcast_id, constant_32_id},
                                        inst_it);

                const uint32_t uconvert_low_id = module_.TakeNextId();
                block.CreateInstruction(spv::OpUConvert, {uint32_type_id, uconvert_low_id, shift_right_id}, inst_it);
                params.push_back(uconvert_low_id);
            } else {
                module_.InternalError(Name(), "CreateFunctionParams has unsupported for float width");
            }
            break;
        }

        case SpvType::kBool: {
            // cast to uint32_t via an OpSelect
            const uint32_t zero_id = type_manager_.GetConstantZeroUint32().Id();
            const uint32_t one_id = type_manager_.GetConstantOneUint32().Id();
            const uint32_t select_id = module_.TakeNextId();
            block.CreateInstruction(spv::OpSelect, {uint32_type_id, select_id, argument_id, one_id, zero_id}, inst_it);
            params.push_back(select_id);
            p_meta.expanded_parameter_count++;
            break;
        }

        case SpvType::kPointer: {
            // Cast to a uvec2 first to avoid needing a Int64
            const uint32_t uvec2_type_id = type_manager_.GetTypeVector(uint32_type, 2).Id();
            const uint32_t bitcast_id = module_.TakeNextId();
            block.CreateInstruction(spv::OpBitcast, {uvec2_type_id, bitcast_id, argument_id}, inst_it);

            const uint32_t extract_id_0 = module_.TakeNextId();
            block.CreateInstruction(spv::OpCompositeExtract, {uint32_type_id, extract_id_0, bitcast_id, 0}, inst_it);
            params.push_back(extract_id_0);
            p_meta.expanded_parameter_count++;

            const uint32_t extract_id_1 = module_.TakeNextId();
            block.CreateInstruction(spv::OpCompositeExtract, {uint32_type_id, extract_id_1, bitcast_id, 1}, inst_it);
            params.push_back(extract_id_1);
            p_meta.expanded_parameter_count++;
            break;
        }

        default:
            module_.InternalError(Name(), "CreateFunctionParams has unsupported function param type");
            break;
    }
}

void DebugPrintfPass::CreateFunctionCall(BasicBlock& block, InstructionIt* inst_it, const InstructionMeta& meta) {
    Function& block_func = *block.function_;
    // need to call to get the underlying 4 IDs (simpler to pass in as 4 uint then a uvec4)
    GetStageInfo(block_func, block, *inst_it);

    const uint32_t inst_position = meta.target_instruction->GetPositionOffset();
    auto inst_position_constant = type_manager_.CreateConstantUInt32(inst_position);

    const uint32_t string_id = meta.target_instruction->Word(5);
    auto string_id_constant = type_manager_.CreateConstantUInt32(string_id);

    const uint32_t void_type = type_manager_.GetTypeVoid().Id();
    const uint32_t function_result = module_.TakeNextId();

    // We know the first part, then build up the rest from the printf arguments
    // except a few slots, we place hold it with zero until we build up the params
    const size_t function_def_slot = 2;
    const size_t double_bitmask_slot = 9;
    const size_t signed_8_bitmask_slot = 10;
    const size_t signed_16_bitmask_slot = 11;
    std::vector<uint32_t> function_call_params = {
        void_type,
        function_result,
        0,  // function_def_slot
        inst_position_constant.Id(),
        block_func.stage_info_x_id_,  // shader stage
        block_func.stage_info_y_id_,
        block_func.stage_info_z_id_,
        block_func.stage_info_w_id_,
        string_id_constant.Id(),
        0,  // double_bitmask_slot,
        0,  // signed_8_bitmask_slot,
        0,  // signed_16_bitmask_slot,
    };

    ParamMeta param_meta;
    // where we find the first arugment in OpExtInst instruction
    const uint32_t first_argument_offset = 6;
    const uint32_t argument_count = meta.target_instruction->Length() - first_argument_offset;
    for (uint32_t i = 0; i < argument_count; i++) {
        const uint32_t argument_id = meta.target_instruction->Word(first_argument_offset + i);
        const Instruction* argument_inst = nullptr;
        const Constant* constant = type_manager_.FindConstantById(argument_id);
        if (constant) {
            argument_inst = &constant->inst_;
        } else {
            argument_inst = block_func.FindInstruction(argument_id);
        }
        assert(argument_inst);  // argument is either constant or found within function block

        const Type* argument_type = type_manager_.FindTypeById(argument_inst->TypeId());
        assert(argument_type);  // type needs to have been declared already

        CreateFunctionParams(argument_inst->ResultId(), *argument_type, function_call_params, block, inst_it, param_meta);
    }

    // 3 params are the [result, function type, and function ID]
    const uint32_t ignored_params = 3;
    const uint32_t param_count = (uint32_t)function_call_params.size() - ignored_params;
    const uint32_t function_def = GetLinkFunctionId(param_count);

    // patch in params
    function_call_params[function_def_slot] = function_def;
    function_call_params[double_bitmask_slot] = type_manager_.GetConstantUInt32(param_meta.double_bitmask).Id();
    function_call_params[signed_8_bitmask_slot] = type_manager_.GetConstantUInt32(param_meta.signed_8_bitmask).Id();
    function_call_params[signed_16_bitmask_slot] = type_manager_.GetConstantUInt32(param_meta.signed_16_bitmask).Id();

    block.CreateInstruction(spv::OpFunctionCall, function_call_params, inst_it);
}

uint32_t DebugPrintfPass::CreateDescriptorSet() {
    // Create descriptor set to match output buffer
    // The following is what the GLSL would look like
    //
    // layout(set = kSet, binding = kBinding, std430) buffer SSBO {
    //     uint written_count;
    //     uint data[];
    // } output_buffer;

    const Type& uint32_type = type_manager_.GetTypeInt(32, false);
    const uint32_t runtime_array_type_id = type_manager_.GetTypeRuntimeArray(uint32_type).Id();

    // if 2 OpTypeRuntimeArray are combined, we can't have ArrayStride twice
    bool has_array_stride = false;
    for (auto& inst : module_.annotations_) {
        if (inst->Opcode() == spv::OpDecorate && inst->Word(1) == runtime_array_type_id &&
            inst->Word(2) == spv::DecorationArrayStride) {
            has_array_stride = true;
            break;
        }
    }
    if (!has_array_stride) {
        module_.AddDecoration(runtime_array_type_id, spv::DecorationArrayStride, {4});
    }

    const uint32_t struct_type_id = module_.TakeNextId();
    auto new_struct_inst = std::make_unique<Instruction>(4, spv::OpTypeStruct);
    new_struct_inst->Fill({struct_type_id, uint32_type.Id(), runtime_array_type_id});
    const Type& struct_type = type_manager_.AddType(std::move(new_struct_inst), SpvType::kStruct);
    module_.AddDecoration(struct_type_id, spv::DecorationBlock, {});
    module_.AddMemberDecoration(struct_type_id, gpuav::kDebugPrintf_OutputBuffer_DWordsCount, spv::DecorationOffset, {0});
    module_.AddMemberDecoration(struct_type_id, gpuav::kDebugPrintf_OutputBuffer_Data, spv::DecorationOffset, {4});

    // create a storage buffer interface variable
    const Type& pointer_type = type_manager_.GetTypePointer(spv::StorageClassStorageBuffer, struct_type);
    const uint32_t output_buffer_variable_id = module_.TakeNextId();
    auto new_inst = std::make_unique<Instruction>(4, spv::OpVariable);
    new_inst->Fill({pointer_type.Id(), output_buffer_variable_id, spv::StorageClassStorageBuffer});
    type_manager_.AddVariable(std::move(new_inst), pointer_type);
    module_.AddInterfaceVariables(output_buffer_variable_id, spv::StorageClassStorageBuffer);

    module_.AddDecoration(output_buffer_variable_id, spv::DecorationDescriptorSet,
                          {module_.settings_.output_buffer_descriptor_set});
    module_.AddDecoration(output_buffer_variable_id, spv::DecorationBinding, {binding_slot_});

    return output_buffer_variable_id;
}

void DebugPrintfPass::CreateBufferWriteFunction(uint32_t argument_count, uint32_t function_id, uint32_t output_buffer_variable_id) {
    // Currently this is generated by the number of arguments
    // The following is what the GLSL would look like
    //
    // void inst_debug_printf_5(uint a, uint b, uint c) {
    //     uint offset = atomicAdd(output_buffer.written_count, 5);
    //     if ((offset + 5) <= uint(output_buffer.data.length())) {
    //         output_buffer.data[offset + 0] = 5; // bytes of buffer
    //         output_buffer.data[offset + 1] = stage_id; // known and not passed in
    //         output_buffer.data[offset + 2] = a;
    //         output_buffer.data[offset + 3] = b;
    //         output_buffer.data[offset + 4] = c;
    //     }
    // }

    // + 1 byte to write the "how many bytes will there be"
    // + 1 byte for the shader stage (which we don't pass in as we know already)
    // - 1 byte as we combine the instruction offset and stage type
    const uint32_t byte_written = argument_count + 1;

    // Debug name is matching number of bytes written into the buffer
    std::string function_name = "inst_debug_printf_" + std::to_string(byte_written);
    module_.AddDebugName(function_name.c_str(), function_id);

    // Need to create the function type
    const uint32_t function_type_id = module_.TakeNextId();
    const uint32_t void_type_id = type_manager_.GetTypeVoid().Id();
    const uint32_t uint32_type_id = type_manager_.GetTypeInt(32, false).Id();
    {
        std::vector<uint32_t> words = {function_type_id, void_type_id};
        for (size_t i = 0; i < argument_count; i++) {
            words.push_back(uint32_type_id);
        }
        auto new_inst = std::make_unique<Instruction>((uint32_t)words.size() + 1, spv::OpTypeFunction);
        new_inst->Fill(words);
        type_manager_.AddType(std::move(new_inst), SpvType::kFunction);
    }

    // We could be doing this at linking time, but since this is the last pass, can add here
    Function& new_function = module_.functions_.emplace_back(module_);
    std::vector<uint32_t> function_param_ids;
    {
        auto new_inst = std::make_unique<Instruction>(5, spv::OpFunction);
        new_inst->Fill({void_type_id, function_id, spv::FunctionControlMaskNone, function_type_id});
        new_function.pre_block_inst_.emplace_back(std::move(new_inst));

        for (size_t i = 0; i < argument_count; i++) {
            const uint32_t new_id = module_.TakeNextId();
            auto param_inst = std::make_unique<Instruction>(3, spv::OpFunctionParameter);
            param_inst->Fill({uint32_type_id, new_id});
            new_function.pre_block_inst_.emplace_back(std::move(param_inst));
            function_param_ids.push_back(new_id);
        }
    }

    BasicBlock& check_block = new_function.InsertNewBlockEnd();
    BasicBlock& store_block = new_function.InsertNewBlockEnd();
    BasicBlock& merge_block = new_function.InsertNewBlockEnd();

    const Type& uint32_type = type_manager_.GetTypeInt(32, false);
    const uint32_t pointer_type_id = type_manager_.GetTypePointer(spv::StorageClassStorageBuffer, uint32_type).Id();
    const uint32_t zero_id = type_manager_.GetConstantZeroUint32().Id();
    const uint32_t one_id = type_manager_.GetConstantOneUint32().Id();
    const uint32_t byte_written_id = type_manager_.GetConstantUInt32(byte_written).Id();
    uint32_t atomic_add_id = 0;

    // Atomically get a write index in the output buffer, and check if this index is with buffer's bounds
    {
        const uint32_t access_chain_id = module_.TakeNextId();
        check_block.CreateInstruction(spv::OpAccessChain, {pointer_type_id, access_chain_id, output_buffer_variable_id, zero_id});

        atomic_add_id = module_.TakeNextId();
        const uint32_t scope_invok_id = type_manager_.GetConstantUInt32(spv::ScopeInvocation).Id();
        const uint32_t mask_none_id = type_manager_.GetConstantUInt32(spv::MemoryAccessMaskNone).Id();
        check_block.CreateInstruction(
            spv::OpAtomicIAdd, {uint32_type_id, atomic_add_id, access_chain_id, scope_invok_id, mask_none_id, byte_written_id});

        const uint32_t int_add_id = module_.TakeNextId();
        check_block.CreateInstruction(spv::OpIAdd, {uint32_type_id, int_add_id, atomic_add_id, byte_written_id});

        // We hardcode the array length because
        //   1. We know it and its a bit faster than grabbing the OpArrayLength
        //   2. It allows us to use VK_DESCRIPTOR_MAPPING_SOURCE_INDIRECT_ADDRESS_EXT
        // We read the array as a uint[] so need to ceil up to 4 bytes (as OpArrayLength does as well)
        const uint32_t array_length = (module_.settings_.debug_printf_buffer_size + 3) / 4;
        const uint32_t array_length_id = type_manager_.GetConstantUInt32(array_length).Id();

        const uint32_t less_than_equal_id = module_.TakeNextId();
        const uint32_t bool_type_id = type_manager_.GetTypeBool().Id();
        check_block.CreateInstruction(spv::OpULessThanEqual, {bool_type_id, less_than_equal_id, int_add_id, array_length_id});

        const uint32_t merge_block_label_id = merge_block.GetLabelId();
        check_block.CreateInstruction(spv::OpSelectionMerge, {merge_block_label_id, spv::SelectionControlMaskNone});

        const uint32_t store_block_label_id = store_block.GetLabelId();
        check_block.CreateInstruction(spv::OpBranchConditional, {less_than_equal_id, store_block_label_id, merge_block_label_id});
    }

    // Store how many 32-bit words
    {
        const uint32_t int_add_id = module_.TakeNextId();
        store_block.CreateInstruction(spv::OpIAdd, {uint32_type_id, int_add_id, atomic_add_id, zero_id});

        const uint32_t access_chain_id = module_.TakeNextId();
        store_block.CreateInstruction(spv::OpAccessChain,
                                      {pointer_type_id, access_chain_id, output_buffer_variable_id, one_id, int_add_id});

        store_block.CreateInstruction(spv::OpStore, {access_chain_id, byte_written_id});
    }

    // Store Shader Stage ID
    {
        const uint32_t int_add_id = module_.TakeNextId();
        store_block.CreateInstruction(spv::OpIAdd, {uint32_type_id, int_add_id, atomic_add_id, one_id});

        const uint32_t access_chain_id = module_.TakeNextId();
        store_block.CreateInstruction(spv::OpAccessChain,
                                      {pointer_type_id, access_chain_id, output_buffer_variable_id, one_id, int_add_id});

        const uint32_t shader_id = type_manager_.GetConstantUInt32(module_.interface_.unique_shader_id).Id();
        store_block.CreateInstruction(spv::OpStore, {access_chain_id, shader_id});
    }

    // Store Instruction Position Offset + Stage Type
    {
        const uint32_t two_id = type_manager_.GetConstantUInt32(2).Id();
        const uint32_t int_add_id = module_.TakeNextId();
        store_block.CreateInstruction(spv::OpIAdd, {uint32_type_id, int_add_id, atomic_add_id, two_id});

        const uint32_t access_chain_id = module_.TakeNextId();
        store_block.CreateInstruction(spv::OpAccessChain,
                                      {pointer_type_id, access_chain_id, output_buffer_variable_id, one_id, int_add_id});

        // Matches the code
        //    error_payload.inst_offset | (stage_info.x << kStageId_Shift);
        // found in log_error.comp
        const uint32_t shift_id = type_manager_.GetConstantUInt32(glsl::kStageId_Shift).Id();
        const uint32_t shift_left_id = module_.TakeNextId();
        const uint32_t stage_info_x = function_param_ids[1];
        store_block.CreateInstruction(spv::OpShiftLeftLogical, {uint32_type_id, shift_left_id, stage_info_x, shift_id});

        const uint32_t bitwise_or_id = module_.TakeNextId();
        const uint32_t instruction_position_offset = function_param_ids[0];
        store_block.CreateInstruction(spv::OpBitwiseOr,
                                      {uint32_type_id, bitwise_or_id, instruction_position_offset, shift_left_id});

        store_block.CreateInstruction(spv::OpStore, {access_chain_id, bitwise_or_id});
    }

    // Write a 32-bit word to the output buffer for each argument.
    // Skip [0] and [1] as they set above already
    for (uint32_t i = 2; i < argument_count; i++) {
        const uint32_t int_add_id = module_.TakeNextId();
        // Offset by 1 such that the first in this loop is at [offset + 3]
        const uint32_t offset_id = type_manager_.GetConstantUInt32(i + 1).Id();
        store_block.CreateInstruction(spv::OpIAdd, {uint32_type_id, int_add_id, atomic_add_id, offset_id});

        const uint32_t access_chain_id = module_.TakeNextId();
        store_block.CreateInstruction(spv::OpAccessChain,
                                      {pointer_type_id, access_chain_id, output_buffer_variable_id, one_id, int_add_id});

        store_block.CreateInstruction(spv::OpStore, {access_chain_id, function_param_ids[i]});
    }

    // merge block of the above if() check
    {
        store_block.CreateInstruction(spv::OpBranch, {merge_block.GetLabelId()});
        merge_block.CreateInstruction(spv::OpReturn, {});
    }

    {
        auto new_inst = std::make_unique<Instruction>(1, spv::OpFunctionEnd);
        new_function.post_block_inst_.emplace_back(std::move(new_inst));
    }
}

bool DebugPrintfPass::Instrument() {
    for (const auto& inst : module_.ext_inst_imports_) {
        const char* import_string = inst->GetAsString(2);
        if (strcmp(import_string, "NonSemantic.DebugPrintf") == 0) {
            ext_import_id_ = inst->ResultId();
            break;
        }
    }

    if (ext_import_id_ == 0) {
        return false;  // no printf strings found, early return
    }

    for (Function& function : module_.functions_) {
        if (!function.called_from_target_ && !function.AddedFromInstrumentation()) {
            continue;
        }

        for (auto block_it = function.blocks_.begin(); block_it != function.blocks_.end(); ++block_it) {
            BasicBlock& current_block = **block_it;

            // This is a silly hack, sorry
            // We use to only add the |functions_| at parsing and linking.
            // But in order to run debug printf on our own GLSL, we do this pass AFTER linking, which means the |functions_| list
            // could have been expanded and now the |function_| reference in |block| is dangling, but just setting it here, before
            // potentially updating the block is a simple-enough-fix
            current_block.function_ = &function;

            cf_.Update(current_block);
            if (debug_disable_loops_ && cf_.in_loop) continue;

            auto& block_instructions = current_block.instructions_;
            for (auto inst_it = block_instructions.begin(); inst_it != block_instructions.end(); ++inst_it) {
                InstructionMeta meta;
                if (!RequiresInstrumentation(*(inst_it->get()), meta)) continue;
                if (!Validate(function, meta)) continue;  // if not valid, don't attempt to instrument it
                instrumentations_count_++;

                // Save the OpString here so we can use it later
                if (function.AddedFromInstrumentation()) {
                    for (const auto& debug_inst : module_.debug_source_) {
                        const uint32_t string_id = (*inst_it)->Word(5);
                        if (debug_inst->Opcode() == spv::OpString && debug_inst->ResultId() == string_id) {
                            internal_only_debug_printf_.emplace_back(InternalOnlyDebugPrintf{
                                module_.interface_.unique_shader_id, string_id, debug_inst->GetAsString(2)});
                        }
                    }
                }

                CreateFunctionCall(current_block, &inst_it, meta);

                // remove the OpExtInst incase they don't support VK_KHR_non_semantic_info
                if (!module_.settings_.support_non_semantic_info) {
                    inst_it = block_instructions.erase(inst_it);
                    inst_it--;
                }
            }
        }
    }
    return instrumentations_count_ != 0;
}

void DebugPrintfPass::PostProcess() {
    const uint32_t output_buffer_variable_id = CreateDescriptorSet();

    // Here we "link" the functions, but since it is all generated, no need to go through the LinkInfo flow
    for (const auto& [number_of_args, function_id] : function_id_map_) {
        CreateBufferWriteFunction(number_of_args, function_id, output_buffer_variable_id);
    }

    // remove the everything else possible incase they don't support VK_KHR_non_semantic_info
    if (!module_.settings_.support_non_semantic_info) {
        bool other_non_semantic = false;
        for (auto inst_it = module_.ext_inst_imports_.begin(); inst_it != module_.ext_inst_imports_.end(); ++inst_it) {
            const char* import_string = (inst_it->get())->GetAsString(2);
            if (strcmp(import_string, "NonSemantic.DebugPrintf") == 0) {
                module_.ext_inst_imports_.erase(inst_it);
                break;
            } else if (strncmp(import_string, "NonSemantic.", 12) == 0) {
                other_non_semantic = true;
            }
        }
        if (!other_non_semantic) {
            for (auto inst_it = module_.extensions_.begin(); inst_it != module_.extensions_.end(); ++inst_it) {
                const char* import_string = (inst_it->get())->GetAsString(1);
                if (strcmp(import_string, "SPV_KHR_non_semantic_info") == 0) {
                    module_.extensions_.erase(inst_it);
                    break;
                }
            }
        }
    }
}

void DebugPrintfPass::PrintDebugInfo() const {
    std::cout << "DebugPrintfPass instrumentation count: " << instrumentations_count_ << '\n';
}

// Strictly speaking - the format given in GLSL_EXT_debug_printf is a client side implementation of SPIR-V
// NonSemantic.DebugPrintf There is nothing stopping someone from creating a debug printf implementation
// that goes `printf("Use this &q to print int", myInt)` but this requires both having
// a different HLL and Tool consuming it.
// Currently RenderDoc and the Validation Layers both follow the same syntax, but that also could possibly change.
// Therefore, we validate these here based on the VVL implementation only
bool DebugPrintfPass::Validate(const Function& current_function, const InstructionMeta& meta) {
    static const char* tag = "DEBUG-PRINTF-FORMATTING";

    struct ParamInfo {
        bool is_float = false;  // else int (don't attempt to validate unsigned vs signed here)
        bool is_64_bit = false;
        bool is_pointer = false;
        uint32_t vector_size = 0;  // zero == scalar
        char modifier[32];
    };

    // where we find the first arugment in OpExtInst instruction
    const uint32_t first_argument_offset = 6;

    if (meta.target_instruction->Length() < first_argument_offset) {
        module_.InternalError(tag, "OpExtInst in a invalid SPIR-V format and should have been caught in spirv-val");
        return false;
    }

    uint32_t string_id = meta.target_instruction->Word(5);
    const char* op_string = nullptr;
    for (const auto& inst : module_.debug_source_) {
        if (inst->Opcode() == spv::OpString && inst->ResultId() == string_id) {
            op_string = inst->GetAsString(2);
            break;
        }
    }
    if (!op_string) {
        module_.InternalError(tag, "OpExtInst points to an empty/invalid OpString, this should have been caught in spirv-val");
        return false;
    }

    const size_t op_string_len = strlen(op_string);
    if (op_string_len == 0) {
        module_.InternalError(tag, "OpString is empty (string was found, but is empty)");
        return false;
    }

    // If we are going to print the OpString to the user for an error/warning, we need to process it first
    auto print_op_string = [&op_string]() {
        // If there is a '\n' we want to print it like we see in the shader, so need to escape the backslash
        std::string result;
        for (const char* p = op_string; *p != '\0'; ++p) {
            if (*p == '\n') {
                result += "\\n";
            } else if (*p == '\t') {
                result += "\\t";
            } else {
                result += *p;
            }
        }
        return result;
    };

    bool valid = true;
    std::vector<ParamInfo> param_infos;

    // No reason to start checking at the last character, since always need % and something following it
    for (size_t i = 0; i < op_string_len - 1; i++) {
        if (op_string[i] != '%') continue;
        const size_t starting_i = i;
        i++;
        char modifier = op_string[i];
        if (modifier == '%') continue;  // skip "%%"

        if (modifier == ' ') {
            std::string err_msg =
                "OpString \"" + print_op_string() + "\" contains a isolated % which is missing the modifier (to escape use %%)";
            module_.InternalError(tag, err_msg);
            valid = false;
            break;
        }

        bool found_specifier = false;
        ParamInfo param_info;
        while (i < op_string_len && modifier != ' ' && valid && !found_specifier) {
            switch (modifier) {
                case 'i':
                case 'd':
                case 'o':
                case 'X':
                case 'x':
                    found_specifier = true;
                    break;
                case 'u':
                    if (i + 1 < op_string_len && op_string[i + 1] == 'l') {
                        param_info.is_64_bit = true;
                    }
                    found_specifier = true;
                    break;
                case 'p':
                    param_info.is_pointer = true;
                    found_specifier = true;
                    break;
                case 'a':
                case 'A':
                case 'e':
                case 'E':
                case 'f':
                case 'F':
                case 'g':
                case 'G':
                    found_specifier = true;
                    param_info.is_float = true;
                    break;
                case 'l':
                    param_info.is_64_bit = true;
                    break;
                case '0':
                case '1':
                case '2':
                case '3':
                case '4':
                case '5':
                case '6':
                case '7':
                case '8':
                case '9':
                case '*':
                case '.':
                    break;  // expected for precision
                case 'v': {
                    if (i + 1 >= op_string_len) {
                        std::string err_msg = "OpString \"" + print_op_string() +
                                              "\" contains a %v at the end, but vectors require a width and type after it";
                        module_.InternalError(tag, err_msg);
                        valid = false;
                    } else {
                        i++;
                        const char vec_size = op_string[i];
                        if (vec_size == '2') {
                            param_info.vector_size = 2;
                        } else if (vec_size == '3') {
                            param_info.vector_size = 3;
                        } else if (vec_size == '4') {
                            param_info.vector_size = 4;
                        } else {
                            std::string err_msg = "OpString \"" + print_op_string() + "\" contains a %v" + vec_size +
                                                  " needs to be valid vector width (v2, v3, or v4)";
                            module_.InternalError(tag, err_msg);
                            valid = false;
                        }
                    }
                    break;
                }
                default:
                    // Need to escape, other error makes no sense
                    std::string err_modifier = (modifier == '\n') ? "\\n" : (modifier == '\t') ? "\\t" : std::to_string(modifier);
                    std::string err_msg = "OpString \"" + print_op_string() + "\" contains a \"" + err_modifier +
                                          "\" modifier which is an unknown modifier.";
                    if (param_info.vector_size > 0) {
                        // Help explain our custom vector format rules
                        err_msg += " (for vectors you need something like %v3f which is just %f with a special v3 prefix)";
                    }
                    module_.InternalError(tag, err_msg);
                    valid = false;
                    break;  // unknown
            };

            i++;
            modifier = op_string[i];
        }

        if (valid) {
            // Get for other error messages
            strncpy(param_info.modifier, &op_string[starting_i], i - starting_i);
            param_info.modifier[i - starting_i] = '\0';

            if (!found_specifier) {
                std::string err_msg = "OpString \"" + print_op_string() + "\" contains \"" + std::string(param_info.modifier) +
                                      "\" which is missing a valid specifier (d, i, o, u, x, X, a, A, e, E, f, F, g, or G).";
                module_.InternalError(tag, err_msg);
                valid = false;
            }
        }

        if (!valid) break;
        param_infos.push_back(param_info);
    }
    if (!valid) return false;

    const uint32_t argument_count = meta.target_instruction->Length() - first_argument_offset;
    if (argument_count > param_infos.size()) {
        std::string err_msg = "OpString \"" + print_op_string() + "\" contains only " + std::to_string(param_infos.size()) +
                              " modifiers, but " + std::to_string(argument_count) +
                              " arguments were passed in and some will be ignored";
        module_.InternalWarning(tag, err_msg);

    } else if (argument_count < param_infos.size()) {
        std::string err_msg = "OpString \"" + print_op_string() + "\" contains " + std::to_string(param_infos.size()) +
                              " modifiers, but only " + std::to_string(argument_count) +
                              " arguments were passed in and garbage data might start to occur";
        module_.InternalError(tag, err_msg);
        return false;
    }

    const uint32_t count = std::min(argument_count, (uint32_t)param_infos.size());
    for (uint32_t i = 0; i < count; i++) {
        const ParamInfo& param = param_infos[i];
        const uint32_t argument_id = meta.target_instruction->Word(first_argument_offset + i);

        const Type* argument_type = type_manager_.FindTypeGlobal(current_function, argument_id);
        if (!argument_type) {
            module_.InternalWarning(tag, "Unable find OpExtInst ID type");
            return true;  // possibily our error, so leave a warning
        }

        // first strip/validate vectors
        if (param.vector_size != 0) {
            if (argument_type->spv_type_ != SpvType::kVector) {
                std::string err_msg = "OpString \"" + print_op_string() + "\" contains a vector modifier \"" + param.modifier +
                                      "\", but the argument (SPIR-V Id " + std::to_string(argument_id) + ") is not a vector";
                module_.InternalError(tag, err_msg);
                return false;
            }
            const uint32_t vector_size = argument_type->inst_.Word(3);
            if (vector_size != param.vector_size) {
                std::string err_msg = "OpString \"" + print_op_string() + "\" contains a " + std::to_string(param.vector_size) +
                                      "-wide vector modifier \"" + param.modifier + "\", but the argument (SPIR-V Id " +
                                      std::to_string(argument_id) + ") is a " + std::to_string(vector_size) +
                                      "-wide vector (values might be truncated or padded)";
                module_.InternalWarning(tag, err_msg);
            }

            // Get the underlying type (float or int)
            argument_type = type_manager_.FindTypeById(argument_type->inst_.Word(2));
            assert(argument_type);
        } else {
            if (argument_type->spv_type_ == SpvType::kVector) {
                std::string err_msg = "OpString \"" + print_op_string() + "\" contains a non-vector modifier \"" + param.modifier +
                                      "\", but the argument (SPIR-V Id " + std::to_string(argument_id) + ") is a vector";
                module_.InternalError(tag, err_msg);
                return false;
            }
        }

        const bool is_pointer_type = argument_type->spv_type_ == SpvType::kPointer &&
                                     (argument_type->inst_.StorageClass() == spv::StorageClassPhysicalStorageBuffer);

        // this is after stripping the vector
        // Pointers are handled by themselves
        if (argument_type->spv_type_ != SpvType::kFloat && argument_type->spv_type_ != SpvType::kInt &&
            argument_type->spv_type_ != SpvType::kBool && !is_pointer_type && !param.is_pointer) {
            std::string err_msg = "OpString \"" + print_op_string() + "\" contains a modifier \"" + param.modifier +
                                  "\", but the argument (SPIR-V Id " + std::to_string(argument_id) +
                                  ") is not a float, int, or bool";
            module_.InternalError(tag, err_msg);
            return false;
        }

        const bool type_is_64 = argument_type->spv_type_ != SpvType::kBool && argument_type->inst_.Word(2) == 64;
        // Do Pointer errors first for better message if it is related to a badly formatted pointer
        if (!param.is_pointer && is_pointer_type) {
            std::string err_msg = "OpString \"" + print_op_string() + "\" contains a non-pointer modifier \"" + param.modifier +
                                  "\", but the argument (SPIR-V Id " + std::to_string(argument_id) +
                                  ") is a pointer and should use %p instead";
            module_.InternalError(tag, err_msg);
        } else if (param.is_pointer && !is_pointer_type) {
            std::string err_msg = "OpString \"" + print_op_string() + "\" contains a pointer modifier \"" + param.modifier +
                                  "\", but the argument (SPIR-V Id " + std::to_string(argument_id) + ") is not a pointer";
            module_.InternalError(tag, err_msg);
        } else if (!param.is_64_bit && type_is_64 && !param.is_pointer) {
            std::string err_msg = "OpString \"" + print_op_string() + "\" contains a non-64-bit modifier \"" + param.modifier +
                                  "\", but the argument (SPIR-V Id " + std::to_string(argument_id) + ") a 64-bit";
            module_.InternalWarning(tag, err_msg);
        } else if (param.is_64_bit && !type_is_64 && !is_pointer_type) {
            std::string err_msg = "OpString \"" + print_op_string() + "\" contains a 64-bit modifier \"" + param.modifier +
                                  "\", but the argument (SPIR-V Id " + std::to_string(argument_id) + ") is not 64-bit";
            module_.InternalWarning(tag, err_msg);
        } else if (!param.is_float && argument_type->spv_type_ == SpvType::kFloat) {
            std::string err_msg = "OpString \"" + print_op_string() + "\" contains a non-float modifier \"" + param.modifier +
                                  "\", but the argument (SPIR-V Id " + std::to_string(argument_id) + ") is a float";
            module_.InternalWarning(tag, err_msg);
        } else if (param.is_float && argument_type->spv_type_ != SpvType::kFloat) {
            std::string err_msg = "OpString \"" + print_op_string() + "\" contains a float modifier \"" + param.modifier +
                                  "\", but the argument (SPIR-V Id " + std::to_string(argument_id) + ") is not a float";
            module_.InternalWarning(tag, err_msg);
        }
    }

    return true;
}

}  // namespace spirv
}  // namespace gpuav
