//
// Copyright 2019 The ANGLE Project Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//
// GlslangUtils: Wrapper for Khronos's glslang compiler.
//

#include "libANGLE/renderer/metal/mtl_glslang_utils.h"

#include <regex>

#include <spirv_msl.hpp>

#include "common/apple_platform_utils.h"
#include "libANGLE/renderer/ShaderInterfaceVariableInfoMap.h"
#include "libANGLE/renderer/glslang_wrapper_utils.h"
#include "libANGLE/renderer/metal/DisplayMtl.h"

namespace rx
{
namespace mtl
{
namespace
{

constexpr uint32_t kGlslangTextureDescSet              = 0;
constexpr uint32_t kGlslangDefaultUniformAndXfbDescSet = 1;
constexpr uint32_t kGlslangDriverUniformsDescSet       = 2;
constexpr uint32_t kGlslangShaderResourceDescSet       = 3;

// Original mapping of front end from sampler name to multiple sampler slots (in form of
// slot:count pair)
using OriginalSamplerBindingMap =
    angle::HashMap<std::string, std::vector<std::pair<uint32_t, uint32_t>>>;

void ResetGlslangProgramInterfaceInfo(GlslangProgramInterfaceInfo *programInterfaceInfo)
{
    // These are binding options passed to glslang. The actual binding might be changed later
    // by spirv-cross.
    programInterfaceInfo->uniformsAndXfbDescriptorSetIndex  = kGlslangDefaultUniformAndXfbDescSet;
    programInterfaceInfo->currentUniformBindingIndex        = 0;
    programInterfaceInfo->textureDescriptorSetIndex         = kGlslangTextureDescSet;
    programInterfaceInfo->currentTextureBindingIndex        = 0;
    programInterfaceInfo->driverUniformsDescriptorSetIndex  = kGlslangDriverUniformsDescSet;
    programInterfaceInfo->shaderResourceDescriptorSetIndex  = kGlslangShaderResourceDescSet;
    programInterfaceInfo->currentShaderResourceBindingIndex = 0;
    programInterfaceInfo->locationsUsedForXfbExtension      = 0;

    static_assert(kDefaultUniformsBindingIndex != 0, "kDefaultUniformsBindingIndex must not be 0");
    static_assert(kDriverUniformsBindingIndex != 0, "kDriverUniformsBindingIndex must not be 0");
}

GlslangSourceOptions CreateSourceOptions()
{
    GlslangSourceOptions options;
    return options;
}

spv::ExecutionModel ShaderTypeToSpvExecutionModel(gl::ShaderType shaderType)
{
    switch (shaderType)
    {
        case gl::ShaderType::Vertex:
            return spv::ExecutionModelVertex;
        case gl::ShaderType::Fragment:
            return spv::ExecutionModelFragment;
        default:
            UNREACHABLE();
            return spv::ExecutionModelMax;
    }
}

void BindBuffers(spirv_cross::CompilerMSL *compiler,
                 const spirv_cross::SmallVector<spirv_cross::Resource> &resources,
                 gl::ShaderType shaderType,
                 const angle::HashMap<std::string, uint32_t> &uboOriginalBindings,
                 const angle::HashMap<uint32_t, uint32_t> &xfbOriginalBindings,
                 std::array<uint32_t, kMaxGLUBOBindings> *uboBindingsRemapOut,
                 std::array<uint32_t, kMaxShaderXFBs> *xfbBindingRemapOut,
                 bool *uboArgumentBufferUsed)
{
    auto &compilerMsl = *compiler;

    uint32_t totalUniformBufferSlots = 0;
    uint32_t totalXfbSlots           = 0;
    struct UniformBufferVar
    {
        const char *name = nullptr;
        spirv_cross::MSLResourceBinding resBinding;
        uint32_t arraySize;
    };
    std::vector<UniformBufferVar> uniformBufferBindings;

    for (const spirv_cross::Resource &resource : resources)
    {
        spirv_cross::MSLResourceBinding resBinding;
        resBinding.stage = ShaderTypeToSpvExecutionModel(shaderType);

        if (compilerMsl.has_decoration(resource.id, spv::DecorationDescriptorSet))
        {
            resBinding.desc_set =
                compilerMsl.get_decoration(resource.id, spv::DecorationDescriptorSet);
        }

        if (!compilerMsl.has_decoration(resource.id, spv::DecorationBinding))
        {
            continue;
        }

        resBinding.binding = compilerMsl.get_decoration(resource.id, spv::DecorationBinding);

        uint32_t bindingPoint = 0;
        // NOTE(hqle): We use separate discrete binding point for now, in future, we should use
        // one argument buffer for each descriptor set.
        switch (resBinding.desc_set)
        {
            case kGlslangTextureDescSet:
                // Texture binding point is ignored. We let spirv-cross automatically assign it and
                // retrieve it later
                continue;
            case kGlslangDriverUniformsDescSet:
                bindingPoint = mtl::kDriverUniformsBindingIndex;
                break;
            case kGlslangDefaultUniformAndXfbDescSet:
                if (shaderType != gl::ShaderType::Vertex ||
                    !xfbOriginalBindings.count(resBinding.binding))
                {
                    bindingPoint = mtl::kDefaultUniformsBindingIndex;
                }
                else
                {
                    // XFB buffer
                    uint32_t xfbSlot = xfbOriginalBindings.at(resBinding.binding);

                    totalXfbSlots++;
                    // XFB buffer is allocated slot starting from last discrete Metal buffer slot.
                    bindingPoint = kMaxShaderBuffers - 1 - xfbSlot;

                    xfbBindingRemapOut->at(xfbSlot) = bindingPoint;
                }
                break;
            case kGlslangShaderResourceDescSet:
            {
                UniformBufferVar uboVar;
                uboVar.name                       = resource.name.c_str();
                uboVar.resBinding                 = resBinding;
                const spirv_cross::SPIRType &type = compilerMsl.get_type_from_variable(resource.id);
                if (!type.array.empty())
                {
                    uboVar.arraySize = type.array[0];
                }
                else
                {
                    uboVar.arraySize = 1;
                }
                totalUniformBufferSlots += uboVar.arraySize;
                uniformBufferBindings.push_back(uboVar);
            }
                continue;
            default:
                // We don't support this descriptor set.
                continue;
        }

        resBinding.msl_buffer = bindingPoint;

        compilerMsl.add_msl_resource_binding(resBinding);
    }  // for (resources)

    if (totalUniformBufferSlots == 0)
    {
        return;
    }

    // Remap the uniform buffers bindings. glslang allows uniform buffers array to use exactly
    // one slot in the descriptor set. However, metal enforces that the uniform buffers array
    // use (n) slots where n=array size.
    uint32_t currentSlot = 0;
    uint32_t maxUBODiscreteSlots =
        kMaxShaderBuffers - totalXfbSlots - kUBOArgumentBufferBindingIndex;

    if (totalUniformBufferSlots > maxUBODiscreteSlots)
    {
        // If shader uses more than maxUBODiscreteSlots number of UBOs, encode them all into
        // an argument buffer. Each buffer will be assigned [[id(n)]] attribute.
        *uboArgumentBufferUsed = true;
    }
    else
    {
        // Use discrete buffer binding slot for UBOs which translates each slot to [[buffer(n)]]
        *uboArgumentBufferUsed = false;
        // Discrete buffer binding slot starts at kUBOArgumentBufferBindingIndex
        currentSlot += kUBOArgumentBufferBindingIndex;
    }

    for (UniformBufferVar &uboVar : uniformBufferBindings)
    {
        spirv_cross::MSLResourceBinding &resBinding = uboVar.resBinding;
        resBinding.msl_buffer                       = currentSlot;

        uint32_t originalBinding = uboOriginalBindings.at(uboVar.name);

        for (uint32_t i = 0; i < uboVar.arraySize; ++i, ++currentSlot)
        {
            // Use consecutive slot for member in array
            uboBindingsRemapOut->at(originalBinding + i) = currentSlot;
        }

        compilerMsl.add_msl_resource_binding(resBinding);
    }
}

void GetAssignedSamplerBindings(const spirv_cross::CompilerMSL &compilerMsl,
                                const OriginalSamplerBindingMap &originalBindings,
                                std::array<SamplerBinding, mtl::kMaxGLSamplerBindings> *bindings)
{
    for (const spirv_cross::Resource &resource : compilerMsl.get_shader_resources().sampled_images)
    {
        uint32_t descriptorSet = 0;
        if (compilerMsl.has_decoration(resource.id, spv::DecorationDescriptorSet))
        {
            descriptorSet = compilerMsl.get_decoration(resource.id, spv::DecorationDescriptorSet);
        }

        // We already assigned descriptor set 0 to textures. Just to double check.
        ASSERT(descriptorSet == kGlslangTextureDescSet);
        ASSERT(compilerMsl.has_decoration(resource.id, spv::DecorationBinding));

        uint32_t actualTextureSlot = compilerMsl.get_automatic_msl_resource_binding(resource.id);
        uint32_t actualSamplerSlot =
            compilerMsl.get_automatic_msl_resource_binding_secondary(resource.id);

        // Assign sequential index for subsequent array elements
        const std::vector<std::pair<uint32_t, uint32_t>> &resOrignalBindings =
            originalBindings.at(resource.name);
        uint32_t currentTextureSlot = actualTextureSlot;
        uint32_t currentSamplerSlot = actualSamplerSlot;
        for (const std::pair<uint32_t, uint32_t> &originalBindingRange : resOrignalBindings)
        {
            SamplerBinding &actualBinding = bindings->at(originalBindingRange.first);
            actualBinding.textureBinding  = currentTextureSlot;
            actualBinding.samplerBinding  = currentSamplerSlot;

            currentTextureSlot += originalBindingRange.second;
            currentSamplerSlot += originalBindingRange.second;
        }
    }
}

std::string PostProcessTranslatedMsl(const std::string &translatedSource)
{
    // Add function_constant attribute to gl_SampleMask.
    // Even though this varying is only used when ANGLECoverageMaskEnabled is true,
    // the spirv-cross doesn't assign function_constant attribute to it. Thus it won't be dead-code
    // removed when ANGLECoverageMaskEnabled=false.
    std::string sampleMaskReplaceStr = std::string("[[sample_mask, function_constant(") +
                                       sh::mtl::kCoverageMaskEnabledConstName + ")]]";

    // This replaces "gl_SampleMask [[sample_mask]]"
    //          with "gl_SampleMask [[sample_mask, function_constant(ANGLECoverageMaskEnabled)]]"
    std::regex sampleMaskDeclareRegex(R"(\[\s*\[\s*sample_mask\s*\]\s*\])");
    return std::regex_replace(translatedSource, sampleMaskDeclareRegex, sampleMaskReplaceStr);
}

// Customized spirv-cross compiler
class SpirvToMslCompiler : public spirv_cross::CompilerMSL
{
  public:
    SpirvToMslCompiler(angle::spirv::Blob &&spriv) : spirv_cross::CompilerMSL(spriv) {}

    void compileEx(gl::ShaderType shaderType,
                   const angle::HashMap<std::string, uint32_t> &uboOriginalBindings,
                   const angle::HashMap<uint32_t, uint32_t> &xfbOriginalBindings,
                   const OriginalSamplerBindingMap &originalSamplerBindings,
                   bool disableRasterization,
                   TranslatedShaderInfo *mslShaderInfoOut)
    {
        spirv_cross::CompilerMSL::Options compOpt;

#if TARGET_OS_OSX || TARGET_OS_MACCATALYST
        compOpt.platform = spirv_cross::CompilerMSL::Options::macOS;
#else
        compOpt.platform = spirv_cross::CompilerMSL::Options::iOS;
#endif

        if (ANGLE_APPLE_AVAILABLE_XCI(10.14, 13.0, 12))
        {
            // Use Metal 2.1
            compOpt.set_msl_version(2, 1);
        }
        else
        {
            // Always use at least Metal 2.0.
            compOpt.set_msl_version(2);
        }

        compOpt.pad_fragment_output_components = true;
        compOpt.disable_rasterization          = disableRasterization;

        // Tell spirv-cross to map default & driver uniform & storage blocks as we want
        spirv_cross::ShaderResources mslRes = spirv_cross::CompilerMSL::get_shader_resources();

        spirv_cross::SmallVector<spirv_cross::Resource> buffers = std::move(mslRes.uniform_buffers);
        buffers.insert(buffers.end(), mslRes.storage_buffers.begin(), mslRes.storage_buffers.end());

        BindBuffers(this, buffers, shaderType, uboOriginalBindings, xfbOriginalBindings,
                    &mslShaderInfoOut->actualUBOBindings, &mslShaderInfoOut->actualXFBBindings,
                    &mslShaderInfoOut->hasUBOArgumentBuffer);

        if (mslShaderInfoOut->hasUBOArgumentBuffer)
        {
            // Enable argument buffer.
            compOpt.argument_buffers = true;

            // Force UBO argument buffer binding to start at kUBOArgumentBufferBindingIndex.
            spirv_cross::MSLResourceBinding argBufferBinding = {};
            argBufferBinding.stage    = ShaderTypeToSpvExecutionModel(shaderType);
            argBufferBinding.desc_set = kGlslangShaderResourceDescSet;
            argBufferBinding.binding =
                spirv_cross::kArgumentBufferBinding;  // spirv-cross built-in binding.
            argBufferBinding.msl_buffer = kUBOArgumentBufferBindingIndex;  // Actual binding.
            spirv_cross::CompilerMSL::add_msl_resource_binding(argBufferBinding);

            // Force discrete slot bindings for textures, default uniforms & driver uniforms
            // instead of using argument buffer.
            spirv_cross::CompilerMSL::add_discrete_descriptor_set(kGlslangTextureDescSet);
            spirv_cross::CompilerMSL::add_discrete_descriptor_set(
                kGlslangDefaultUniformAndXfbDescSet);
            spirv_cross::CompilerMSL::add_discrete_descriptor_set(kGlslangDriverUniformsDescSet);
        }
        else
        {
            // Disable argument buffer generation for uniform buffers
            compOpt.argument_buffers = false;
        }

        spirv_cross::CompilerMSL::set_msl_options(compOpt);

        // Actual compilation
        mslShaderInfoOut->metalShaderSource =
            PostProcessTranslatedMsl(spirv_cross::CompilerMSL::compile());

        // Retrieve automatic texture slot assignments
        GetAssignedSamplerBindings(*this, originalSamplerBindings,
                                   &mslShaderInfoOut->actualSamplerBindings);
    }
};

angle::Result ConvertSpirvToMsl(Context *context,
                                gl::ShaderType shaderType,
                                const angle::HashMap<std::string, uint32_t> &uboOriginalBindings,
                                const angle::HashMap<uint32_t, uint32_t> &xfbOriginalBindings,
                                const OriginalSamplerBindingMap &originalSamplerBindings,
                                bool disableRasterization,
                                angle::spirv::Blob *sprivCode,
                                TranslatedShaderInfo *translatedShaderInfoOut)
{
    if (!sprivCode || sprivCode->empty())
    {
        return angle::Result::Continue;
    }

    SpirvToMslCompiler compilerMsl(std::move(*sprivCode));

    // NOTE(hqle): spirv-cross uses exceptions to report error, what should we do here
    // in case of error?
    compilerMsl.compileEx(shaderType, uboOriginalBindings, xfbOriginalBindings,
                          originalSamplerBindings, disableRasterization, translatedShaderInfoOut);
    if (translatedShaderInfoOut->metalShaderSource.size() == 0)
    {
        ANGLE_MTL_CHECK(context, false, GL_INVALID_OPERATION);
    }

    return angle::Result::Continue;
}

}  // namespace

void GlslangGetShaderSpirvCode(const gl::Context *context,
                               const gl::ProgramState &programState,
                               const gl::ProgramLinkedResources &resources,
                               gl::ShaderMap<const angle::spirv::Blob *> *spirvBlobsOut,
                               ShaderInterfaceVariableInfoMap *variableInfoMapOut,
                               ShaderInterfaceVariableInfoMap *xfbOnlyVSVariableInfoMapOut)
{
    GlslangSourceOptions options = CreateSourceOptions();
    GlslangProgramInterfaceInfo programInterfaceInfo;
    ResetGlslangProgramInterfaceInfo(&programInterfaceInfo);

    options.supportsTransformFeedbackEmulation = true;

    // Get shader sources and fill variable info map with transform feedback disabled.
    rx::GlslangGetShaderSpirvCode(context, options, programState, resources, &programInterfaceInfo,
                                  spirvBlobsOut, variableInfoMapOut);

    const gl::ProgramExecutable &programExecutable = programState.getExecutable();

    // Fill variable info map with transform feedback enabled.
    if (!programExecutable.getLinkedTransformFeedbackVaryings().empty())
    {
        GlslangProgramInterfaceInfo xfbOnlyInterfaceInfo;
        ResetGlslangProgramInterfaceInfo(&xfbOnlyInterfaceInfo);

        options.enableTransformFeedbackEmulation = true;
        UniformBindingIndexMap uniformBindingIndexMap;
        GlslangAssignLocations(options, programExecutable, resources.varyingPacking,
                               gl::ShaderType::Vertex, gl::ShaderType::InvalidEnum, true,
                               &xfbOnlyInterfaceInfo, &uniformBindingIndexMap,
                               xfbOnlyVSVariableInfoMapOut);
    }
}

angle::Result GlslangTransformSpirvCode(const gl::ShaderBitSet &linkedShaderStages,
                                        const gl::ShaderMap<const angle::spirv::Blob *> &spirvBlobs,
                                        bool isTransformFeedbackEnabled,
                                        const ShaderInterfaceVariableInfoMap &variableInfoMap,
                                        gl::ShaderMap<angle::spirv::Blob> *shaderCodeOut)
{
    for (const gl::ShaderType shaderType : linkedShaderStages)
    {
        GlslangSpirvOptions options;
        options.shaderType = shaderType;
        options.isTransformFeedbackStage =
            shaderType == gl::ShaderType::Vertex && isTransformFeedbackEnabled;
        options.isTransformFeedbackEmulated = true;

        angle::Result status = GlslangTransformSpirvCode(
            options, variableInfoMap, *spirvBlobs[shaderType], &(*shaderCodeOut)[shaderType]);
        if (status != angle::Result::Continue)
        {
            return status;
        }
    }

    return angle::Result::Continue;
}

angle::Result SpirvCodeToMsl(Context *context,
                             const gl::ProgramState &programState,
                             const ShaderInterfaceVariableInfoMap &xfbVSVariableInfoMap,
                             gl::ShaderMap<angle::spirv::Blob> *spirvShaderCode,
                             angle::spirv::Blob *xfbOnlySpirvCode /** nullable */,
                             gl::ShaderMap<TranslatedShaderInfo> *mslShaderInfoOut,
                             TranslatedShaderInfo *mslXfbOnlyShaderInfoOut /** nullable */)
{
    // Retrieve original uniform buffer bindings generated by front end. We will need to do a remap.
    angle::HashMap<std::string, uint32_t> uboOriginalBindings;
    const std::vector<gl::InterfaceBlock> &blocks = programState.getUniformBlocks();
    for (uint32_t bufferIdx = 0; bufferIdx < blocks.size(); ++bufferIdx)
    {
        const gl::InterfaceBlock &block = blocks[bufferIdx];
        if (!uboOriginalBindings.count(block.mappedName))
        {
            uboOriginalBindings[block.mappedName] = bufferIdx;
        }
    }
    // Retrieve original XFB buffers bindings produced by front end.
    angle::HashMap<uint32_t, uint32_t> xfbOriginalBindings;
    for (uint32_t bufferIdx = 0; bufferIdx < kMaxShaderXFBs; ++bufferIdx)
    {
        if (xfbVSVariableInfoMap.hasTransformFeedbackInfo(gl::ShaderType::Vertex, bufferIdx))
        {
            const ShaderInterfaceVariableInfo &info =
                xfbVSVariableInfoMap.getTransformFeedbackInfo(gl::ShaderType::Vertex, bufferIdx);
            xfbOriginalBindings[info.binding] = bufferIdx;
        }
    }

    // Retrieve original sampler bindings produced by front end.
    OriginalSamplerBindingMap originalSamplerBindings;
    const std::vector<gl::SamplerBinding> &samplerBindings = programState.getSamplerBindings();
    const std::vector<gl::LinkedUniform> &uniforms         = programState.getUniforms();

    for (uint32_t textureIndex = 0; textureIndex < samplerBindings.size(); ++textureIndex)
    {
        const gl::SamplerBinding &samplerBinding = samplerBindings[textureIndex];
        uint32_t uniformIndex = programState.getUniformIndexFromSamplerIndex(textureIndex);
        const gl::LinkedUniform &samplerUniform = uniforms[uniformIndex];
        std::string mappedSamplerName           = GlslangGetMappedSamplerName(samplerUniform.name);
        originalSamplerBindings[mappedSamplerName].push_back(
            {textureIndex, static_cast<uint32_t>(samplerBinding.boundTextureUnits.size())});
    }

    // Do the actual translation
    for (gl::ShaderType shaderType : gl::kAllGLES2ShaderTypes)
    {
        angle::spirv::Blob &sprivCode = spirvShaderCode->at(shaderType);
        ANGLE_TRY(ConvertSpirvToMsl(context, shaderType, uboOriginalBindings, xfbOriginalBindings,
                                    originalSamplerBindings, /* disableRasterization */ false,
                                    &sprivCode, &mslShaderInfoOut->at(shaderType)));
    }  // for (gl::ShaderType shaderType

    // Special version of XFB only
    if (xfbOnlySpirvCode && !programState.getLinkedTransformFeedbackVaryings().empty())
    {
        ANGLE_TRY(ConvertSpirvToMsl(context, gl::ShaderType::Vertex, uboOriginalBindings,
                                    xfbOriginalBindings, originalSamplerBindings,
                                    /* disableRasterization */ true, xfbOnlySpirvCode,
                                    mslXfbOnlyShaderInfoOut));
    }

    return angle::Result::Continue;
}

}  // namespace mtl
}  // namespace rx
