//
// Copyright 2020 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.
//

#include <algorithm>
#include <cstring>
#include <numeric>
#include <unordered_map>
#include <unordered_set>

#include "compiler/translator/Compiler.h"
#include "compiler/translator/TranslatorMetalDirect.h"
#include "compiler/translator/TranslatorMetalDirect/AstHelpers.h"
#include "compiler/translator/TranslatorMetalDirect/ModifyStruct.h"

using namespace sh;

////////////////////////////////////////////////////////////////////////////////

size_t ModifiedStructMachineries::size() const
{
    return ordering.size();
}

const ModifiedStructMachinery &ModifiedStructMachineries::at(size_t index) const
{
    ASSERT(index < size());
    const TStructure *s              = ordering[index];
    const ModifiedStructMachinery *m = find(*s);
    ASSERT(m);
    return *m;
}

const ModifiedStructMachinery *ModifiedStructMachineries::find(const TStructure &s) const
{
    auto iter = originalToMachinery.find(&s);
    if (iter == originalToMachinery.end())
    {
        return nullptr;
    }
    return &iter->second;
}

void ModifiedStructMachineries::insert(const TStructure &s,
                                       const ModifiedStructMachinery &machinery)
{
    ASSERT(!find(s));
    originalToMachinery[&s] = machinery;
    ordering.push_back(&s);
}

////////////////////////////////////////////////////////////////////////////////

namespace
{

TIntermTyped &Flatten(SymbolEnv &symbolEnv, TIntermTyped &node)
{
    auto &type = node.getType();
    ASSERT(type.isArray());

    auto &retType = InnermostType(type);
    retType.makeArray(1);

    return symbolEnv.callFunctionOverload(Name("flatten"), retType, *new TIntermSequence{&node});
}

struct FlattenArray
{};

struct PathItem
{
    enum class Type
    {
        Field,         // Struct field indexing.
        Index,         // Array, vector, or matrix indexing.
        FlattenArray,  // Array of any rank -> pointer of innermost type.
    };

    PathItem(const TField &field) : field(&field), type(Type::Field) {}
    PathItem(int index) : index(index), type(Type::Index) {}
    PathItem(unsigned index) : PathItem(static_cast<int>(index)) {}
    PathItem(FlattenArray flatten) : type(Type::FlattenArray) {}

    union
    {
        const TField *field;
        int index;
    };
    Type type;
};

TIntermTyped &BuildPathAccess(SymbolEnv &symbolEnv,
                              const TVariable &var,
                              const std::vector<PathItem> &path)
{
    TIntermTyped *curr = new TIntermSymbol(&var);
    for (const PathItem &item : path)
    {
        switch (item.type)
        {
            case PathItem::Type::Field:
                curr = &AccessField(*curr, item.field->name());
                break;
            case PathItem::Type::Index:
                curr = &AccessIndex(*curr, item.index);
                break;
            case PathItem::Type::FlattenArray:
            {
                curr = &Flatten(symbolEnv, *curr);
            }
            break;
        }
    }
    return *curr;
}

////////////////////////////////////////////////////////////////////////////////

using OriginalParam = const TVariable &;
using ModifiedParam = const TVariable &;

using OriginalAccess = TIntermTyped;
using ModifiedAccess = TIntermTyped;

struct Access
{
    OriginalAccess &original;
    ModifiedAccess &modified;

    struct Env
    {
        const ConvertType type;
    };
};

using ConversionFunc = std::function<Access(Access::Env &, OriginalAccess &, ModifiedAccess &)>;

class ConvertStructState : angle::NonCopyable
{
  private:
    struct ConversionInfo
    {
        ConversionFunc stdFunc;
        const TFunction *astFunc;
        std::vector<PathItem> pathItems;
        ImmutableString pathName;
    };

  public:
    ConvertStructState(TCompiler &compiler,
                       SymbolEnv &symbolEnv,
                       IdGen &idGen,
                       const ModifyStructConfig &config,
                       ModifiedStructMachineries &outMachineries,
                       const bool isUBO)
        : mCompiler(compiler),
          config(config),
          symbolEnv(symbolEnv),
          modifiedFields(*new TFieldList()),
          symbolTable(symbolEnv.symbolTable()),
          idGen(idGen),
          outMachineries(outMachineries),
          isUBO(isUBO)
    {}

    ~ConvertStructState()
    {
        ASSERT(namePath.empty());
        ASSERT(namePathSizes.empty());
    }

    void publish(const TStructure &originalStruct, const Name &modifiedStructName)
    {
        const bool isOriginalToModified = config.convertType == ConvertType::OriginalToModified;

        auto &modifiedStruct = *new TStructure(&symbolTable, modifiedStructName.rawName(),
                                               &modifiedFields, modifiedStructName.symbolType());

        auto &func = *new TFunction(
            &symbolTable,
            idGen.createNewName(isOriginalToModified ? "originalToModified" : "modifiedToOriginal")
                .rawName(),
            SymbolType::AngleInternal, new TType(TBasicType::EbtVoid), false);

        OriginalParam originalParam =
            CreateInstanceVariable(symbolTable, originalStruct, Name("original"));
        ModifiedParam modifiedParam =
            CreateInstanceVariable(symbolTable, modifiedStruct, Name("modified"));

        symbolEnv.markAsReference(originalParam, AddressSpace::Thread);
        symbolEnv.markAsReference(modifiedParam, config.externalAddressSpace);
        if (isOriginalToModified)
        {
            func.addParameter(&originalParam);
            func.addParameter(&modifiedParam);
        }
        else
        {
            func.addParameter(&modifiedParam);
            func.addParameter(&originalParam);
        }

        TIntermBlock &body = *new TIntermBlock();

        Access::Env env{config.convertType};

        for (ConversionInfo &info : conversionInfos)
        {
            auto convert = [&](OriginalAccess &original, ModifiedAccess &modified) {
                if (info.astFunc)
                {
                    ASSERT(!info.stdFunc);
                    TIntermTyped &src  = isOriginalToModified ? modified : original;
                    TIntermTyped &dest = isOriginalToModified ? original : modified;
                    body.appendStatement(TIntermAggregate::CreateFunctionCall(
                        *info.astFunc, new TIntermSequence{&dest, &src}));
                }
                else
                {
                    ASSERT(info.stdFunc);
                    Access access      = info.stdFunc(env, original, modified);
                    TIntermTyped &src  = isOriginalToModified ? access.original : access.modified;
                    TIntermTyped &dest = isOriginalToModified ? access.modified : access.original;
                    body.appendStatement(new TIntermBinary(TOperator::EOpAssign, &dest, &src));
                }
            };

            OriginalAccess *original = &BuildPathAccess(symbolEnv, originalParam, info.pathItems);
            ModifiedAccess *modified = &AccessField(modifiedParam, info.pathName);

            const TType ot = original->getType();
            const TType mt = modified->getType();
            ASSERT(ot.isArray() == mt.isArray());

            if (ot.isArray() && (ot.getLayoutQualifier().matrixPacking == EmpRowMajor || ot != mt))
            {
                ASSERT(ot.getArraySizes() == mt.getArraySizes());
                if (ot.isArrayOfArrays())
                {
                    original = &Flatten(symbolEnv, *original);
                    modified = &Flatten(symbolEnv, *modified);
                }
                const int volume = static_cast<int>(ot.getArraySizeProduct());
                for (int i = 0; i < volume; ++i)
                {
                    if (i != 0)
                    {
                        original = original->deepCopy();
                        modified = modified->deepCopy();
                    }
                    OriginalAccess &o = AccessIndex(*original, i);
                    OriginalAccess &m = AccessIndex(*modified, i);
                    convert(o, m);
                }
            }
            else
            {
                convert(*original, *modified);
            }
        }

        auto *funcProto = new TIntermFunctionPrototype(&func);
        auto *funcDef   = new TIntermFunctionDefinition(funcProto, &body);

        ModifiedStructMachinery machinery;
        machinery.modifiedStruct                   = &modifiedStruct;
        machinery.getConverter(config.convertType) = funcDef;

        outMachineries.insert(originalStruct, machinery);
    }

    void pushPath(PathItem const &item)
    {
        pathItems.push_back(item);

        switch (item.type)
        {
            case PathItem::Type::Field:
                pushNamePath(item.field->name().data());
                break;

            case PathItem::Type::Index:
                pushNamePath(item.index);
                break;

            case PathItem::Type::FlattenArray:
                namePathSizes.push_back(namePath.size());
                break;
        }
    }

    void popPath()
    {
        ASSERT(!namePath.empty());
        ASSERT(!namePathSizes.empty());
        namePath.resize(namePathSizes.back());
        namePathSizes.pop_back();

        ASSERT(!pathItems.empty());
        pathItems.pop_back();
    }

    void finalize(const bool allowPadding)
    {
        ASSERT(!finalized);
        finalized = true;
        introducePacking();
        ASSERT(metalLayoutTotal == Layout::Identity());
        // Only pad substructs. We don't want to pad the structure that contains all the UBOs, only
        // individual UBOs.
        if (allowPadding)
            introducePadding();
    }

    void addModifiedField(const TField &field,
                          TType &newType,
                          TLayoutBlockStorage storage,
                          TLayoutMatrixPacking packing,
                          const AddressSpace *addressSpace)
    {
        TLayoutQualifier layoutQualifier = newType.getLayoutQualifier();
        layoutQualifier.blockStorage     = storage;
        layoutQualifier.matrixPacking    = packing;
        newType.setLayoutQualifier(layoutQualifier);

        const ImmutableString pathName(namePath);
        TField *modifiedField = new TField(&newType, pathName, field.line(), field.symbolType());
        if (addressSpace)
        {
            symbolEnv.markAsPointer(*modifiedField, *addressSpace);
        }
        if (symbolEnv.isUBO(field))
        {
            symbolEnv.markAsUBO(*modifiedField);
        }
        modifiedFields.push_back(modifiedField);
    }

    void addConversion(const ConversionFunc &func)
    {
        ASSERT(!modifiedFields.empty());
        conversionInfos.push_back({func, nullptr, pathItems, modifiedFields.back()->name()});
    }

    void addConversion(const TFunction &func)
    {
        ASSERT(!modifiedFields.empty());
        conversionInfos.push_back({{}, &func, pathItems, modifiedFields.back()->name()});
    }

    bool hasPacking() const { return containsPacked; }

    bool hasPadding() const { return padFieldCount > 0; }

    bool recurse(const TStructure &structure,
                 ModifiedStructMachinery &outMachinery,
                 const bool isUBO)
    {
        const ModifiedStructMachinery *m = outMachineries.find(structure);
        if (m == nullptr)
        {
            TranslatorMetalReflection *reflection =
                ((sh::TranslatorMetalDirect *)&mCompiler)->getTranslatorMetalReflection();
            reflection->addOriginalName(structure.uniqueId().get(), structure.name().data());
            const Name name = idGen.createNewName(structure.name().data());
            if (!TryCreateModifiedStruct(mCompiler, symbolEnv, idGen, config, structure, name,
                                         outMachineries, isUBO, true))
            {
                return false;
            }
            m = outMachineries.find(structure);
            ASSERT(m);
        }
        outMachinery = *m;
        return true;
    }

    bool getIsUBO() const { return isUBO; }

  private:
    void addPadding(size_t padAmount, bool updateLayout)
    {
        if (padAmount == 0)
        {
            return;
        }

        const size_t begin = modifiedFields.size();

        // Iteratively adding in scalar or vector padding because some struct types will not
        // allow matrix or array members.
        while (padAmount > 0)
        {
            TType *padType;
            if (padAmount >= 16)
            {
                padAmount -= 16;
                padType = new TType(TBasicType::EbtFloat, 4);
            }
            else if (padAmount >= 8)
            {
                padAmount -= 8;
                padType = new TType(TBasicType::EbtFloat, 2);
            }
            else if (padAmount >= 4)
            {
                padAmount -= 4;
                padType = new TType(TBasicType::EbtFloat);
            }
            else if (padAmount >= 2)
            {
                padAmount -= 2;
                padType = new TType(TBasicType::EbtBool, 2);
            }
            else
            {
                ASSERT(padAmount == 1);
                padAmount -= 1;
                padType = new TType(TBasicType::EbtBool);
            }

            if (updateLayout)
            {
                metalLayoutTotal += MetalLayoutOf(*padType);
            }

            const Name name = idGen.createNewName("pad");
            modifiedFields.push_back(
                new TField(padType, name.rawName(), kNoSourceLoc, name.symbolType()));
            ++padFieldCount;
        }

        std::reverse(modifiedFields.begin() + begin, modifiedFields.end());
    }

    void introducePacking()
    {
        if (!config.allowPacking)
        {
            return;
        }

        auto setUnpackedStorage = [](TType &type) {
            TLayoutBlockStorage storage = type.getLayoutQualifier().blockStorage;
            switch (storage)
            {
                case TLayoutBlockStorage::EbsShared:
                    storage = TLayoutBlockStorage::EbsStd140;
                    break;
                case TLayoutBlockStorage::EbsPacked:
                    storage = TLayoutBlockStorage::EbsStd430;
                    break;
                case TLayoutBlockStorage::EbsStd140:
                case TLayoutBlockStorage::EbsStd430:
                case TLayoutBlockStorage::EbsUnspecified:
                    break;
            }
            SetBlockStorage(type, storage);
        };

        Layout glslLayoutTotal = Layout::Identity();
        const size_t size      = modifiedFields.size();

        for (size_t i = 0; i < size; ++i)
        {
            TField &curr           = *modifiedFields[i];
            TType &currType        = *curr.type();
            const bool canBePacked = CanBePacked(currType);

            auto dontPack = [&]() {
                if (canBePacked)
                {
                    setUnpackedStorage(currType);
                }
                glslLayoutTotal += GlslLayoutOf(currType);
            };

            if (!CanBePacked(currType))
            {
                dontPack();
                continue;
            }

            const Layout packedGlslLayout           = GlslLayoutOf(currType);
            const TLayoutBlockStorage packedStorage = currType.getLayoutQualifier().blockStorage;
            setUnpackedStorage(currType);
            const Layout unpackedGlslLayout = GlslLayoutOf(currType);
            SetBlockStorage(currType, packedStorage);

            ASSERT(packedGlslLayout.sizeOf <= unpackedGlslLayout.sizeOf);
            if (packedGlslLayout.sizeOf == unpackedGlslLayout.sizeOf)
            {
                dontPack();
                continue;
            }

            const size_t j = i + 1;
            if (j == size)
            {
                dontPack();
                break;
            }

            const size_t pad            = unpackedGlslLayout.sizeOf - packedGlslLayout.sizeOf;
            const TField &next          = *modifiedFields[j];
            const Layout nextGlslLayout = GlslLayoutOf(*next.type());

            if (pad < nextGlslLayout.sizeOf)
            {
                dontPack();
                continue;
            }

            symbolEnv.markAsPacked(curr);
            glslLayoutTotal += packedGlslLayout;
            containsPacked = true;
        }
    }

    void introducePadding()
    {
        if (!config.allowPadding)
        {
            return;
        }

        MetalLayoutOfConfig layoutConfig;
        layoutConfig.disablePacking             = !config.allowPacking;
        layoutConfig.assumeStructsAreTailPadded = true;

        TFieldList fields = std::move(modifiedFields);
        ASSERT(!fields.empty());  // GLSL requires at least one member.

        const TField *const first = fields.front();

        for (TField *field : fields)
        {
            const TType &type = *field->type();

            const Layout glslLayout  = GlslLayoutOf(type);
            const Layout metalLayout = MetalLayoutOf(type, layoutConfig);

            size_t prePadAmount = 0;
            if (glslLayout.alignOf > metalLayout.alignOf && field != first)
            {
                const size_t prePaddedSize = metalLayoutTotal.sizeOf;
                metalLayoutTotal.requireAlignment(glslLayout.alignOf, true);
                const size_t paddedSize = metalLayoutTotal.sizeOf;
                prePadAmount            = paddedSize - prePaddedSize;
                metalLayoutTotal += metalLayout;
                addPadding(prePadAmount, false);  // Note: requireAlignment() already updated layout
            }
            else
            {
                metalLayoutTotal += metalLayout;
            }

            modifiedFields.push_back(field);

            if (glslLayout.sizeOf > metalLayout.sizeOf && field != fields.back())
            {
                const bool updateLayout = true;  // XXX: Correct?
                const size_t padAmount  = glslLayout.sizeOf - metalLayout.sizeOf;
                addPadding(padAmount, updateLayout);
            }
        }
    }

    void pushNamePath(const char *extra)
    {
        ASSERT(extra && *extra != '\0');
        namePathSizes.push_back(namePath.size());
        const char *p = extra;
        if (namePath.empty())
        {
            namePath = p;
            return;
        }
        while (*p == '_')
        {
            ++p;
        }
        if (*p == '\0')
        {
            p = "x";
        }
        if (namePath.back() != '_')
        {
            namePath += '_';
        }
        namePath += p;
    }

    void pushNamePath(unsigned extra)
    {
        char buffer[std::numeric_limits<unsigned>::digits10 + 1];
        sprintf(buffer, "%u", extra);
        pushNamePath(buffer);
    }

  public:
    TCompiler &mCompiler;
    const ModifyStructConfig &config;
    SymbolEnv &symbolEnv;

  private:
    TFieldList &modifiedFields;
    Layout metalLayoutTotal = Layout::Identity();
    size_t padFieldCount    = 0;
    bool containsPacked     = false;
    bool finalized          = false;

    std::vector<PathItem> pathItems;

    std::vector<size_t> namePathSizes;
    std::string namePath;

    std::vector<ConversionInfo> conversionInfos;
    TSymbolTable &symbolTable;
    IdGen &idGen;
    ModifiedStructMachineries &outMachineries;
    const bool isUBO;
};

////////////////////////////////////////////////////////////////////////////////

using ModifyFunc = bool (*)(ConvertStructState &state,
                            const TField &field,
                            const TLayoutBlockStorage storage,
                            const TLayoutMatrixPacking packing);

bool ModifyRecursive(ConvertStructState &state,
                     const TField &field,
                     const TLayoutBlockStorage storage,
                     const TLayoutMatrixPacking packing);

bool IdentityModify(ConvertStructState &state,
                    const TField &field,
                    const TLayoutBlockStorage storage,
                    const TLayoutMatrixPacking packing)
{
    const TType &type = *field.type();
    state.addModifiedField(field, CloneType(type), storage, packing, nullptr);
    state.addConversion([=](Access::Env &, OriginalAccess &o, ModifiedAccess &m) {
        return Access{o, m};
    });
    return false;
}

bool InlineStruct(ConvertStructState &state,
                  const TField &field,
                  const TLayoutBlockStorage storage,
                  const TLayoutMatrixPacking packing)
{
    const TType &type              = *field.type();
    const TStructure *substructure = state.symbolEnv.remap(type.getStruct());
    if (!substructure)
    {
        return false;
    }
    if (type.isArray())
    {
        return false;
    }
    if (!state.config.inlineStruct(field))
    {
        return false;
    }

    const TFieldList &subfields = substructure->fields();
    for (const TField *subfield : subfields)
    {
        const TType &subtype                  = *subfield->type();
        const TLayoutBlockStorage substorage  = Overlay(storage, subtype);
        const TLayoutMatrixPacking subpacking = Overlay(packing, subtype);
        ModifyRecursive(state, *subfield, substorage, subpacking);
    }

    return true;
}

bool RecurseStruct(ConvertStructState &state,
                   const TField &field,
                   const TLayoutBlockStorage storage,
                   const TLayoutMatrixPacking packing)
{
    const TType &type              = *field.type();
    const TStructure *substructure = state.symbolEnv.remap(type.getStruct());
    if (!substructure)
    {
        return false;
    }
    if (!state.config.recurseStruct(field))
    {
        return false;
    }

    ModifiedStructMachinery machinery;
    if (!state.recurse(*substructure, machinery, state.getIsUBO()))
    {
        return false;
    }

    TType &newType = *new TType(machinery.modifiedStruct, false);
    if (type.isArray())
    {
        newType.makeArrays(type.getArraySizes());
    }

    TIntermFunctionDefinition *converter = machinery.getConverter(state.config.convertType);
    ASSERT(converter);

    state.addModifiedField(field, newType, storage, packing, state.symbolEnv.isPointer(field));
    if (state.symbolEnv.isPointer(field))
    {
        state.symbolEnv.removePointer(field);
    }
    state.addConversion(*converter->getFunction());

    return true;
}

bool SplitMatrixColumns(ConvertStructState &state,
                        const TField &field,
                        const TLayoutBlockStorage storage,
                        const TLayoutMatrixPacking packing)
{
    const TType &type = *field.type();
    if (!type.isMatrix())
    {
        return false;
    }

    if (!state.config.splitMatrixColumns(field))
    {
        return false;
    }

    const int cols = type.getCols();
    TType &rowType = DropColumns(type);

    for (int c = 0; c < cols; ++c)
    {
        state.pushPath(c);

        state.addModifiedField(field, rowType, storage, packing, state.symbolEnv.isPointer(field));
        if (state.symbolEnv.isPointer(field))
        {
            state.symbolEnv.removePointer(field);
        }
        state.addConversion([=](Access::Env &, OriginalAccess &o, ModifiedAccess &m) {
            return Access{o, m};
        });

        state.popPath();
    }

    return true;
}

bool SaturateMatrixRows(ConvertStructState &state,
                        const TField &field,
                        const TLayoutBlockStorage storage,
                        const TLayoutMatrixPacking packing)
{
    const TType &type = *field.type();
    if (!type.isMatrix())
    {
        return false;
    }
    const bool isRowMajor = type.getLayoutQualifier().matrixPacking == EmpRowMajor;
    const int rows        = type.getRows();
    const int saturation  = state.config.saturateMatrixRows(field);
    if (saturation <= rows && !isRowMajor)
    {
        return false;
    }

    const int cols = type.getCols();
    TType &satType = SetMatrixRowDim(type, saturation);
    state.addModifiedField(field, satType, storage, packing, state.symbolEnv.isPointer(field));
    if (state.symbolEnv.isPointer(field))
    {
        state.symbolEnv.removePointer(field);
    }

    for (int c = 0; c < cols; ++c)
    {
        for (int r = 0; r < rows; ++r)
        {
            state.addConversion([=](Access::Env &, OriginalAccess &o, ModifiedAccess &m) {
                int firstModifiedIndex  = isRowMajor ? r : c;
                int secondModifiedIndex = isRowMajor ? c : r;
                auto &o_                = AccessIndex(AccessIndex(o, c), r);
                auto &m_ = AccessIndex(AccessIndex(m, firstModifiedIndex), secondModifiedIndex);
                return Access{o_, m_};
            });
        }
    }

    return true;
}

bool TestBoolToUint(ConvertStructState &state, const TField &field)
{
    if (field.type()->getBasicType() != TBasicType::EbtBool)
    {
        return false;
    }
    if (!state.config.promoteBoolToUint(field))
    {
        return false;
    }
    return true;
}

Access ConvertBoolToUint(ConvertType convertType, OriginalAccess &o, ModifiedAccess &m)
{
    auto coerce = [](TIntermTyped &to, TIntermTyped &from) -> TIntermTyped & {
        return *TIntermAggregate::CreateConstructor(to.getType(), new TIntermSequence{&from});
    };
    switch (convertType)
    {
        case ConvertType::OriginalToModified:
            return Access{coerce(m, o), m};
        case ConvertType::ModifiedToOriginal:
            return Access{o, coerce(o, m)};
    }
}

bool SaturateScalarOrVectorCommon(ConvertStructState &state,
                                  const TField &field,
                                  const TLayoutBlockStorage storage,
                                  const TLayoutMatrixPacking packing,
                                  const bool array)
{
    const TType &type = *field.type();
    if (type.isArray() != array)
    {
        return false;
    }
    if (!((type.isRank0() && HasScalarBasicType(type)) || type.isVector()))
    {
        return false;
    }
    const auto saturator =
        array ? state.config.saturateScalarOrVectorArrays : state.config.saturateScalarOrVector;
    const int dim        = type.getNominalSize();
    const int saturation = saturator(field);
    if (saturation <= dim)
    {
        return false;
    }

    TType &satType        = SetVectorDim(type, saturation);
    const bool boolToUint = TestBoolToUint(state, field);
    if (boolToUint)
    {
        satType.setBasicType(TBasicType::EbtUInt);
    }
    state.addModifiedField(field, satType, storage, packing, state.symbolEnv.isPointer(field));
    if (state.symbolEnv.isPointer(field))
    {
        state.symbolEnv.removePointer(field);
    }

    for (int d = 0; d < dim; ++d)
    {
        state.addConversion([=](Access::Env &env, OriginalAccess &o, ModifiedAccess &m) {
            auto &o_ = dim > 1 ? AccessIndex(o, d) : o;
            auto &m_ = AccessIndex(m, d);
            if (boolToUint)
            {
                return ConvertBoolToUint(env.type, o_, m_);
            }
            else
            {
                return Access{o_, m_};
            }
        });
    }

    return true;
}

bool SaturateScalarOrVectorArrays(ConvertStructState &state,
                                  const TField &field,
                                  const TLayoutBlockStorage storage,
                                  const TLayoutMatrixPacking packing)
{
    return SaturateScalarOrVectorCommon(state, field, storage, packing, true);
}

bool SaturateScalarOrVector(ConvertStructState &state,
                            const TField &field,
                            const TLayoutBlockStorage storage,
                            const TLayoutMatrixPacking packing)
{
    return SaturateScalarOrVectorCommon(state, field, storage, packing, false);
}

bool PromoteBoolToUint(ConvertStructState &state,
                       const TField &field,
                       const TLayoutBlockStorage storage,
                       const TLayoutMatrixPacking packing)
{
    if (!TestBoolToUint(state, field))
    {
        return false;
    }

    auto &promotedType = CloneType(*field.type());
    promotedType.setBasicType(TBasicType::EbtUInt);
    state.addModifiedField(field, promotedType, storage, packing, state.symbolEnv.isPointer(field));
    if (state.symbolEnv.isPointer(field))
    {
        state.symbolEnv.removePointer(field);
    }

    state.addConversion([=](Access::Env &env, OriginalAccess &o, ModifiedAccess &m) {
        return ConvertBoolToUint(env.type, o, m);
    });

    return true;
}

bool ModifyCommon(ConvertStructState &state,
                  const TField &field,
                  const TLayoutBlockStorage storage,
                  const TLayoutMatrixPacking packing)
{
    ModifyFunc funcs[] = {
        InlineStruct,                  //
        RecurseStruct,                 //
        SplitMatrixColumns,            //
        SaturateMatrixRows,            //
        SaturateScalarOrVectorArrays,  //
        SaturateScalarOrVector,        //
        PromoteBoolToUint,             //
    };

    for (ModifyFunc func : funcs)
    {
        if (func(state, field, storage, packing))
        {
            return true;
        }
    }

    return IdentityModify(state, field, storage, packing);
}

bool InlineArray(ConvertStructState &state,
                 const TField &field,
                 const TLayoutBlockStorage storage,
                 const TLayoutMatrixPacking packing)
{
    const TType &type = *field.type();
    if (!type.isArray())
    {
        return false;
    }
    if (!state.config.inlineArray(field))
    {
        return false;
    }

    const unsigned volume = type.getArraySizeProduct();
    const bool isMultiDim = type.isArrayOfArrays();

    auto &innermostType = InnermostType(type);
    const TField innermostField(&innermostType, field.name(), field.line(), field.symbolType());

    if (isMultiDim)
    {
        state.pushPath(FlattenArray());
    }

    for (unsigned i = 0; i < volume; ++i)
    {
        state.pushPath(i);
        ModifyCommon(state, innermostField, storage, packing);
        state.popPath();
    }

    if (isMultiDim)
    {
        state.popPath();
    }

    return true;
}

bool ModifyRecursive(ConvertStructState &state,
                     const TField &field,
                     const TLayoutBlockStorage storage,
                     const TLayoutMatrixPacking packing)
{
    state.pushPath(field);

    bool modified;
    if (InlineArray(state, field, storage, packing))
    {
        modified = true;
    }
    else
    {
        modified = ModifyCommon(state, field, storage, packing);
    }

    state.popPath();

    return modified;
}

}  // anonymous namespace

////////////////////////////////////////////////////////////////////////////////

bool sh::TryCreateModifiedStruct(TCompiler &compiler,
                                 SymbolEnv &symbolEnv,
                                 IdGen &idGen,
                                 const ModifyStructConfig &config,
                                 const TStructure &originalStruct,
                                 const Name &modifiedStructName,
                                 ModifiedStructMachineries &outMachineries,
                                 const bool isUBO,
                                 const bool allowPadding)
{
    ConvertStructState state(compiler, symbolEnv, idGen, config, outMachineries, isUBO);
    size_t identicalFieldCount = 0;

    const TFieldList &originalFields = originalStruct.fields();
    for (TField *originalField : originalFields)
    {
        const TType &originalType          = *originalField->type();
        const TLayoutBlockStorage storage  = Overlay(config.initialBlockStorage, originalType);
        const TLayoutMatrixPacking packing = Overlay(config.initialMatrixPacking, originalType);
        if (!ModifyRecursive(state, *originalField, storage, packing))
        {
            ++identicalFieldCount;
        }
    }

    state.finalize(allowPadding);

    if (identicalFieldCount == originalFields.size() && !state.hasPacking() &&
        !state.hasPadding() && !isUBO)
    {
        return false;
    }

    state.publish(originalStruct, modifiedStructName);

    return true;
}
