//
// 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 <cctype>
#include <cstring>
#include <limits>
#include <unordered_map>
#include <unordered_set>

#include "common/unsafe_buffers.h"
#include "compiler/translator/Symbol.h"
#include "compiler/translator/msl/Layout.h"

using namespace sh;

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

enum class Language
{
    Metal,
    GLSL,
};

static size_t RoundUpToMultipleOf(size_t x, size_t multiple)
{
    const size_t rem = x % multiple;
    return rem == 0 ? x : x + (multiple - rem);
}

void Layout::requireAlignment(size_t align, bool pad)
{
    alignOf = std::max(alignOf, align);
    if (pad)
    {
        sizeOf = RoundUpToMultipleOf(sizeOf, align);
    }
}

bool Layout::operator==(const Layout &other) const
{
    return sizeOf == other.sizeOf && alignOf == other.alignOf;
}

void Layout::operator+=(const Layout &other)
{
    requireAlignment(other.alignOf, true);
    sizeOf += other.sizeOf;
}

void Layout::operator*=(size_t scale)
{
    sizeOf *= scale;
}

Layout Layout::operator+(const Layout &other) const
{
    auto self = *this;
    self += other;
    return self;
}

Layout Layout::operator*(size_t scale) const
{
    auto self = *this;
    self *= scale;
    return self;
}

static Layout ScalarLayoutOf(const TType &type, Language language)
{
    const TBasicType basicType = type.getBasicType();
    switch (basicType)
    {
        case TBasicType::EbtBool:
            return {1, 1};
        case TBasicType::EbtInt:
        case TBasicType::EbtUInt:
        case TBasicType::EbtFloat:
            return {4, 4};
        default:
            if (IsSampler(basicType))
            {
                switch (language)
                {
                    case Language::Metal:
                        return {8, 8};
                    case Language::GLSL:
                        return {4, 4};
                }
            }
            UNIMPLEMENTED();
            return Layout::Invalid();
    }
}

static const size_t innerScalesPacked[]   = {0, 1, 2, 3, 4};
static const size_t innerScalesUnpacked[] = {0, 1, 2, 4, 4};

Layout sh::MetalLayoutOf(const TType &type, MetalLayoutOfConfig config)
{
    ASSERT(type.getNominalSize() <= 4);
    ASSERT(type.getSecondarySize() <= 4);

    const TLayoutBlockStorage storage = type.getLayoutQualifier().blockStorage;

    const bool isPacked = !config.disablePacking && (storage == TLayoutBlockStorage::EbsPacked ||
                                                     storage == TLayoutBlockStorage::EbsShared);

    if (type.isArray() && !config.maskArray)
    {
        config.maskArray    = true;
        const Layout layout = MetalLayoutOf(type, config);
        config.maskArray    = false;
        const size_t vol    = type.getArraySizeProduct();
        return layout * vol;
    }

    if (const TStructure *structure = type.getStruct())
    {
        ASSERT(type.getNominalSize() == 1);
        ASSERT(type.getSecondarySize() == 1);
        auto config2             = config;
        config2.maskArray        = false;
        auto layout              = Layout::Identity();
        const TFieldList &fields = structure->fields();
        for (const TField *field : fields)
        {
            layout += MetalLayoutOf(*field->type(), config2);
        }
        if (config.assumeStructsAreTailPadded)
        {
            size_t pad =
                (kDefaultStructAlignmentSize - layout.sizeOf) % kDefaultStructAlignmentSize;
            layout.sizeOf += pad;
        }
        layout.sizeOf = RoundUpToMultipleOf(layout.sizeOf, layout.alignOf);
        return layout;
    }

    if (config.treatSamplersAsTextureEnv && IsSampler(type.getBasicType()))
    {
        return {16, 8};  // pointer{8,8} and metal::sampler{8,8}
    }

    const Layout scalarLayout = ScalarLayoutOf(type, Language::Metal);

    if (type.isRank0())
    {
        return scalarLayout;
    }
    else if (type.isVector())
    {
        if (isPacked)
        {
            const size_t innerScale = ANGLE_UNSAFE_TODO(innerScalesPacked[type.getNominalSize()]);
            auto layout = Layout{scalarLayout.sizeOf * innerScale, scalarLayout.alignOf};
            return layout;
        }
        else
        {
            const size_t innerScale = ANGLE_UNSAFE_TODO(innerScalesUnpacked[type.getNominalSize()]);
            auto layout             = Layout::Both(scalarLayout.sizeOf * innerScale);
            return layout;
        }
    }
    else
    {
        ASSERT(type.isMatrix());
        ASSERT(type.getBasicType() == TBasicType::EbtFloat);
        // typeCxR <=> typeR[C]
        const size_t innerScale = ANGLE_UNSAFE_TODO(innerScalesUnpacked[type.getRows()]);
        const size_t outerScale = static_cast<size_t>(type.getCols());
        const size_t n          = scalarLayout.sizeOf * innerScale;
        return {n * outerScale, n};
    }
}

TLayoutBlockStorage sh::Overlay(TLayoutBlockStorage oldStorage, const TType &type)
{
    const TLayoutBlockStorage newStorage = type.getLayoutQualifier().blockStorage;
    switch (newStorage)
    {
        case TLayoutBlockStorage::EbsUnspecified:
            return oldStorage == TLayoutBlockStorage::EbsUnspecified ? kDefaultLayoutBlockStorage
                                                                     : oldStorage;
        default:
            return newStorage;
    }
}

TLayoutMatrixPacking sh::Overlay(TLayoutMatrixPacking oldPacking, const TType &type)
{
    const TLayoutMatrixPacking newPacking = type.getLayoutQualifier().matrixPacking;
    switch (newPacking)
    {
        case TLayoutMatrixPacking::EmpUnspecified:
            return oldPacking == TLayoutMatrixPacking::EmpUnspecified ? kDefaultLayoutMatrixPacking
                                                                      : oldPacking;
        default:
            return newPacking;
    }
}

bool sh::CanBePacked(TLayoutBlockStorage storage)
{
    switch (storage)
    {
        case TLayoutBlockStorage::EbsPacked:
        case TLayoutBlockStorage::EbsShared:
            return true;
        case TLayoutBlockStorage::EbsStd140:
        case TLayoutBlockStorage::EbsStd430:
            return false;
        case TLayoutBlockStorage::EbsUnspecified:
            UNREACHABLE();
            return false;
    }
}

bool sh::CanBePacked(TLayoutQualifier layoutQualifier)
{
    return CanBePacked(layoutQualifier.blockStorage);
}

bool sh::CanBePacked(const TType &type)
{
    if (!type.isVector())
    {
        return false;
    }
    return CanBePacked(type.getLayoutQualifier());
}

void sh::SetBlockStorage(TType &type, TLayoutBlockStorage storage)
{
    auto qual         = type.getLayoutQualifier();
    qual.blockStorage = storage;
    type.setLayoutQualifier(qual);
}

static Layout CommonGlslStructLayoutOf(TField const *const *begin,
                                       TField const *const *end,
                                       const TLayoutBlockStorage storage,
                                       const TLayoutMatrixPacking matrixPacking,
                                       const bool maskArray,
                                       const size_t baseAlignment)
{
    const bool isPacked =
        storage == TLayoutBlockStorage::EbsPacked || storage == TLayoutBlockStorage::EbsShared;

    auto layout = Layout::Identity();
    for (auto iter = begin; iter != end; ANGLE_UNSAFE_TODO(++iter))
    {
        layout += GlslLayoutOf(*(*iter)->type(), storage, matrixPacking, false);
    }
    if (!isPacked)  // XXX: Correct?
    {
        layout.sizeOf = RoundUpToMultipleOf(layout.sizeOf, layout.alignOf);
    }
    layout.requireAlignment(baseAlignment, true);
    return layout;
}

static Layout CommonGlslLayoutOf(const TType &type,
                                 const TLayoutBlockStorage storage,
                                 const TLayoutMatrixPacking matrixPacking,
                                 const bool maskArray,
                                 const size_t baseAlignment)
{
    ASSERT(storage != TLayoutBlockStorage::EbsUnspecified);

    const bool isPacked =
        storage == TLayoutBlockStorage::EbsPacked || storage == TLayoutBlockStorage::EbsShared;

    if (type.isArray() && !type.isMatrix() && !maskArray)
    {
        auto layout = GlslLayoutOf(type, storage, matrixPacking, true);
        layout *= type.getArraySizeProduct();
        layout.requireAlignment(baseAlignment, true);
        return layout;
    }

    if (const TStructure *structure = type.getStruct())
    {
        ASSERT(type.getNominalSize() == 1);
        ASSERT(type.getSecondarySize() == 1);
        const TFieldList &fields = structure->fields();
        return CommonGlslStructLayoutOf(fields.data(),
                                        ANGLE_UNSAFE_TODO(fields.data() + fields.size()), storage,
                                        matrixPacking, maskArray, baseAlignment);
    }

    const auto scalarLayout = ScalarLayoutOf(type, Language::GLSL);

    if (type.isRank0())
    {
        return scalarLayout;
    }
    else if (type.isVector())
    {
        if (isPacked)
        {
            const size_t sizeScale  = ANGLE_UNSAFE_TODO(innerScalesPacked[type.getNominalSize()]);
            const size_t alignScale = ANGLE_UNSAFE_TODO(innerScalesUnpacked[type.getNominalSize()]);
            auto layout =
                Layout{scalarLayout.sizeOf * sizeScale, scalarLayout.alignOf * alignScale};
            return layout;
        }
        else
        {
            const size_t innerScale = ANGLE_UNSAFE_TODO(innerScalesUnpacked[type.getNominalSize()]);
            auto layout             = Layout::Both(scalarLayout.sizeOf * innerScale);
            return layout;
        }
    }
    else
    {
        ASSERT(type.isMatrix());

        size_t innerDim;
        size_t outerDim;

        switch (matrixPacking)
        {
            case TLayoutMatrixPacking::EmpColumnMajor:
                innerDim = static_cast<size_t>(type.getRows());
                outerDim = static_cast<size_t>(type.getCols());
                break;
            case TLayoutMatrixPacking::EmpRowMajor:
                innerDim = static_cast<size_t>(type.getCols());
                outerDim = static_cast<size_t>(type.getRows());
                break;
            case TLayoutMatrixPacking::EmpUnspecified:
                UNREACHABLE();
                innerDim = 0;
                outerDim = 0;
        }

        outerDim *= type.getArraySizeProduct();

        const size_t innerScale = ANGLE_UNSAFE_TODO(innerScalesUnpacked[innerDim]);
        const size_t n          = innerScale * scalarLayout.sizeOf;
        Layout layout           = {outerDim * n, n};
        layout.requireAlignment(baseAlignment, true);
        return layout;
    }
}

Layout sh::GlslLayoutOf(const TType &type,
                        TLayoutBlockStorage storage,
                        TLayoutMatrixPacking matrixPacking,
                        bool maskArray)
{
    ASSERT(type.getNominalSize() <= 4);
    ASSERT(type.getSecondarySize() <= 4);

    storage       = Overlay(storage, type);
    matrixPacking = Overlay(matrixPacking, type);

    switch (storage)
    {
        case TLayoutBlockStorage::EbsPacked:
            return CommonGlslLayoutOf(type, storage, matrixPacking, maskArray, 1);
        case TLayoutBlockStorage::EbsShared:
            return CommonGlslLayoutOf(type, storage, matrixPacking, maskArray, 16);
        case TLayoutBlockStorage::EbsStd140:
            return CommonGlslLayoutOf(type, storage, matrixPacking, maskArray, 16);
        case TLayoutBlockStorage::EbsStd430:
            return CommonGlslLayoutOf(type, storage, matrixPacking, maskArray, 1);
        case TLayoutBlockStorage::EbsUnspecified:
            UNREACHABLE();
            return Layout::Invalid();
    }
}

[[nodiscard]] Layout sh::GlslStructLayoutOf(TField const *const *begin,
                                            TField const *const *end,
                                            TLayoutBlockStorage storage,
                                            TLayoutMatrixPacking matrixPacking,
                                            bool maskArray)
{
    ASSERT(storage != TLayoutBlockStorage::EbsUnspecified);
    ASSERT(matrixPacking != TLayoutMatrixPacking::EmpUnspecified);

    switch (storage)
    {
        case TLayoutBlockStorage::EbsPacked:
            return CommonGlslStructLayoutOf(begin, end, storage, matrixPacking, maskArray, 1);
        case TLayoutBlockStorage::EbsShared:
            return CommonGlslStructLayoutOf(begin, end, storage, matrixPacking, maskArray, 16);
        case TLayoutBlockStorage::EbsStd140:
            return CommonGlslStructLayoutOf(begin, end, storage, matrixPacking, maskArray, 16);
        case TLayoutBlockStorage::EbsStd430:
            return CommonGlslStructLayoutOf(begin, end, storage, matrixPacking, maskArray, 1);
        case TLayoutBlockStorage::EbsUnspecified:
            UNREACHABLE();
            return Layout::Invalid();
    }
}
