// MobileGL - MobileGL/MG_Impl/GLImpl/Program/ProgramInterface.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
//   https://www.gnu.org/licenses/gpl-3.0.txt
//   https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header

#include "ProgramInterface.h"

#include <MG_State/GLState/ProgramState/ProgramObject.h>
#include <MG_Util/ShaderTranspiler/Types.h>

#include <cstring>

namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
    namespace {
        // glslang folds atomic counters into synthesized blocks named
        // "<getAtomicCounterBlockName()>_<binding>" (ParseContextBase.cpp), one per GL
        // atomic-counter binding point. That block IS the GL_ATOMIC_COUNTER_BUFFER resource
        // and its trailing number IS GL_BUFFER_BINDING; its members stay GL_UNIFORMs.
        constexpr const char* kAtomicCounterBlockPrefix = MG_Util::ShaderTranspiler::ATOMIC_COUNTER_BLOCK_PREFIX;

        enum class BlockKind {
            Uniform,       // a real GL uniform block
            GlobalUbo,     // the synthesized MGL_GLOBAL_UBO: GL sees its members as default-block
            AtomicCounter, // gl_AtomicCounterBlock_<binding>
            Storage,       // a shader storage block
        };

        // One row of any interface. Fields a given interface does not have keep the
        // spec-mandated "not applicable" value, so a prop read never has to special-case
        // the interface a second time.
        struct Resource {
            String name;
            GLenum type = GL_NONE;
            GLint arraySize = 1;
            GLint location = -1;
            GLint locationIndex = -1;
            GLint blockIndex = -1;
            GLint offset = -1;
            GLint arrayStride = -1;
            GLint matrixStride = -1;
            GLint isRowMajor = 0;
            GLint atomicCounterBufferIndex = -1;
            GLint topLevelArraySize = 0;
            GLint topLevelArrayStride = 0;
            GLint bufferBinding = 0;
            GLint bufferDataSize = 0;
            GLint isPerPatch = 0;
            GLint xfbBufferIndex = 0;
            Uint32 stages = 0; // EShLanguageMask
            Vector<GLuint> activeVariables;
        };

        using ResourceList = Vector<Resource>;

        struct Model {
            ResourceList uniforms;
            ResourceList uniformBlocks;
            ResourceList atomicCounterBuffers;
            ResourceList bufferVariables;
            ResourceList storageBlocks;
            ResourceList programInputs;
            ResourceList programOutputs;
            ResourceList xfbVaryings;
            Bool valid = false;
        };

        const ResourceList& EmptyList() {
            static const ResourceList empty;
            return empty;
        }

        // ---- name spelling (cluster 6) -------------------------------------------------

        Bool EndsWithZeroSubscript(const String& name) {
            return name.length() >= 3 && name.compare(name.length() - 3, 3, "[0]") == 0;
        }

        // The enumerated spelling of an array resource is "name[0]". glslang already applies
        // that to uniforms and buffer variables (EShReflectionBasicArraySuffix), but never to
        // stage inputs/outputs, so those get it here.
        String WithArraySuffix(const String& name, const ProgramObject::TypeFacts& type) {
            if (!type.isArray || EndsWithZeroSubscript(name)) return name;
            return name + "[0]";
        }

        // GL_ARRAY_SIZE: element count for a sized array, 0 for a runtime-sized one
        // (a shader storage block's unsized trailing member), 1 for a non-array.
        // `record.arraySize` is already the sized-array/reflected-size resolution; the only
        // extra rule here is GL's 0 for a runtime-sized array.
        GLint ArraySizeOf(const ProgramObject::ResourceReflection& record) {
            if (record.type.isArray && !record.type.isSizedArray) return 0;
            return record.arraySize;
        }

        // Two spellings name the same resource when they are equal, or differ only by the
        // "[0]" the enumeration appends to an array.
        Bool NamesMatch(const String& resourceName, const String& query) {
            if (resourceName == query) return true;
            if (EndsWithZeroSubscript(resourceName) &&
                resourceName.compare(0, resourceName.length() - 3, query) == 0) {
                return true;
            }
            return EndsWithZeroSubscript(query) && query.compare(0, query.length() - 3, resourceName) == 0;
        }

        // Splits "base[k]" into ("base", k). GL 4.6 §7.3.1.1 requires the subscript to be a
        // decimal integer with no white space and no leading zeros, which is exactly what
        // separates array-names' "a[1]" (resolves) from "a[01]", "a[0 + 0]" and "a[ 0]" (do
        // not). Returns false when there is no trailing subscript at all; sets `malformed`
        // when there is one but it is not a strict decimal.
        Bool SplitTrailingSubscript(const String& name, String& outBase, Uint& outElement, Bool& outMalformed) {
            outMalformed = false;
            if (name.empty() || name.back() != ']') return false;
            const SizeT bracket = name.rfind('[');
            if (bracket == String::npos) return false;
            const SizeT first = bracket + 1;
            const SizeT last = name.length() - 1; // one past the digits
            if (first >= last) {
                outMalformed = true;
                return false;
            }
            // No leading zeros: "0" is the only spelling that may start with '0'.
            if (name[first] == '0' && last - first > 1) {
                outMalformed = true;
                return false;
            }
            Uint element = 0;
            for (SizeT i = first; i < last; ++i) {
                if (name[i] < '0' || name[i] > '9') {
                    outMalformed = true;
                    return false;
                }
                element = element * 10 + static_cast<Uint>(name[i] - '0');
                if (element > 0x0FFFFFFFu) {
                    outMalformed = true;
                    return false;
                }
            }
            outBase = name.substr(0, bracket);
            outElement = element;
            return true;
        }

        // ---- block classification ------------------------------------------------------

        Bool IsAtomicCounterBlockName(const String& name) {
            return name.compare(0, std::strlen(kAtomicCounterBlockPrefix), kAtomicCounterBlockPrefix) == 0;
        }

        // "gl_AtomicCounterBlock_5" -> 5. The suffix is the GL binding the counters were
        // declared with, which glslang does NOT keep in the block's own layout qualifier
        // (that one is remapped to a plain buffer binding).
        GLint AtomicCounterBlockBinding(const String& name) {
            const SizeT underscore = name.rfind('_');
            if (underscore == String::npos || underscore + 1 >= name.length()) return 0;
            GLint binding = 0;
            for (SizeT i = underscore + 1; i < name.length(); ++i) {
                if (name[i] < '0' || name[i] > '9') return 0;
                binding = binding * 10 + (name[i] - '0');
            }
            return binding;
        }

        // Element index of an arrayed block instance ("TrickyBuffer[1]" -> 1).
        GLint BlockArrayElement(const String& name) {
            String base;
            Uint element = 0;
            Bool malformed = false;
            if (!SplitTrailingSubscript(name, base, element, malformed)) return 0;
            return static_cast<GLint>(element);
        }

        BlockKind ClassifyBlock(const ProgramObject::BlockReflection& block) {
            if (std::strstr(block.name.c_str(), MG_Util::ShaderTranspiler::GLOBAL_UBO_NAME) != nullptr) {
                return BlockKind::GlobalUbo;
            }
            if (IsAtomicCounterBlockName(block.name)) return BlockKind::AtomicCounter;
            if (block.type.isBuffer) return BlockKind::Storage;
            return BlockKind::Uniform;
        }

        // std140/std430 column stride, the same vec4-rounded rule ProgramObject applies to
        // uniform matrices. 0 for a non-matrix.
        GLint MatrixStrideOf(const ProgramObject::TypeFacts& type) {
            if (!type.isMatrix) return 0;
            const bool rowMajor = type.layoutMatrix == static_cast<Int>(glslang::ElmRowMajor);
            const int strideVectorComponents = rowMajor ? type.matrixCols : type.matrixRows;
            constexpr int scalarSize = 4;
            const int vectorAlignment = (strideVectorComponents <= 1)    ? scalarSize
                                        : (strideVectorComponents == 2) ? 2 * scalarSize
                                                                        : 4 * scalarSize;
            return (vectorAlignment + 15) & ~15;
        }

        GLint IsRowMajorOf(const ProgramObject::TypeFacts& type) {
            if (!type.isMatrix) return 0;
            return type.layoutMatrix == static_cast<Int>(glslang::ElmRowMajor) ? 1 : 0;
        }

        GLint MappedLocation(Int rawLocation) {
            // glslang parks "no location" at layoutLocationEnd; GL spells it -1.
            if (rawLocation < 0 || rawLocation >= static_cast<Int>(glslang::TQualifier::layoutLocationEnd)) return -1;
            return rawLocation;
        }

        // ---- model construction --------------------------------------------------------

        // GL_REFERENCED_BY_*_SHADER for an ARRAYED block instance, refined per element.
        //
        // glslang records a block reference by walking up to the base symbol and calling
        // addBlockName with the whole ARRAY type, which ORs the referencing stage into every
        // element at once - it has not resolved the subscript yet at that point. So reading
        // "e[0].b" marks both TrickyBlock[0] and TrickyBlock[1] as referenced by the fragment
        // stage (KHR-GL43.program_interface_query.uniform-block-types).
        //
        // The MEMBER masks are exact: EShReflectionAllBlockVariables enumerates every member of
        // every element with the stage mask suppressed, and only the dereference chain actually
        // walked turns a bit on - and that chain carries the subscript. So the union of a block
        // instance's members is the reference set of that instance.
        //
        // Applied ONLY to arrayed instances, because for a scalar block glslang is already exact.
        // Note the union is used even when it is empty: an array element nobody dereferenced has
        // no member bits and is genuinely referenced by nobody, which is the whole point - falling
        // back to the block's own mask there would restore the over-approximation.
        Vector<Uint32> BuildBlockStagesFromMembers(const ProgramObject::LinkArtifacts& reflection,
                                                    Int blockCount) {
            Vector<Uint32> stagesByBlock(static_cast<SizeT>(blockCount < 0 ? 0 : blockCount), 0u);
            const Int uniformCount = static_cast<Int>(reflection.uniformReflection.size());
            for (Int index = 0; index < uniformCount; ++index) {
                const auto& uniform = reflection.uniformReflection[index];
                const Int owner = uniform.index;
                if (owner < 0 || owner >= blockCount) continue;
                stagesByBlock[static_cast<SizeT>(owner)] |= static_cast<Uint32>(uniform.stages);
            }
            return stagesByBlock;
        }

        // UNIFORM blocks only, and that scope is load-bearing rather than cautious. The member
        // names glslang produces for a uniform block array carry the subscript
        // ("TrickyBlock[0].b", via EShReflectionStrictArraySuffix), so each element's members are
        // distinct entries and the bits land on the right one. A SHADER STORAGE block array does
        // NOT get that treatment - its buffer variables reflect under one subscript-free spelling
        // shared by every element - so a union over them credits element 0 and starves the rest.
        // KHR-GL43.program_interface_query.ssb-types is the case that says so: it reads ss[0] and
        // ss[1] and requires both to report the fragment stage, which only glslang's own
        // (deliberately over-approximating) block mask gets right. Storage and atomic-counter
        // blocks therefore keep that mask untouched.
        Uint32 UniformBlockStages(const ProgramObject::BlockReflection& block, const Vector<Uint32>& stagesFromMembers,
                                  Int tIndex) {
            String arrayBase;
            Uint element = 0;
            Bool malformed = false;
            if (!SplitTrailingSubscript(block.name, arrayBase, element, malformed) || malformed) {
                return static_cast<Uint32>(block.stages);
            }
            if (tIndex < 0 || tIndex >= static_cast<Int>(stagesFromMembers.size())) {
                return static_cast<Uint32>(block.stages);
            }
            return stagesFromMembers[static_cast<SizeT>(tIndex)];
        }

        void BuildBlocks(ProgramObject& program, const ProgramObject::LinkArtifacts& reflection, Model& model,
                         Vector<BlockKind>& blockKind, Vector<Int>& blockInterfaceIndex) {
            const Int blockCount = static_cast<Int>(reflection.blockReflection.size());
            blockKind.assign(blockCount, BlockKind::Uniform);
            blockInterfaceIndex.assign(blockCount, -1);
            const Vector<Uint32> stagesFromMembers = BuildBlockStagesFromMembers(reflection, blockCount);

            for (Int tIndex = 0; tIndex < blockCount; ++tIndex) {
                const auto& block = reflection.blockReflection[tIndex];
                const BlockKind kind = ClassifyBlock(block);
                blockKind[tIndex] = kind;
                if (kind == BlockKind::AtomicCounter) {
                    Resource resource;
                    // GL_ATOMIC_COUNTER_BUFFER resources have no name (and GetProgramResource
                    // Index/Name reject the interface outright, which is why this stays empty).
                    resource.bufferBinding = AtomicCounterBlockBinding(block.name);
                    resource.bufferDataSize = block.size;
                    resource.stages = static_cast<Uint32>(block.stages);
                    blockInterfaceIndex[tIndex] = static_cast<Int>(model.atomicCounterBuffers.size());
                    model.atomicCounterBuffers.push_back(Move(resource));
                } else if (kind == BlockKind::Storage) {
                    Resource resource;
                    resource.name = block.name;
                    // glslang reports the DECLARED binding for every instance of an arrayed
                    // block; GL gives element k the binding base + k. That is only the initial
                    // value: GL_BUFFER_BINDING must report the CURRENT binding, so a later
                    // glShaderStorageBlockBinding wins over the declaration (GL 4.6 §7.6.2 -
                    // exactly the same rule GL_UNIFORM_BLOCK follows through
                    // GetUniformBlockBinding below).
                    const GLint declared = block.binding;
                    resource.bufferBinding = declared < 0 ? 0 : declared + BlockArrayElement(block.name);
                    const Int rebound = program.GetShaderStorageBlockBindingOverride(block.name);
                    if (rebound >= 0) resource.bufferBinding = static_cast<GLint>(rebound);
                    resource.bufferDataSize = block.size;
                    resource.stages = static_cast<Uint32>(block.stages);
                    blockInterfaceIndex[tIndex] = static_cast<Int>(model.storageBlocks.size());
                    model.storageBlocks.push_back(Move(resource));
                }
            }

            // GL_UNIFORM_BLOCK keeps the index space glUniformBlockBinding and
            // glGetActiveUniformBlockiv already use, so an index handed out here is usable
            // with them (which is exactly what the CTS does).
            const Int glBlockCount = program.GetGlUniformBlockCount();
            for (Int glIndex = 0; glIndex < glBlockCount; ++glIndex) {
                // The block-space index the block-keyed accessors want; the two spaces differ
                // whenever the program also has a storage or atomic counter block, which
                // glslang files under the same reflection list (no EShReflectionSeparateBuffers).
                const Int blockIndex = program.BlockIndexFromGlUniformBlock(static_cast<Uint>(glIndex));
                Resource resource;
                resource.name = program.GetUniformBlockName(static_cast<Uint>(blockIndex));
                resource.bufferBinding = static_cast<GLint>(program.GetUniformBlockBinding(static_cast<Uint>(blockIndex)));
                resource.bufferDataSize = static_cast<GLint>(program.GetUBOSizeAt(static_cast<Uint>(blockIndex)));
                const Int tIndex = program.TProgramBlockIndex(static_cast<Uint>(blockIndex));
                if (tIndex >= 0 && tIndex < blockCount) {
                    resource.stages = UniformBlockStages(reflection.blockReflection[tIndex],
                                                      stagesFromMembers, tIndex);
                }
                model.uniformBlocks.push_back(Move(resource));
            }
        }

        void BuildUniformsAndBufferVariables(ProgramObject& program,
                                             const ProgramObject::LinkArtifacts& reflection, Model& model,
                                             const Vector<BlockKind>& blockKind,
                                             const Vector<Int>& blockInterfaceIndex) {
            // Walks the TPROGRAM uniform space, not the GL one. A buffer variable is not a GL
            // uniform (GL 4.6 core 7.3.1) and DoReflection therefore keeps it out of the GL
            // active-uniform index space - but GL_BUFFER_VARIABLE still has to enumerate it, and
            // this is the only place that does. GL uniforms keep their GL index as their
            // GL_UNIFORM resource index: the GL space is a subsequence of this one, so pushing
            // the GL-visible entries in this order preserves the correspondence.
            const Int tUniformCount = static_cast<Int>(reflection.uniformReflection.size());
            for (Int tIndex = 0; tIndex < tUniformCount; ++tIndex) {
                const auto& refl = ProgramObject::UniformAtIn(reflection, tIndex);
                const auto& type = refl.type;
                const Int owner = refl.index;
                const BlockKind kind = (owner >= 0 && owner < static_cast<Int>(blockKind.size()))
                                           ? blockKind[owner]
                                           : BlockKind::GlobalUbo;
                const Int glIndex = program.GlUniformIndexFromTProgram(tIndex);
                // Everything except a buffer variable is enumerated through the GL space, so a
                // uniform the relaxed parse swept out of it (a declared-but-dead default-block
                // one) stays out of GL_UNIFORM too.
                if (kind != BlockKind::Storage && glIndex < 0) continue;

                Resource resource;
                resource.name = refl.name;
                resource.type = static_cast<GLenum>(refl.glDefineType);
                resource.arraySize = ArraySizeOf(refl);
                resource.stages = static_cast<Uint32>(refl.stages);

                if (kind == BlockKind::Storage) {
                    resource.blockIndex = blockInterfaceIndex[owner];
                    resource.offset = refl.offset;
                    resource.arrayStride = refl.arrayStride;
                    resource.matrixStride = MatrixStrideOf(type);
                    resource.isRowMajor = IsRowMajorOf(type);
                    // GL requires 1 for a member that is not inside a top-level array (and for
                    // the top-level array itself); glslang leaves 0/-1 there.
                    resource.topLevelArraySize = refl.topLevelArraySize > 0 ? refl.topLevelArraySize : 1;
                    resource.topLevelArrayStride = refl.topLevelArrayStride;
                    model.bufferVariables.push_back(Move(resource));
                    continue;
                }

                if (kind == BlockKind::AtomicCounter) {
                    // An atomic counter is a default-block uniform with no location and no
                    // owning uniform block; what it does have is a buffer to point at.
                    resource.type = GL_UNSIGNED_INT_ATOMIC_COUNTER;
                    resource.blockIndex = -1;
                    resource.offset = refl.offset;
                    resource.arrayStride = refl.arrayStride;
                    resource.matrixStride = 0;
                    resource.atomicCounterBufferIndex = blockInterfaceIndex[owner];
                    resource.location = -1;
                } else {
                    const Uint glUniformIndex = static_cast<Uint>(glIndex);
                    resource.blockIndex = program.GetActiveUniformBlockIndex(glUniformIndex);
                    resource.offset = program.GetActiveUniformOffset(glUniformIndex);
                    resource.arrayStride = program.GetActiveUniformArrayStride(glUniformIndex);
                    resource.matrixStride = program.GetActiveUniformMatrixStride(glUniformIndex);
                    resource.isRowMajor = program.GetActiveUniformIsRowMajor(glUniformIndex);
                    // A member of a named uniform block has no location, whatever the
                    // frontend's own location table says (it hands one out to every uniform
                    // so glUniform* can address block members through the global UBO).
                    resource.location =
                        resource.blockIndex >= 0 ? -1 : program.GetUniformLocation(refl.name);
                }
                model.uniforms.push_back(Move(resource));
            }

            // GL_ACTIVE_VARIABLES, both directions.
            for (SizeT i = 0; i < model.uniforms.size(); ++i) {
                const Resource& uniform = model.uniforms[i];
                if (uniform.atomicCounterBufferIndex >= 0 &&
                    uniform.atomicCounterBufferIndex < static_cast<GLint>(model.atomicCounterBuffers.size())) {
                    model.atomicCounterBuffers[uniform.atomicCounterBufferIndex].activeVariables.push_back(
                        static_cast<GLuint>(i));
                }
            }
            for (SizeT glBlockIndex = 0; glBlockIndex < model.uniformBlocks.size(); ++glBlockIndex) {
                // Members of an arrayed block are reflected once, against instance [0].
                // GetUniformBlockMemberOwnerIndex takes and answers BLOCK indices, while
                // Resource::blockIndex is a GL_UNIFORM_BLOCK index, so translate both ways.
                const Int blockIndex = program.BlockIndexFromGlUniformBlock(static_cast<Uint>(glBlockIndex));
                const Int owner = program.GlUniformBlockIndexFromBlock(
                    static_cast<Int>(program.GetUniformBlockMemberOwnerIndex(static_cast<Uint>(blockIndex))));
                for (SizeT i = 0; i < model.uniforms.size(); ++i) {
                    if (model.uniforms[i].blockIndex == owner) {
                        model.uniformBlocks[glBlockIndex].activeVariables.push_back(static_cast<GLuint>(i));
                    }
                }
            }
            for (SizeT blockIndex = 0; blockIndex < model.storageBlocks.size(); ++blockIndex) {
                for (SizeT i = 0; i < model.bufferVariables.size(); ++i) {
                    if (model.bufferVariables[i].blockIndex == static_cast<GLint>(blockIndex)) {
                        model.storageBlocks[blockIndex].activeVariables.push_back(static_cast<GLuint>(i));
                    }
                }
            }
        }

        // A built-in interface block that a shader redeclares with fewer members keeps the
        // omitted ones in its type when the redeclaration is ANONYMOUS - glslang hides them
        // (basic type void) instead of erasing them, because the original shared declaration
        // has to stay usable. Only the instance-named form erases. So a separable vertex
        // program that redeclares `out gl_PerVertex { vec4 gl_Position; }` still carries
        // gl_PointSize and gl_ClipDistance through the block-unwrapping reflection, and they
        // are not part of its output interface.
        Bool IsHiddenBlockMember(const ProgramObject::TypeFacts& type) { return type.isVoid; }

        void BuildStageIO(ProgramObject& program, const ProgramObject::LinkArtifacts& reflection, Model& model) {
            const Int inputCount = static_cast<Int>(reflection.pipeInputReflection.size());
            for (Int index = 0; index < inputCount; ++index) {
                const auto& refl = reflection.pipeInputReflection[index];
                const auto& type = refl.type;
                if (IsHiddenBlockMember(type)) continue;
                Resource resource;
                // The Vulkan-semantics parse reflects the vertex builtins under their SPIR-V
                // names; GL enumerates the GL spellings.
                const String& glName = ProgramObject::NormalizeBuiltinPipeInputName(refl.name);
                resource.name = WithArraySuffix(glName, type);
                resource.type = static_cast<GLenum>(refl.glDefineType);
                resource.arraySize = ArraySizeOf(refl);
                resource.location = program.GetAttributeLocation(refl.name);
                if (resource.location < 0) resource.location = MappedLocation(refl.location);
                resource.isPerPatch = type.isPatch ? 1 : 0;
                resource.stages = static_cast<Uint32>(refl.stages);
                model.programInputs.push_back(Move(resource));
            }

            // A color number, and therefore a color INDEX, exists only for a fragment stage's
            // outputs. The output interface belongs to the program's last stage, so for a
            // separable tessellation/geometry/vertex program these are varyings: asking the
            // frag-data maps about them can still answer a location (a tess-control output
            // carries its own layout(location=N)), and a location then manufactures a color
            // index of 0 where GL requires -1
            // (KHR-GL43.program_interface_query.separate-programs-tess-control).
            const Bool lastStageIsFragment = reflection.lastStageIsFragment;
            const Int outputCount = static_cast<Int>(reflection.pipeOutputReflection.size());
            for (Int index = 0; index < outputCount; ++index) {
                const auto& refl = reflection.pipeOutputReflection[index];
                const auto& type = refl.type;
                if (IsHiddenBlockMember(type)) continue;
                Resource resource;
                resource.name = WithArraySuffix(refl.name, type);
                resource.type = static_cast<GLenum>(refl.glDefineType);
                resource.arraySize = ArraySizeOf(refl);
                resource.location = MappedLocation(program.GetFragmentDataLocation(refl.name.c_str()));
                if (resource.location < 0 || !lastStageIsFragment) {
                    // A built-in output (gl_FragDepth, gl_SampleMask) has no location, and a
                    // non-fragment stage's outputs have no color number at all - either way there
                    // is no color index.
                    resource.locationIndex = -1;
                } else {
                    resource.locationIndex = program.GetFragmentDataIndex(refl.name.c_str());
                    // glBindFragDataLocationIndexed wins; otherwise the shader's
                    // layout(index = N), which the frag-data maps never saw.
                    if (resource.locationIndex == 0 && type.hasIndex) {
                        resource.locationIndex = static_cast<GLint>(type.layoutIndex);
                    }
                }
                resource.isPerPatch = type.isPatch ? 1 : 0;
                resource.stages = static_cast<Uint32>(refl.stages);
                model.programOutputs.push_back(Move(resource));
            }
        }

        void BuildXfb(ProgramObject& program, Model& model) {
            const auto& requested = program.GetTransformFeedbackInterfaceNames();
            const auto& captured = program.GetTransformFeedbackVaryings();
            for (const String& name : requested) {
                Resource resource;
                resource.name = name;
                // ARB_transform_feedback3's layout controls are enumerated as resources of
                // type NONE: gl_NextBuffer with array size 0, gl_SkipComponentsN with N.
                if (name == "gl_NextBuffer") {
                    resource.type = GL_NONE;
                    resource.arraySize = 0;
                } else if (name.size() == 18 && name.compare(0, 17, "gl_SkipComponents") == 0 && name[17] >= '1' &&
                           name[17] <= '4') {
                    resource.type = GL_NONE;
                    resource.arraySize = name[17] - '0';
                } else {
                    resource.type = GL_NONE;
                    resource.arraySize = 1;
                    for (const auto& varying : captured) {
                        if (varying.name != name) continue;
                        resource.type = varying.type;
                        resource.arraySize = varying.size < 1 ? 1 : varying.size;
                        resource.offset = static_cast<GLint>(varying.offsetBytes);
                        resource.xfbBufferIndex = static_cast<GLint>(varying.bufferIndex);
                        break;
                    }
                }
                model.xfbVaryings.push_back(Move(resource));
            }
        }

        Model BuildModel(ProgramObject& program) {
            Model model;
            if (!program.GetLinkStatus()) return model;
            const ProgramObject::LinkArtifacts& reflection = program.GetLinkReflection();
            model.valid = true;

            Vector<BlockKind> blockKind;
            Vector<Int> blockInterfaceIndex;
            BuildBlocks(program, reflection, model, blockKind, blockInterfaceIndex);
            BuildUniformsAndBufferVariables(program, reflection, model, blockKind, blockInterfaceIndex);
            BuildStageIO(program, reflection, model);
            BuildXfb(program, model);
            return model;
        }

        const ResourceList& Select(const Model& model, GLenum programInterface) {
            switch (programInterface) {
            case GL_UNIFORM:
                return model.uniforms;
            case GL_UNIFORM_BLOCK:
                return model.uniformBlocks;
            case GL_ATOMIC_COUNTER_BUFFER:
                return model.atomicCounterBuffers;
            case GL_BUFFER_VARIABLE:
                return model.bufferVariables;
            case GL_SHADER_STORAGE_BLOCK:
                return model.storageBlocks;
            case GL_PROGRAM_INPUT:
                return model.programInputs;
            case GL_PROGRAM_OUTPUT:
                return model.programOutputs;
            case GL_TRANSFORM_FEEDBACK_VARYING:
                return model.xfbVaryings;
            default:
                // The subroutine interfaces are accepted by the API but nothing can populate
                // them: glslang refuses `subroutine` when generating SPIR-V, so a program
                // using one never links. Zero active resources is the honest answer.
                return EmptyList();
            }
        }
    } // namespace

    Bool IsInterfaceEnum(GLenum programInterface) {
        switch (programInterface) {
        case GL_UNIFORM:
        case GL_UNIFORM_BLOCK:
        case GL_PROGRAM_INPUT:
        case GL_PROGRAM_OUTPUT:
        case GL_BUFFER_VARIABLE:
        case GL_SHADER_STORAGE_BLOCK:
        case GL_ATOMIC_COUNTER_BUFFER:
        case GL_TRANSFORM_FEEDBACK_VARYING:
        case GL_TRANSFORM_FEEDBACK_BUFFER:
        case GL_VERTEX_SUBROUTINE:
        case GL_TESS_CONTROL_SUBROUTINE:
        case GL_TESS_EVALUATION_SUBROUTINE:
        case GL_GEOMETRY_SUBROUTINE:
        case GL_FRAGMENT_SUBROUTINE:
        case GL_COMPUTE_SUBROUTINE:
        case GL_VERTEX_SUBROUTINE_UNIFORM:
        case GL_TESS_CONTROL_SUBROUTINE_UNIFORM:
        case GL_TESS_EVALUATION_SUBROUTINE_UNIFORM:
        case GL_GEOMETRY_SUBROUTINE_UNIFORM:
        case GL_FRAGMENT_SUBROUTINE_UNIFORM:
        case GL_COMPUTE_SUBROUTINE_UNIFORM:
            return true;
        default:
            return false;
        }
    }

    Bool IsNamedInterface(GLenum programInterface) {
        // GL 4.6 §7.3.1.2: the two buffer interfaces have no resource names, and asking for
        // one is INVALID_ENUM (deliberately asymmetric with GetProgramInterfaceiv, which
        // does count them).
        return IsInterfaceEnum(programInterface) && programInterface != GL_ATOMIC_COUNTER_BUFFER &&
               programInterface != GL_TRANSFORM_FEEDBACK_BUFFER;
    }

    Bool InterfaceHasLocations(GLenum programInterface) {
        switch (programInterface) {
        case GL_UNIFORM:
        case GL_PROGRAM_INPUT:
        case GL_PROGRAM_OUTPUT:
        case GL_VERTEX_SUBROUTINE_UNIFORM:
        case GL_TESS_CONTROL_SUBROUTINE_UNIFORM:
        case GL_TESS_EVALUATION_SUBROUTINE_UNIFORM:
        case GL_GEOMETRY_SUBROUTINE_UNIFORM:
        case GL_FRAGMENT_SUBROUTINE_UNIFORM:
        case GL_COMPUTE_SUBROUTINE_UNIFORM:
            return true;
        default:
            return false;
        }
    }

    Bool IsResourceProp(GLenum prop) {
        switch (prop) {
        case GL_NAME_LENGTH:
        case GL_TYPE:
        case GL_ARRAY_SIZE:
        case GL_OFFSET:
        case GL_BLOCK_INDEX:
        case GL_ARRAY_STRIDE:
        case GL_MATRIX_STRIDE:
        case GL_IS_ROW_MAJOR:
        case GL_ATOMIC_COUNTER_BUFFER_INDEX:
        case GL_BUFFER_BINDING:
        case GL_BUFFER_DATA_SIZE:
        case GL_NUM_ACTIVE_VARIABLES:
        case GL_ACTIVE_VARIABLES:
        case GL_REFERENCED_BY_VERTEX_SHADER:
        case GL_REFERENCED_BY_TESS_CONTROL_SHADER:
        case GL_REFERENCED_BY_TESS_EVALUATION_SHADER:
        case GL_REFERENCED_BY_GEOMETRY_SHADER:
        case GL_REFERENCED_BY_FRAGMENT_SHADER:
        case GL_REFERENCED_BY_COMPUTE_SHADER:
        case GL_TOP_LEVEL_ARRAY_SIZE:
        case GL_TOP_LEVEL_ARRAY_STRIDE:
        case GL_LOCATION:
        case GL_LOCATION_INDEX:
        case GL_IS_PER_PATCH:
        case GL_LOCATION_COMPONENT:
        case GL_TRANSFORM_FEEDBACK_BUFFER_INDEX:
        case GL_TRANSFORM_FEEDBACK_BUFFER_STRIDE:
        case GL_NUM_COMPATIBLE_SUBROUTINES:
        case GL_COMPATIBLE_SUBROUTINES:
            return true;
        default:
            return false;
        }
    }

    // GL 4.6 Table 7.2, transcribed row by row: which interfaces each property applies to.
    // Too tight a table turns a currently-answered prop into a fresh INVALID_OPERATION, so
    // the rows below are deliberately no narrower than the spec's.
    Bool InterfaceSupportsProp(GLenum programInterface, GLenum prop) {
        const Bool isSubroutine =
            programInterface == GL_VERTEX_SUBROUTINE || programInterface == GL_TESS_CONTROL_SUBROUTINE ||
            programInterface == GL_TESS_EVALUATION_SUBROUTINE || programInterface == GL_GEOMETRY_SUBROUTINE ||
            programInterface == GL_FRAGMENT_SUBROUTINE || programInterface == GL_COMPUTE_SUBROUTINE;
        const Bool isSubroutineUniform =
            programInterface == GL_VERTEX_SUBROUTINE_UNIFORM ||
            programInterface == GL_TESS_CONTROL_SUBROUTINE_UNIFORM ||
            programInterface == GL_TESS_EVALUATION_SUBROUTINE_UNIFORM ||
            programInterface == GL_GEOMETRY_SUBROUTINE_UNIFORM ||
            programInterface == GL_FRAGMENT_SUBROUTINE_UNIFORM || programInterface == GL_COMPUTE_SUBROUTINE_UNIFORM;

        switch (prop) {
        case GL_NAME_LENGTH:
            return programInterface != GL_ATOMIC_COUNTER_BUFFER && programInterface != GL_TRANSFORM_FEEDBACK_BUFFER;
        case GL_TYPE:
        case GL_ARRAY_SIZE:
            return programInterface == GL_UNIFORM || programInterface == GL_PROGRAM_INPUT ||
                   programInterface == GL_PROGRAM_OUTPUT || programInterface == GL_BUFFER_VARIABLE ||
                   programInterface == GL_TRANSFORM_FEEDBACK_VARYING ||
                   (prop == GL_ARRAY_SIZE && isSubroutineUniform);
        case GL_OFFSET:
            return programInterface == GL_UNIFORM || programInterface == GL_BUFFER_VARIABLE ||
                   programInterface == GL_TRANSFORM_FEEDBACK_VARYING;
        case GL_BLOCK_INDEX:
        case GL_ARRAY_STRIDE:
        case GL_MATRIX_STRIDE:
        case GL_IS_ROW_MAJOR:
            return programInterface == GL_UNIFORM || programInterface == GL_BUFFER_VARIABLE;
        case GL_ATOMIC_COUNTER_BUFFER_INDEX:
            return programInterface == GL_UNIFORM;
        case GL_BUFFER_BINDING:
        case GL_NUM_ACTIVE_VARIABLES:
        case GL_ACTIVE_VARIABLES:
            // Table 7.2 lists GL_TRANSFORM_FEEDBACK_BUFFER on these three rows too. This
            // implementation enumerates no resources on that interface, so the query still
            // ends in an error - but INVALID_VALUE for the out-of-range index, not the
            // INVALID_OPERATION a narrower table would invent.
            return programInterface == GL_UNIFORM_BLOCK || programInterface == GL_ATOMIC_COUNTER_BUFFER ||
                   programInterface == GL_SHADER_STORAGE_BLOCK ||
                   programInterface == GL_TRANSFORM_FEEDBACK_BUFFER;
        case GL_BUFFER_DATA_SIZE:
            return programInterface == GL_UNIFORM_BLOCK || programInterface == GL_ATOMIC_COUNTER_BUFFER ||
                   programInterface == GL_SHADER_STORAGE_BLOCK;
        case GL_REFERENCED_BY_VERTEX_SHADER:
        case GL_REFERENCED_BY_TESS_CONTROL_SHADER:
        case GL_REFERENCED_BY_TESS_EVALUATION_SHADER:
        case GL_REFERENCED_BY_GEOMETRY_SHADER:
        case GL_REFERENCED_BY_FRAGMENT_SHADER:
        case GL_REFERENCED_BY_COMPUTE_SHADER:
            return programInterface == GL_UNIFORM || programInterface == GL_UNIFORM_BLOCK ||
                   programInterface == GL_ATOMIC_COUNTER_BUFFER || programInterface == GL_BUFFER_VARIABLE ||
                   programInterface == GL_SHADER_STORAGE_BLOCK || programInterface == GL_PROGRAM_INPUT ||
                   programInterface == GL_PROGRAM_OUTPUT || isSubroutineUniform;
        case GL_TOP_LEVEL_ARRAY_SIZE:
        case GL_TOP_LEVEL_ARRAY_STRIDE:
            return programInterface == GL_BUFFER_VARIABLE;
        case GL_LOCATION:
            return InterfaceHasLocations(programInterface);
        case GL_LOCATION_INDEX:
            return programInterface == GL_PROGRAM_OUTPUT;
        case GL_IS_PER_PATCH:
        case GL_LOCATION_COMPONENT:
            return programInterface == GL_PROGRAM_INPUT || programInterface == GL_PROGRAM_OUTPUT;
        case GL_TRANSFORM_FEEDBACK_BUFFER_INDEX:
            return programInterface == GL_TRANSFORM_FEEDBACK_VARYING;
        case GL_TRANSFORM_FEEDBACK_BUFFER_STRIDE:
            return programInterface == GL_TRANSFORM_FEEDBACK_BUFFER;
        case GL_NUM_COMPATIBLE_SUBROUTINES:
        case GL_COMPATIBLE_SUBROUTINES:
            return isSubroutineUniform;
        default:
            (void)isSubroutine;
            return false;
        }
    }

    Int GetActiveResourceCount(ProgramObject& program, GLenum programInterface) {
        const Model model = BuildModel(program);
        return static_cast<Int>(Select(model, programInterface).size());
    }

    Int GetMaxNameLength(ProgramObject& program, GLenum programInterface) {
        if (!IsNamedInterface(programInterface)) return 0;
        const Model model = BuildModel(program);
        SizeT longest = 0;
        for (const Resource& resource : Select(model, programInterface)) {
            longest = std::max(longest, resource.name.length() + 1);
        }
        return static_cast<Int>(longest);
    }

    Int GetMaxNumActiveVariables(ProgramObject& program, GLenum programInterface) {
        const Model model = BuildModel(program);
        SizeT longest = 0;
        for (const Resource& resource : Select(model, programInterface)) {
            longest = std::max(longest, resource.activeVariables.size());
        }
        return static_cast<Int>(longest);
    }

    GLuint GetResourceIndex(ProgramObject& program, GLenum programInterface, const char* name) {
        if (name == nullptr || name[0] == '\0') return GL_INVALID_INDEX;
        const Model model = BuildModel(program);
        const ResourceList& resources = Select(model, programInterface);
        const String query = name;
        // The layout controls of an interleaved capture are enumerable but not addressable
        // by name (GL 4.6 §7.3.1.1).
        if (programInterface == GL_TRANSFORM_FEEDBACK_VARYING &&
            (query == "gl_NextBuffer" ||
             (query.size() == 18 && query.compare(0, 17, "gl_SkipComponents") == 0))) {
            return GL_INVALID_INDEX;
        }
        for (SizeT i = 0; i < resources.size(); ++i) {
            if (NamesMatch(resources[i].name, query)) return static_cast<GLuint>(i);
        }
        return GL_INVALID_INDEX;
    }

    Bool GetResourceName(ProgramObject& program, GLenum programInterface, GLuint index, String& outName) {
        const Model model = BuildModel(program);
        const ResourceList& resources = Select(model, programInterface);
        if (index >= resources.size()) return false;
        outName = resources[index].name;
        return true;
    }

    Bool GetResourceProp(ProgramObject& program, GLenum programInterface, GLuint index, GLenum prop,
                         Vector<GLint>& outValues) {
        const Model model = BuildModel(program);
        const ResourceList& resources = Select(model, programInterface);
        if (index >= resources.size()) return false;
        const Resource& resource = resources[index];

        const auto referencedBy = [&resource](EShLanguage stage) {
            return (resource.stages & static_cast<Uint32>(1u << stage)) != 0 ? GL_TRUE : GL_FALSE;
        };

        switch (prop) {
        case GL_NAME_LENGTH:
            outValues.push_back(static_cast<GLint>(resource.name.length() + 1));
            break;
        case GL_TYPE:
            outValues.push_back(static_cast<GLint>(resource.type));
            break;
        case GL_ARRAY_SIZE:
            outValues.push_back(resource.arraySize);
            break;
        case GL_OFFSET:
            outValues.push_back(resource.offset);
            break;
        case GL_BLOCK_INDEX:
            outValues.push_back(resource.blockIndex);
            break;
        case GL_ARRAY_STRIDE:
            outValues.push_back(resource.arrayStride);
            break;
        case GL_MATRIX_STRIDE:
            outValues.push_back(resource.matrixStride);
            break;
        case GL_IS_ROW_MAJOR:
            outValues.push_back(resource.isRowMajor);
            break;
        case GL_ATOMIC_COUNTER_BUFFER_INDEX:
            outValues.push_back(resource.atomicCounterBufferIndex);
            break;
        case GL_BUFFER_BINDING:
            outValues.push_back(resource.bufferBinding);
            break;
        case GL_BUFFER_DATA_SIZE:
            outValues.push_back(resource.bufferDataSize);
            break;
        case GL_NUM_ACTIVE_VARIABLES:
            outValues.push_back(static_cast<GLint>(resource.activeVariables.size()));
            break;
        case GL_ACTIVE_VARIABLES:
            for (const GLuint variable : resource.activeVariables) outValues.push_back(static_cast<GLint>(variable));
            break;
        case GL_REFERENCED_BY_VERTEX_SHADER:
            outValues.push_back(referencedBy(EShLangVertex));
            break;
        case GL_REFERENCED_BY_TESS_CONTROL_SHADER:
            outValues.push_back(referencedBy(EShLangTessControl));
            break;
        case GL_REFERENCED_BY_TESS_EVALUATION_SHADER:
            outValues.push_back(referencedBy(EShLangTessEvaluation));
            break;
        case GL_REFERENCED_BY_GEOMETRY_SHADER:
            outValues.push_back(referencedBy(EShLangGeometry));
            break;
        case GL_REFERENCED_BY_FRAGMENT_SHADER:
            outValues.push_back(referencedBy(EShLangFragment));
            break;
        case GL_REFERENCED_BY_COMPUTE_SHADER:
            outValues.push_back(referencedBy(EShLangCompute));
            break;
        case GL_TOP_LEVEL_ARRAY_SIZE:
            outValues.push_back(resource.topLevelArraySize);
            break;
        case GL_TOP_LEVEL_ARRAY_STRIDE:
            outValues.push_back(resource.topLevelArrayStride);
            break;
        case GL_LOCATION:
            outValues.push_back(resource.location);
            break;
        case GL_LOCATION_INDEX:
            outValues.push_back(resource.locationIndex);
            break;
        case GL_IS_PER_PATCH:
            outValues.push_back(resource.isPerPatch);
            break;
        case GL_LOCATION_COMPONENT:
            outValues.push_back(0);
            break;
        case GL_TRANSFORM_FEEDBACK_BUFFER_INDEX:
            outValues.push_back(resource.xfbBufferIndex);
            break;
        default:
            outValues.push_back(0);
            break;
        }
        return true;
    }

    GLint GetResourceLocation(ProgramObject& program, GLenum programInterface, const char* name) {
        if (name == nullptr || name[0] == '\0') return -1;
        const String query = name;

        String base;
        Uint element = 0;
        Bool malformed = false;
        const Bool subscripted = SplitTrailingSubscript(query, base, element, malformed);
        if (malformed) return -1;

        const Model model = BuildModel(program);
        const ResourceList& resources = Select(model, programInterface);
        for (const Resource& resource : resources) {
            if (NamesMatch(resource.name, query)) return resource.location;
        }
        if (!subscripted || element == 0) return -1;
        // "d[1]" addresses the second element of an array resource enumerated as "d[0]".
        for (const Resource& resource : resources) {
            if (!NamesMatch(resource.name, base)) continue;
            if (resource.location < 0 || static_cast<GLint>(element) >= resource.arraySize) return -1;
            return resource.location + static_cast<GLint>(element);
        }
        return -1;
    }

    GLint GetResourceLocationIndex(ProgramObject& program, GLenum programInterface, const char* name) {
        if (programInterface != GL_PROGRAM_OUTPUT || name == nullptr || name[0] == '\0') return -1;
        const String query = name;
        String base;
        Uint element = 0;
        Bool malformed = false;
        const Bool subscripted = SplitTrailingSubscript(query, base, element, malformed);
        if (malformed) return -1;

        const Model model = BuildModel(program);
        for (const Resource& resource : model.programOutputs) {
            if (NamesMatch(resource.name, query)) return resource.locationIndex;
        }
        if (!subscripted) return -1;
        for (const Resource& resource : model.programOutputs) {
            if (!NamesMatch(resource.name, base)) continue;
            if (resource.location < 0 || static_cast<GLint>(element) >= resource.arraySize) return -1;
            return resource.locationIndex;
        }
        return -1;
    }
} // namespace MobileGL::MG_Impl::GLImpl::ProgramInterface
