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

// EGLSyncControlTest.cpp:
//   Tests pertaining to eglGetSyncValuesCHROMIUM.

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

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

using namespace angle;

typedef EGLBoolean(EGLAPIENTRYP PFNEGLGETSYNCVALUESCHROMIUMPROC)(EGLDisplay dpy,
                                                                 EGLSurface surface,
                                                                 EGLuint64KHR *ust,
                                                                 EGLuint64KHR *msc,
                                                                 EGLuint64KHR *sbc);

class EGLSyncControlTest : public testing::Test
{
  protected:
    EGLSyncControlTest() {}

    void SetUp() override
    {
        mD3D11Module = LoadLibrary(TEXT("d3d11.dll"));
        if (mD3D11Module == nullptr)
        {
            std::cout << "Unable to LoadLibrary D3D11" << std::endl;
            return;
        }

        mD3D11CreateDevice = reinterpret_cast<PFN_D3D11_CREATE_DEVICE>(
            GetProcAddress(mD3D11Module, "D3D11CreateDevice"));
        if (mD3D11CreateDevice == nullptr)
        {
            std::cout << "Could not retrieve D3D11CreateDevice from d3d11.dll" << std::endl;
            return;
        }

        mD3D11Available = true;

        const char *extensionString =
            static_cast<const char *>(eglQueryString(EGL_NO_DISPLAY, EGL_EXTENSIONS));
        if (strstr(extensionString, "EGL_ANGLE_device_creation"))
        {
            if (strstr(extensionString, "EGL_ANGLE_device_creation_d3d11"))
            {
                mDeviceCreationD3D11ExtAvailable = true;
            }
        }

        eglGetSyncValuesCHROMIUM = reinterpret_cast<PFNEGLGETSYNCVALUESCHROMIUMPROC>(
            eglGetProcAddress("eglGetSyncValuesCHROMIUM"));
    }

    void TearDown() override
    {
        SafeRelease(mDevice);
        SafeRelease(mDeviceContext);

        SafeDelete(mOSWindow);

        if (mSurface != EGL_NO_SURFACE)
        {
            eglDestroySurface(mDisplay, mSurface);
            mSurface = EGL_NO_SURFACE;
        }

        if (mContext != EGL_NO_CONTEXT)
        {
            eglDestroyContext(mDisplay, mContext);
            mContext = EGL_NO_CONTEXT;
        }

        if (mDisplay != EGL_NO_DISPLAY)
        {
            eglTerminate(mDisplay);
            mDisplay = EGL_NO_DISPLAY;
        }
    }

    void CreateD3D11Device()
    {
        ASSERT_TRUE(mD3D11Available);
        ASSERT_EQ(nullptr, mDevice);  // The device shouldn't be created twice

        HRESULT hr =
            mD3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE, 0, 0, nullptr, 0,
                               D3D11_SDK_VERSION, &mDevice, &mFeatureLevel, &mDeviceContext);

        ASSERT_TRUE(SUCCEEDED(hr));
    }

    void InitializeDisplay()
    {
        EGLint displayAttribs[] = {EGL_PLATFORM_ANGLE_TYPE_ANGLE,
                                   EGL_PLATFORM_ANGLE_TYPE_D3D11_ANGLE,
                                   EGL_PLATFORM_ANGLE_MAX_VERSION_MAJOR_ANGLE,
                                   EGL_DONT_CARE,
                                   EGL_PLATFORM_ANGLE_MAX_VERSION_MINOR_ANGLE,
                                   EGL_DONT_CARE,
                                   EGL_PLATFORM_ANGLE_DEVICE_TYPE_ANGLE,
                                   EGL_PLATFORM_ANGLE_DEVICE_TYPE_HARDWARE_ANGLE,
                                   EGL_NONE};

        // Create an OS Window
        mOSWindow = CreateOSWindow();
        mOSWindow->initialize("EGLSyncControlTest", 64, 64);
        mOSWindow->setVisible(true);

        // Create an EGLDisplay using the EGLDevice
        mDisplay = eglGetPlatformDisplayEXT(EGL_PLATFORM_ANGLE_ANGLE,
                                            reinterpret_cast<void *>(mOSWindow->getNativeDisplay()),
                                            displayAttribs);
        ASSERT_TRUE(mDisplay != EGL_NO_DISPLAY);

        EGLint majorVersion, minorVersion;
        ASSERT_TRUE(eglInitialize(mDisplay, &majorVersion, &minorVersion) == EGL_TRUE);
    }

    void CreateWindowSurface()
    {
        eglBindAPI(EGL_OPENGL_ES_API);
        ASSERT_EGL_SUCCESS();

        // Choose a config
        const EGLint configAttributes[] = {EGL_NONE};

        EGLint configCount = 0;
        ASSERT_EGL_TRUE(eglChooseConfig(mDisplay, configAttributes, &mConfig, 1, &configCount));

        const EGLint surfaceAttributes[] = {EGL_DIRECT_COMPOSITION_ANGLE, EGL_TRUE, EGL_NONE};

        // Create window surface
        mSurface = eglCreateWindowSurface(mDisplay, mConfig, mOSWindow->getNativeWindow(),
                                          surfaceAttributes);
        if (mSurface == nullptr)
        {
            std::cout << "Unable to create window surface with Direct Composition" << std::endl;
            return;
        }

        mDirectCompositionSurfaceAvailable = true;

        // Create EGL context
        EGLint contextAttibutes[] = {EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE};

        mContext = eglCreateContext(mDisplay, mConfig, nullptr, contextAttibutes);
        ASSERT_EGL_SUCCESS();

        // Make the surface current
        eglMakeCurrent(mDisplay, mSurface, mSurface, mContext);
        ASSERT_EGL_SUCCESS();
    }

    bool mD3D11Available                       = false;
    HMODULE mD3D11Module                       = nullptr;
    PFN_D3D11_CREATE_DEVICE mD3D11CreateDevice = nullptr;

    ID3D11Device *mDevice               = nullptr;
    ID3D11DeviceContext *mDeviceContext = nullptr;
    D3D_FEATURE_LEVEL mFeatureLevel;

    bool mDeviceCreationD3D11ExtAvailable = false;

    bool mDirectCompositionSurfaceAvailable = false;

    OSWindow *mOSWindow = nullptr;

    EGLDisplay mDisplay                                      = EGL_NO_DISPLAY;
    EGLSurface mSurface                                      = EGL_NO_SURFACE;
    EGLContext mContext                                      = EGL_NO_CONTEXT;
    EGLConfig mConfig                                        = 0;
    PFNEGLGETSYNCVALUESCHROMIUMPROC eglGetSyncValuesCHROMIUM = nullptr;
};

// Basic test for eglGetSyncValuesCHROMIUM extension. Verifies that eglGetSyncValuesCHROMIUM
// can be called on DX11 with direct composition and that it returns reasonable enough values.
TEST_F(EGLSyncControlTest, DISABLED_SyncValuesTest)
{
    static const DWORD kPollInterval    = 10;
    static const int kNumPollIterations = 100;

    if (!mD3D11Available)
    {
        std::cout << "D3D11 not available, skipping test" << std::endl;
        return;
    }

    CreateD3D11Device();
    InitializeDisplay();
    CreateWindowSurface();

    if (!mDirectCompositionSurfaceAvailable)
    {
        std::cout << "Direct Composition surface not available, skipping test" << std::endl;
        return;
    }

    const char *extensionString =
        static_cast<const char *>(eglQueryString(mDisplay, EGL_EXTENSIONS));
    ASSERT_TRUE(strstr(extensionString, "EGL_CHROMIUM_sync_control"));

    EGLuint64KHR ust = 0, msc = 0, sbc = 0;
    // It appears there is a race condition so the very first call to eglGetSyncValuesCHROMIUM
    // can fail within D3D with DXGI_ERROR_FRAME_STATISTICS_DISJOINT.
    // Should that be handled inside eglGetSyncValuesCHROMIUM?
    eglGetSyncValuesCHROMIUM(mDisplay, mSurface, &ust, &msc, &sbc);

    ASSERT_EGL_TRUE(eglGetSyncValuesCHROMIUM(mDisplay, mSurface, &ust, &msc, &sbc));
    // Initial msc and sbc value should be true. Initial ust value is unspecified.
    ASSERT_EQ(0ull, msc);
    ASSERT_EQ(0ull, sbc);

    // Perform some very basic rendering.
    glClearColor(1.0f, 0.0f, 1.0f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);
    ASSERT_GL_NO_ERROR();

    ASSERT_EGL_TRUE(eglSwapBuffers(mDisplay, mSurface));

    // Poll until sbc value increases. Normally it should change within 16-17 ms.
    for (int i = 0; i < kNumPollIterations; i++)
    {
        ::Sleep(kPollInterval);
        ASSERT_EGL_TRUE(eglGetSyncValuesCHROMIUM(mDisplay, mSurface, &ust, &msc, &sbc));
        if (sbc > 0)
            break;
    }

    // sbc should change to 1. msc and ust to some non-zero values.
    ASSERT_EQ(1ull, sbc);
    ASSERT_GT(ust, 0ull);
    ASSERT_GT(msc, 0ull);

    // Perform more rendering.
    glClearColor(0.0f, 0.0f, 1.0f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);
    ASSERT_GL_NO_ERROR();

    ASSERT_EGL_TRUE(eglSwapBuffers(mDisplay, mSurface));

    // Poll until sbc value increases. Normally it should change within 16-17 ms.
    EGLuint64KHR ust2 = 0, msc2 = 0, sbc2 = 0;
    for (int i = 0; i < kNumPollIterations; i++)
    {
        ::Sleep(kPollInterval);
        ASSERT_EGL_TRUE(eglGetSyncValuesCHROMIUM(mDisplay, mSurface, &ust2, &msc2, &sbc2));
        if (sbc2 > sbc)
            break;
    }

    // sbc2 should be 2. msc2 and ust2 should be greater than previous msc and ust values.
    ASSERT_EQ(2ull, sbc2);
    ASSERT_GT(ust2, ust);
    ASSERT_GT(msc2, msc);
}
