//
// Copyright 2019 The ANGLE Project Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//
// Tests the eglQueryStringiANGLE and eglQueryDisplayAttribANGLE functions exposed by the
// extension EGL_ANGLE_feature_control.

#include <gtest/gtest.h>
#include <optional>
#include "common/unsafe_buffers.h"

#include "common/string_utils.h"
#include "libANGLE/Display.h"
#include "test_utils/ANGLETest.h"

using namespace angle;

class EGLFeatureControlTest : public ANGLETest<>
{
  public:
    void testSetUp() override { mDisplay = EGL_NO_DISPLAY; }

    void testTearDown() override
    {
        if (mDisplay != EGL_NO_DISPLAY)
        {
            eglTerminate(mDisplay);
        }
    }

  protected:
    EGLDisplay mDisplay;

    bool initTest()
    {
        // http://anglebug.com/42262291 This test sporadically times out on Win10/Intel
        if (IsWindows() && IsIntel())
            return false;

        EGLAttrib dispattrs[] = {EGL_PLATFORM_ANGLE_TYPE_ANGLE, GetParam().getRenderer(), EGL_NONE};
        mDisplay              = eglGetPlatformDisplay(GetEglPlatform(),
                                                      reinterpret_cast<void *>(EGL_DEFAULT_DISPLAY), dispattrs);
        EXPECT_NE(mDisplay, EGL_NO_DISPLAY);

        EXPECT_EQ(eglInitialize(mDisplay, nullptr, nullptr), static_cast<EGLBoolean>(EGL_TRUE));

        EXPECT_TRUE(IsEGLClientExtensionEnabled("EGL_ANGLE_feature_control"));

        return true;
    }

    using FeatureNameModifier = std::function<std::string(const std::string &)>;
    void testOverrideFeatures(FeatureNameModifier modifyName);
};

// Ensure eglQueryStringiANGLE generates EGL_BAD_DISPLAY if the display passed in is invalid.
TEST_P(EGLFeatureControlTest, InvalidDisplay)
{
    ANGLE_SKIP_TEST_IF(!initTest());
    EXPECT_EQ(nullptr, eglQueryStringiANGLE(EGL_NO_DISPLAY, EGL_FEATURE_NAME_ANGLE, 0));
    EXPECT_EGL_ERROR(EGL_BAD_DISPLAY);
}

// Ensure eglQueryStringiANGLE generates EGL_BAD_PARAMETER if the index is negative.
TEST_P(EGLFeatureControlTest, NegativeIndex)
{
    ANGLE_SKIP_TEST_IF(!initTest());
    EXPECT_EQ(nullptr, eglQueryStringiANGLE(mDisplay, EGL_FEATURE_NAME_ANGLE, -1));
    EXPECT_EGL_ERROR(EGL_BAD_PARAMETER);
}

// Ensure eglQueryStringiANGLE generates EGL_BAD_PARAMETER if the index is out of bounds.
TEST_P(EGLFeatureControlTest, IndexOutOfBounds)
{
    ANGLE_SKIP_TEST_IF(!initTest());
    egl::Display *display = static_cast<egl::Display *>(mDisplay);
    EXPECT_EQ(nullptr, eglQueryStringiANGLE(mDisplay, EGL_FEATURE_NAME_ANGLE,
                                            display->getFeatures().size()));
    EXPECT_EGL_ERROR(EGL_BAD_PARAMETER);
}

// Ensure eglQueryStringiANGLE generates EGL_BAD_PARAMETER if the name is not one of the valid
// options specified in EGL_ANGLE_feature_control.
TEST_P(EGLFeatureControlTest, InvalidName)
{
    ANGLE_SKIP_TEST_IF(!initTest());
    EXPECT_EQ(nullptr, eglQueryStringiANGLE(mDisplay, 100, 0));
    EXPECT_EGL_ERROR(EGL_BAD_PARAMETER);
}

// For each valid name and index in the feature description arrays, query the values and ensure
// that no error is generated, and that the values match the correct values frim ANGLE's display's
// FeatureList.
TEST_P(EGLFeatureControlTest, QueryAll)
{
    ANGLE_SKIP_TEST_IF(!initTest());
    egl::Display *display       = static_cast<egl::Display *>(mDisplay);
    angle::FeatureList features = display->getFeatures();
    for (size_t i = 0; i < features.size(); i++)
    {
        EXPECT_STREQ(features[i]->name, eglQueryStringiANGLE(mDisplay, EGL_FEATURE_NAME_ANGLE, i));
        EXPECT_STREQ(FeatureCategoryToString(features[i]->category),
                     eglQueryStringiANGLE(mDisplay, EGL_FEATURE_CATEGORY_ANGLE, i));
        EXPECT_STREQ(FeatureStatusToString(features[i]->enabled),
                     eglQueryStringiANGLE(mDisplay, EGL_FEATURE_STATUS_ANGLE, i));
        ASSERT_EGL_SUCCESS();
    }
}

// Ensure eglQueryDisplayAttribANGLE returns the correct number of features when queried with
// attribute EGL_FEATURE_COUNT_ANGLE
TEST_P(EGLFeatureControlTest, FeatureCount)
{
    ANGLE_SKIP_TEST_IF(!initTest());
    egl::Display *display = static_cast<egl::Display *>(mDisplay);
    EGLAttrib value       = -1;
    EXPECT_EQ(static_cast<EGLBoolean>(EGL_TRUE),
              eglQueryDisplayAttribANGLE(mDisplay, EGL_FEATURE_COUNT_ANGLE, &value));
    EXPECT_EQ(display->getFeatures().size(), static_cast<size_t>(value));
    ASSERT_EGL_SUCCESS();
}

void EGLFeatureControlTest::testOverrideFeatures(FeatureNameModifier modifyName)
{
    ANGLE_SKIP_TEST_IF(!initTest());
    egl::Display *display       = static_cast<egl::Display *>(mDisplay);
    angle::FeatureList features = display->getFeatures();

    // Build lists of features to enable/disabled. Toggle features we know are ok to toggle based
    // from this list.
    std::vector<const char *> enabled;
    std::vector<const char *> disabled;
    std::vector<std::string> modifiedNameStorage;
    std::vector<bool> shouldBe;
    std::vector<std::string> testedFeatures = {
        // Safe to toggle on GL
        angle::GetFeatureName(angle::Feature::AddAndTrueToLoopCondition),
        angle::GetFeatureName(angle::Feature::ClampFragDepth),
        // Safe to toggle on GL and Vulkan
        angle::GetFeatureName(angle::Feature::ClampPointSize),
        // Safe to toggle on D3D
        angle::GetFeatureName(angle::Feature::ExpandIntegerPowExpressions),
        angle::GetFeatureName(angle::Feature::RewriteUnaryMinusOperator),
    };

    modifiedNameStorage.reserve(features.size());
    shouldBe.reserve(features.size());

    for (size_t i = 0; i < features.size(); i++)
    {
        modifiedNameStorage.push_back(modifyName(features[i]->name));

        bool toggle = std::find(testedFeatures.begin(), testedFeatures.end(),
                                std::string(features[i]->name)) != testedFeatures.end();
        if (features[i]->enabled ^ toggle)
        {
            enabled.push_back(modifiedNameStorage[i].c_str());
        }
        else
        {
            disabled.push_back(modifiedNameStorage[i].c_str());
        }
        // Save what we expect the feature status will be when checking later.
        shouldBe.push_back(features[i]->enabled ^ toggle);
    }
    disabled.push_back(0);
    enabled.push_back(0);

    // Terminate the old display (we just used it to collect features)
    eglTerminate(mDisplay);

    // Create a new display with these overridden features.
    EGLAttrib dispattrs[] = {EGL_PLATFORM_ANGLE_TYPE_ANGLE,
                             GetParam().getRenderer(),
                             EGL_FEATURE_OVERRIDES_ENABLED_ANGLE,
                             reinterpret_cast<EGLAttrib>(enabled.data()),
                             EGL_FEATURE_OVERRIDES_DISABLED_ANGLE,
                             reinterpret_cast<EGLAttrib>(disabled.data()),
                             EGL_NONE};
    mDisplay              = eglGetPlatformDisplay(GetEglPlatform(),
                                                  reinterpret_cast<void *>(EGL_DEFAULT_DISPLAY), dispattrs);
    ASSERT_EGL_SUCCESS();
    ASSERT_NE(mDisplay, EGL_NO_DISPLAY);
    ASSERT_EQ(eglInitialize(mDisplay, nullptr, nullptr), EGLBoolean(EGL_TRUE));

    // Check that all features have the correct status (even the ones we toggled).
    for (size_t i = 0; i < features.size(); i++)
    {
        EXPECT_STREQ(FeatureStatusToString(shouldBe[i]),
                     eglQueryStringiANGLE(mDisplay, EGL_FEATURE_STATUS_ANGLE, i))
            << modifiedNameStorage[i];
    }
}

// Submit a list of features to override when creating the display with eglGetPlatformDisplay, and
// ensure that the features are correctly overridden.
TEST_P(EGLFeatureControlTest, OverrideFeatures)
{
    testOverrideFeatures([](const std::string &featureName) { return featureName; });
}

// Similar to OverrideFeatures, but ensures that camelCase variants of the name match as well.
TEST_P(EGLFeatureControlTest, OverrideFeaturesCamelCase)
{
    testOverrideFeatures(
        [](const std::string &featureName) { return angle::ToCamelCase(featureName); });
}

// Similar to OverrideFeatures, but ensures wildcard matching works
TEST_P(EGLFeatureControlTest, OverrideFeaturesWildcard)
{
    for (int j = 0; j < 2; j++)
    {
        const bool testEnableOverride = (j != 0);

        ANGLE_SKIP_TEST_IF(!initTest());

        egl::Display *display       = static_cast<egl::Display *>(mDisplay);
        angle::FeatureList features = display->getFeatures();

        // Note that we don't use the broader 'prefer_*' here because
        // prefer_monolithic_pipelines_over_libraries may affect other feature
        // flags.
        std::vector<const char *> featuresToOverride = {"allow_host_image*", nullptr};

        std::vector<std::string> featureNameStorage;
        std::vector<bool> shouldBe;

        shouldBe.reserve(features.size());
        featureNameStorage.reserve(features.size());

        for (size_t i = 0; i < features.size(); i++)
        {
            std::string featureName = std::string(features[i]->name);
            std::transform(featureName.begin(), featureName.end(), featureName.begin(),
                           [](unsigned char c) { return std::tolower(c); });

            const bool featureMatch =
                ANGLE_UNSAFE_TODO(strncmp(featureName.c_str(), "allowhostimage", 14)) == 0;

            std::optional<bool> overrideState;
            if (featureMatch)
            {
                overrideState = testEnableOverride;
            }

            // Save what we expect the feature status will be when checking later.
            shouldBe.push_back(overrideState.value_or(features[i]->enabled));
            featureNameStorage.push_back(features[i]->name);
        }

        // Terminate the old display (we just used it to collect features)
        eglTerminate(mDisplay);
        mDisplay = nullptr;

        // Create a new display with these overridden features.
        EGLAttrib dispattrs[] = {EGL_PLATFORM_ANGLE_TYPE_ANGLE, GetParam().getRenderer(),
                                 testEnableOverride ? EGL_FEATURE_OVERRIDES_ENABLED_ANGLE
                                                    : EGL_FEATURE_OVERRIDES_DISABLED_ANGLE,
                                 reinterpret_cast<EGLAttrib>(featuresToOverride.data()), EGL_NONE};
        mDisplay              = eglGetPlatformDisplay(GetEglPlatform(),
                                                      reinterpret_cast<void *>(EGL_DEFAULT_DISPLAY), dispattrs);
        ASSERT_EGL_SUCCESS();
        ASSERT_NE(mDisplay, EGL_NO_DISPLAY);
        ASSERT_EQ(eglInitialize(mDisplay, nullptr, nullptr), EGLBoolean(EGL_TRUE));

        // Check that all features have the correct status (even the ones we toggled).
        for (size_t i = 0; i < features.size(); i++)
        {
            EXPECT_STREQ(FeatureStatusToString(shouldBe[i]),
                         eglQueryStringiANGLE(mDisplay, EGL_FEATURE_STATUS_ANGLE, i))
                << featureNameStorage[i];
        }

        // Clean up display for next iteration.
        eglTerminate(mDisplay);
        mDisplay = nullptr;
    }
}

// Ensure that dependent features are affected properly by overrides
TEST_P(EGLFeatureControlTest, OverrideFeaturesDependent)
{
    ANGLE_SKIP_TEST_IF(!initTest());

    egl::Display *display       = static_cast<egl::Display *>(mDisplay);
    angle::FeatureList features = display->getFeatures();

    const std::vector<const char *> featuresDisabled = {
        GetFeatureName(Feature::SupportsRenderpass2),
        GetFeatureName(Feature::SupportsImage2dViewOf3d), nullptr};

    const std::vector<const char *> featuresExpectDisabled = {
        // Features we changed
        GetFeatureName(Feature::SupportsRenderpass2),
        GetFeatureName(Feature::SupportsImage2dViewOf3d),

        // Features that must become disabled as a result of the above
        // depends on SupportsRenderpass2
        GetFeatureName(Feature::SupportsDepthStencilResolve),
        // depends on SupportsRenderpass2
        GetFeatureName(Feature::SupportsMultisampledRenderToSingleSampled),
        // depends on SupportsRenderpass2
        GetFeatureName(Feature::SupportsFragmentShadingRate),
        // depends on SupportsImage2dViewOf3d
        GetFeatureName(Feature::SupportsSampler2dViewOf3d),

        // Features that must become disabled as a result of the above
        // depends on supportsDepthStencilResolve
        GetFeatureName(Feature::SupportsDepthStencilIndependentResolveNone),
        // depends on SupportsMultisampledRenderToSingleSampled
        GetFeatureName(Feature::PreferMSRTSSFlagByDefault),
        // depends on SupportsMultisampledRenderToSingleSampled
        GetFeatureName(Feature::SupportsMultiviewMultisampleRenderToTexture),
        // depends on SupportsFragmentShadingRate
        GetFeatureName(Feature::SupportFragmentShadingRateExtExtensions),
        // depends on SupportsFragmentShadingRate
        GetFeatureName(Feature::SupportsFoveatedRendering),
    };

    // Features that could be different on some vendors
    const std::set<std::string> featuresThatCouldBeDifferent = {
        // Depends-on Feature::SupportsDepthStencilResolve
        GetFeatureName(Feature::EnableMultisampledRenderToTexture),
        // Depends-on Feature::SupportsFragmentShadingRate
        GetFeatureName(Feature::SupportsFoveatedRendering),
        // Depends-on Feature::EnableMultisampledRenderToTexture
        GetFeatureName(Feature::PreferDynamicRendering),
    };

    std::vector<std::string> featureNameStorage;
    std::vector<bool> shouldBe;

    shouldBe.reserve(features.size());
    featureNameStorage.reserve(features.size());

    for (size_t i = 0; i < features.size(); i++)
    {
        bool featureMatch = false;
        for (auto *ptr : featuresExpectDisabled)
        {
            if (ANGLE_UNSAFE_TODO(strcmp(ptr, features[i]->name)) == 0)
            {
                featureMatch = true;
                break;
            }
        }

        std::string featureName = std::string(features[i]->name);
        std::transform(featureName.begin(), featureName.end(), featureName.begin(),
                       [](unsigned char c) { return std::tolower(c); });

        // Save what we expect the feature status will be when checking later.
        shouldBe.push_back(features[i]->enabled && !featureMatch);

        // Store copy of the feature name string, in case we need to print for a test failure
        featureNameStorage.push_back(features[i]->name);
    }

    // Terminate the old display (we just used it to collect features)
    eglTerminate(mDisplay);

    // Create a new display with these overridden features.
    EGLAttrib dispattrs[] = {EGL_PLATFORM_ANGLE_TYPE_ANGLE, GetParam().getRenderer(),
                             EGL_FEATURE_OVERRIDES_DISABLED_ANGLE,
                             reinterpret_cast<EGLAttrib>(featuresDisabled.data()), EGL_NONE};
    mDisplay              = eglGetPlatformDisplay(GetEglPlatform(),
                                                  reinterpret_cast<void *>(EGL_DEFAULT_DISPLAY), dispattrs);
    ASSERT_EGL_SUCCESS();
    ASSERT_NE(mDisplay, EGL_NO_DISPLAY);
    ASSERT_EQ(eglInitialize(mDisplay, nullptr, nullptr), EGLBoolean(EGL_TRUE));

    // Check that all features have the correct status (even the ones we toggled).
    for (size_t i = 0; i < features.size(); i++)
    {
        if (featuresThatCouldBeDifferent.count(featureNameStorage[i]) > 0)
        {
            // On some vendors these features could be different
            continue;
        }

        EXPECT_STREQ(FeatureStatusToString(shouldBe[i]),
                     eglQueryStringiANGLE(mDisplay, EGL_FEATURE_STATUS_ANGLE, i))
            << featureNameStorage[i];
    }
}

ANGLE_INSTANTIATE_TEST(EGLFeatureControlTest,
                       WithNoFixture(ES2_D3D11()),
                       WithNoFixture(ES2_METAL()),
                       WithNoFixture(ES2_OPENGL()),
                       WithNoFixture(ES2_VULKAN()),
                       WithNoFixture(ES3_D3D11()),
                       WithNoFixture(ES3_METAL()),
                       WithNoFixture(ES3_OPENGL()));
