//
// Copyright (c) 2015 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.
//

// EGLContextCompatibilityTest.cpp:
//   This test will try to use all combinations of context configs and
//   surface configs. If the configs are compatible, it checks that simple
//   rendering works, otherwise it checks an error is generated one MakeCurrent.
#include <gtest/gtest.h>

#include <EGL/egl.h>
#include <EGL/eglext.h>
#include <vector>

#include "OSWindow.h"
#include "test_utils/ANGLETest.h"
#include "test_utils/angle_test_configs.h"

using namespace angle;

namespace
{

const EGLint contextAttribs[] = {EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE};
}

class EGLContextCompatibilityTest : public ANGLETest
{
  public:
    EGLContextCompatibilityTest() : mDisplay(0) {}

    void SetUp() override
    {
        PFNEGLGETPLATFORMDISPLAYEXTPROC eglGetPlatformDisplayEXT =
            reinterpret_cast<PFNEGLGETPLATFORMDISPLAYEXTPROC>(
                eglGetProcAddress("eglGetPlatformDisplayEXT"));
        ASSERT_TRUE(eglGetPlatformDisplayEXT != nullptr);

        EGLint dispattrs[] = {EGL_PLATFORM_ANGLE_TYPE_ANGLE, GetParam().getRenderer(), EGL_NONE};
        mDisplay           = eglGetPlatformDisplayEXT(
            EGL_PLATFORM_ANGLE_ANGLE, reinterpret_cast<void *>(EGL_DEFAULT_DISPLAY), dispattrs);
        ASSERT_TRUE(mDisplay != EGL_NO_DISPLAY);

        ASSERT_TRUE(eglInitialize(mDisplay, nullptr, nullptr) == EGL_TRUE);

        int nConfigs = 0;
        ASSERT_TRUE(eglGetConfigs(mDisplay, nullptr, 0, &nConfigs) == EGL_TRUE);
        ASSERT_TRUE(nConfigs != 0);

        int nReturnedConfigs = 0;
        mConfigs.resize(nConfigs);
        ASSERT_TRUE(eglGetConfigs(mDisplay, mConfigs.data(), nConfigs, &nReturnedConfigs) ==
                    EGL_TRUE);
        ASSERT_TRUE(nConfigs == nReturnedConfigs);
    }

    void TearDown() override
    {
        eglMakeCurrent(mDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
        eglTerminate(mDisplay);
    };

  protected:
    // Queries EGL config to determine if multisampled or not
    bool isMultisampledConfig(EGLConfig config)
    {
        EGLint samples = 0;
        eglGetConfigAttrib(mDisplay, config, EGL_SAMPLES, &samples);
        return (samples > 1);
    }

    bool areConfigsCompatible(EGLConfig c1, EGLConfig c2, EGLint surfaceBit)
    {
        EGLint colorBufferType1, colorBufferType2;
        EGLint red1, red2, green1, green2, blue1, blue2, alpha1, alpha2;
        EGLint depth1, depth2, stencil1, stencil2;
        EGLint surfaceType1, surfaceType2;

        eglGetConfigAttrib(mDisplay, c1, EGL_COLOR_BUFFER_TYPE, &colorBufferType1);
        eglGetConfigAttrib(mDisplay, c2, EGL_COLOR_BUFFER_TYPE, &colorBufferType2);

        eglGetConfigAttrib(mDisplay, c1, EGL_RED_SIZE, &red1);
        eglGetConfigAttrib(mDisplay, c2, EGL_RED_SIZE, &red2);
        eglGetConfigAttrib(mDisplay, c1, EGL_GREEN_SIZE, &green1);
        eglGetConfigAttrib(mDisplay, c2, EGL_GREEN_SIZE, &green2);
        eglGetConfigAttrib(mDisplay, c1, EGL_BLUE_SIZE, &blue1);
        eglGetConfigAttrib(mDisplay, c2, EGL_BLUE_SIZE, &blue2);
        eglGetConfigAttrib(mDisplay, c1, EGL_ALPHA_SIZE, &alpha1);
        eglGetConfigAttrib(mDisplay, c2, EGL_ALPHA_SIZE, &alpha2);

        eglGetConfigAttrib(mDisplay, c1, EGL_DEPTH_SIZE, &depth1);
        eglGetConfigAttrib(mDisplay, c2, EGL_DEPTH_SIZE, &depth2);
        eglGetConfigAttrib(mDisplay, c1, EGL_STENCIL_SIZE, &stencil1);
        eglGetConfigAttrib(mDisplay, c2, EGL_STENCIL_SIZE, &stencil2);

        eglGetConfigAttrib(mDisplay, c1, EGL_SURFACE_TYPE, &surfaceType1);
        eglGetConfigAttrib(mDisplay, c2, EGL_SURFACE_TYPE, &surfaceType2);

        EGLint colorComponentType1 = EGL_COLOR_COMPONENT_TYPE_FIXED_EXT;
        EGLint colorComponentType2 = EGL_COLOR_COMPONENT_TYPE_FIXED_EXT;
        if (eglDisplayExtensionEnabled(mDisplay, "EGL_EXT_pixel_format_float"))
        {
            eglGetConfigAttrib(mDisplay, c1, EGL_COLOR_COMPONENT_TYPE_EXT, &colorComponentType1);
            eglGetConfigAttrib(mDisplay, c2, EGL_COLOR_COMPONENT_TYPE_EXT, &colorComponentType2);
        }

        EXPECT_EGL_SUCCESS();

        return colorBufferType1 == colorBufferType2 && red1 == red2 && green1 == green2 &&
               blue1 == blue2 && alpha1 == alpha2 && colorComponentType1 == colorComponentType2 &&
               depth1 == depth2 && stencil1 == stencil2 && (surfaceType1 & surfaceBit) != 0 &&
               (surfaceType2 & surfaceBit) != 0;
    }

    void testWindowCompatibility(EGLConfig windowConfig,
                                 EGLConfig contextConfig,
                                 bool compatible) const
    {
        OSWindow *osWindow = CreateOSWindow();
        ASSERT_TRUE(osWindow != nullptr);
        osWindow->initialize("EGLContextCompatibilityTest", 500, 500);
        osWindow->setVisible(true);

        EGLContext context =
            eglCreateContext(mDisplay, contextConfig, EGL_NO_CONTEXT, contextAttribs);
        ASSERT_TRUE(context != EGL_NO_CONTEXT);

        EGLSurface window =
            eglCreateWindowSurface(mDisplay, windowConfig, osWindow->getNativeWindow(), nullptr);
        ASSERT_EGL_SUCCESS();

        if (compatible)
        {
            testClearSurface(window, windowConfig, context);
        }
        else
        {
            testMakeCurrentFails(window, context);
        }

        eglDestroySurface(mDisplay, window);
        ASSERT_EGL_SUCCESS();

        eglDestroyContext(mDisplay, context);
        ASSERT_EGL_SUCCESS();

        SafeDelete(osWindow);
    }

    void testPbufferCompatibility(EGLConfig pbufferConfig,
                                  EGLConfig contextConfig,
                                  bool compatible) const
    {
        EGLContext context =
            eglCreateContext(mDisplay, contextConfig, EGL_NO_CONTEXT, contextAttribs);
        ASSERT_TRUE(context != EGL_NO_CONTEXT);

        const EGLint pBufferAttribs[] = {
            EGL_WIDTH, 500, EGL_HEIGHT, 500, EGL_NONE,
        };
        EGLSurface pbuffer = eglCreatePbufferSurface(mDisplay, pbufferConfig, pBufferAttribs);
        ASSERT_TRUE(pbuffer != EGL_NO_SURFACE);

        if (compatible)
        {
            testClearSurface(pbuffer, pbufferConfig, context);
        }
        else
        {
            testMakeCurrentFails(pbuffer, context);
        }

        eglDestroySurface(mDisplay, pbuffer);
        ASSERT_EGL_SUCCESS();

        eglDestroyContext(mDisplay, context);
        ASSERT_EGL_SUCCESS();
    }

    std::vector<EGLConfig> mConfigs;
    EGLDisplay mDisplay;

  private:
    void testClearSurface(EGLSurface surface, EGLConfig surfaceConfig, EGLContext context) const
    {
        eglMakeCurrent(mDisplay, surface, surface, context);
        ASSERT_EGL_SUCCESS();

        glViewport(0, 0, 500, 500);
        glClearColor(0.0f, 0.0f, 1.0f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);
        ASSERT_GL_NO_ERROR();

        EGLint surfaceCompontentType = EGL_COLOR_COMPONENT_TYPE_FIXED_EXT;
        if (eglDisplayExtensionEnabled(mDisplay, "EGL_EXT_pixel_format_float"))
        {
            eglGetConfigAttrib(mDisplay, surfaceConfig, EGL_COLOR_COMPONENT_TYPE_EXT,
                               &surfaceCompontentType);
        }

        if (surfaceCompontentType == EGL_COLOR_COMPONENT_TYPE_FIXED_EXT)
        {
            EXPECT_PIXEL_EQ(250, 250, 0, 0, 255, 255);
        }
        else
        {
            EXPECT_PIXEL_32F_EQ(250, 250, 0, 0, 1.0f, 1.0f);
        }

        eglMakeCurrent(mDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
        ASSERT_EGL_SUCCESS();
    }

    void testMakeCurrentFails(EGLSurface surface, EGLContext context) const
    {
        eglMakeCurrent(mDisplay, surface, surface, context);
        EXPECT_EGL_ERROR(EGL_BAD_MATCH);
    }
};

// The test is split in several subtest so that simple cases
// are tested separately. Also each surface types are not tested
// together.

// Basic test checking contexts and windows created with the
// same config can render.
TEST_P(EGLContextCompatibilityTest, WindowSameConfig)
{
    for (size_t i = 0; i < mConfigs.size(); i++)
    {
        EGLConfig config = mConfigs[i];

        EGLint surfaceType;
        eglGetConfigAttrib(mDisplay, config, EGL_SURFACE_TYPE, &surfaceType);
        ASSERT_EGL_SUCCESS();

        if ((surfaceType & EGL_WINDOW_BIT) != 0)
        {
            // Disabling multisampled configurations due to test instability with various graphics
            // cards
            if (isMultisampledConfig(config))
            {
                continue;
            }
            testWindowCompatibility(config, config, true);
        }
    }
}

// Basic test checking contexts and pbuffers created with the
// same config can render.
TEST_P(EGLContextCompatibilityTest, PbufferSameConfig)
{
    for (size_t i = 0; i < mConfigs.size(); i++)
    {
        EGLConfig config = mConfigs[i];

        EGLint surfaceType;
        eglGetConfigAttrib(mDisplay, config, EGL_SURFACE_TYPE, &surfaceType);
        ASSERT_EGL_SUCCESS();

        if ((surfaceType & EGL_PBUFFER_BIT) != 0)
        {
            // Disabling multisampled configurations due to test instability with various graphics
            // cards
            if (isMultisampledConfig(config))
            {
                continue;
            }
            testPbufferCompatibility(config, config, true);
        }
    }
}

// Check that a context rendering to a window with a different
// config works or errors according to the EGL compatibility rules
TEST_P(EGLContextCompatibilityTest, WindowDifferentConfig)
{
    // anglebug.com/2183
    // Actually failed only on (IsIntel() && IsWindows() && IsD3D11()),
    // but it's impossible to do other tests since GL_RENDERER is NULL
    ANGLE_SKIP_TEST_IF(IsWindows());

    for (size_t i = 0; i < mConfigs.size(); i++)
    {
        EGLConfig config1 = mConfigs[i];
        // Disabling multisampled configurations due to test instability with various graphics cards
        if (isMultisampledConfig(config1))
        {
            continue;
        }

        EGLint surfaceType;
        eglGetConfigAttrib(mDisplay, config1, EGL_SURFACE_TYPE, &surfaceType);
        ASSERT_EGL_SUCCESS();

        if ((surfaceType & EGL_WINDOW_BIT) == 0)
        {
            continue;
        }

        for (size_t j = 0; j < mConfigs.size(); j++)
        {
            EGLConfig config2 = mConfigs[j];
            // Disabling multisampled configurations due to test instability with various graphics
            // cards
            if (isMultisampledConfig(config2))
            {
                continue;
            }
            testWindowCompatibility(config1, config2,
                                    areConfigsCompatible(config1, config2, EGL_WINDOW_BIT));
        }
    }
}

// Check that a context rendering to a pbuffer with a different
// config works or errors according to the EGL compatibility rules
TEST_P(EGLContextCompatibilityTest, PbufferDifferentConfig)
{
    for (size_t i = 0; i < mConfigs.size(); i++)
    {
        EGLConfig config1 = mConfigs[i];
        // Disabling multisampled configurations due to test instability with various graphics cards
        if (isMultisampledConfig(config1))
        {
            continue;
        }

        EGLint surfaceType;
        eglGetConfigAttrib(mDisplay, config1, EGL_SURFACE_TYPE, &surfaceType);
        ASSERT_EGL_SUCCESS();

        if ((surfaceType & EGL_PBUFFER_BIT) == 0)
        {
            continue;
        }

        for (size_t j = 0; j < mConfigs.size(); j++)
        {
            EGLConfig config2 = mConfigs[j];
            // Disabling multisampled configurations due to test instability with various graphics
            // cards
            if (isMultisampledConfig(config2))
            {
                continue;
            }
            testPbufferCompatibility(config1, config2,
                                     areConfigsCompatible(config1, config2, EGL_PBUFFER_BIT));
        }
    }
}

// Only run the EGLContextCompatibilityTest on release builds.  The execution time of this test
// scales with the square of the number of configs exposed and can time out in some debug builds.
// http://anglebug.com/2121
#if defined(NDEBUG)
ANGLE_INSTANTIATE_TEST(EGLContextCompatibilityTest,
                       ES2_D3D9(),
                       ES2_D3D11(),
                       ES2_OPENGL(),
                       ES2_OPENGLES(),
                       ES2_VULKAN());
#endif
