/*-------------------------------------------------------------------------
 * drawElements Quality Program EGL Module
 * ---------------------------------------
 *
 * Copyright 2014 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 *//*!
 * \file
 * \brief Tests for resizing the native window of a surface.
 *//*--------------------------------------------------------------------*/

#include "teglResizeTests.hpp"

#include "teglSimpleConfigCase.hpp"

#include "tcuImageCompare.hpp"
#include "tcuSurface.hpp"
#include "tcuPlatform.hpp"
#include "tcuTestLog.hpp"
#include "tcuInterval.hpp"
#include "tcuTextureUtil.hpp"
#include "tcuResultCollector.hpp"

#include "egluNativeDisplay.hpp"
#include "egluNativeWindow.hpp"
#include "egluNativePixmap.hpp"
#include "egluUnique.hpp"
#include "egluUtil.hpp"

#include "eglwLibrary.hpp"
#include "eglwEnums.hpp"

#include "gluDefs.hpp"
#include "glwFunctions.hpp"
#include "glwEnums.hpp"

#include "tcuTestLog.hpp"
#include "tcuVector.hpp"

#include "deThread.h"
#include "deUniquePtr.hpp"

#include <sstream>

namespace deqp
{
namespace egl
{

using de::MovePtr;
using eglu::AttribMap;
using eglu::NativeDisplay;
using eglu::NativeWindow;
using eglu::NativeWindowFactory;
using eglu::ScopedCurrentContext;
using eglu::UniqueContext;
using eglu::UniqueSurface;
using eglu::WindowParams;
using std::ostringstream;
using std::string;
using std::vector;
using tcu::CommandLine;
using tcu::ConstPixelBufferAccess;
using tcu::Interval;
using tcu::IVec2;
using tcu::ResultCollector;
using tcu::Surface;
using tcu::TestLog;
using tcu::UVec4;
using tcu::Vec3;
using tcu::Vec4;
using namespace eglw;

typedef eglu::WindowParams::Visibility Visibility;
typedef TestCase::IterateResult IterateResult;

struct ResizeParams
{
    string name;
    string description;
    IVec2 oldSize;
    IVec2 newSize;
};

class ResizeTest : public TestCase
{
public:
    ResizeTest(EglTestContext &eglTestCtx, const ResizeParams &params)
        : TestCase(eglTestCtx, params.name.c_str(), params.description.c_str())
        , m_oldSize(params.oldSize)
        , m_newSize(params.newSize)
        , m_display(EGL_NO_DISPLAY)
        , m_config(DE_NULL)
        , m_log(m_testCtx.getLog())
        , m_status(m_log)
    {
    }

    void init(void);
    void deinit(void);

protected:
    virtual EGLenum surfaceType(void) const
    {
        return EGL_WINDOW_BIT;
    }
    void resize(IVec2 size);

    const IVec2 m_oldSize;
    const IVec2 m_newSize;
    EGLDisplay m_display;
    EGLConfig m_config;
    MovePtr<NativeWindow> m_nativeWindow;
    MovePtr<UniqueSurface> m_surface;
    MovePtr<UniqueContext> m_context;
    TestLog &m_log;
    ResultCollector m_status;
    glw::Functions m_gl;
};

EGLConfig getEGLConfig(const Library &egl, const EGLDisplay eglDisplay, EGLenum surfaceType)
{
    AttribMap attribMap;

    attribMap[EGL_SURFACE_TYPE]    = surfaceType;
    attribMap[EGL_RENDERABLE_TYPE] = EGL_OPENGL_ES2_BIT;

    return eglu::chooseSingleConfig(egl, eglDisplay, attribMap);
}

void ResizeTest::init(void)
{
    TestCase::init();

    const Library &egl          = m_eglTestCtx.getLibrary();
    const CommandLine &cmdLine  = m_testCtx.getCommandLine();
    const EGLDisplay eglDisplay = eglu::getAndInitDisplay(m_eglTestCtx.getNativeDisplay());
    const EGLConfig eglConfig   = getEGLConfig(egl, eglDisplay, surfaceType());
    const EGLint ctxAttribs[]   = {EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE};
    EGLContext eglContext       = egl.createContext(eglDisplay, eglConfig, EGL_NO_CONTEXT, ctxAttribs);
    EGLU_CHECK_MSG(egl, "eglCreateContext()");
    MovePtr<UniqueContext> context(new UniqueContext(egl, eglDisplay, eglContext));
    const EGLint configId        = eglu::getConfigAttribInt(egl, eglDisplay, eglConfig, EGL_CONFIG_ID);
    const Visibility visibility  = eglu::parseWindowVisibility(cmdLine);
    NativeDisplay &nativeDisplay = m_eglTestCtx.getNativeDisplay();
    const NativeWindowFactory &windowFactory =
        eglu::selectNativeWindowFactory(m_eglTestCtx.getNativeDisplayFactory(), cmdLine);

    const WindowParams windowParams(m_oldSize.x(), m_oldSize.y(), visibility);
    MovePtr<NativeWindow> nativeWindow(
        windowFactory.createWindow(&nativeDisplay, eglDisplay, eglConfig, DE_NULL, windowParams));
    const EGLSurface eglSurface =
        eglu::createWindowSurface(nativeDisplay, *nativeWindow, eglDisplay, eglConfig, DE_NULL);
    MovePtr<UniqueSurface> surface(new UniqueSurface(egl, eglDisplay, eglSurface));

    m_log << TestLog::Message << "Chose EGLConfig with id " << configId << ".\n"
          << "Created initial surface with size " << m_oldSize << TestLog::EndMessage;

    m_eglTestCtx.initGLFunctions(&m_gl, glu::ApiType::es(2, 0));
    m_config       = eglConfig;
    m_surface      = surface;
    m_context      = context;
    m_display      = eglDisplay;
    m_nativeWindow = nativeWindow;
    EGLU_CHECK_MSG(egl, "init");
}

void ResizeTest::deinit(void)
{
    m_config = DE_NULL;
    m_context.clear();
    m_surface.clear();
    m_nativeWindow.clear();

    if (m_display != EGL_NO_DISPLAY)
    {
        m_eglTestCtx.getLibrary().terminate(m_display);
        m_display = EGL_NO_DISPLAY;
    }
}

void ResizeTest::resize(IVec2 size)
{
    m_nativeWindow->setSurfaceSize(size);
    m_testCtx.getPlatform().processEvents();
    m_log << TestLog::Message << "Resized surface to size " << size << TestLog::EndMessage;
}

class ChangeSurfaceSizeCase : public ResizeTest
{
public:
    ChangeSurfaceSizeCase(EglTestContext &eglTestCtx, const ResizeParams &params) : ResizeTest(eglTestCtx, params)
    {
    }

    IterateResult iterate(void);
};

void drawRectangle(const glw::Functions &gl, IVec2 pos, IVec2 size, Vec3 color)
{
    gl.clearColor(color.x(), color.y(), color.z(), 1.0);
    gl.scissor(pos.x(), pos.y(), size.x(), size.y());
    gl.enable(GL_SCISSOR_TEST);
    gl.clear(GL_COLOR_BUFFER_BIT);
    gl.disable(GL_SCISSOR_TEST);
    GLU_EXPECT_NO_ERROR(gl.getError(), "Rectangle drawing with glScissor and glClear failed.");
}

void initSurface(const glw::Functions &gl, IVec2 oldSize)
{
    const Vec3 frameColor(0.0f, 0.0f, 1.0f);
    const Vec3 fillColor(1.0f, 0.0f, 0.0f);
    const Vec3 markColor(0.0f, 1.0f, 0.0f);

    drawRectangle(gl, IVec2(0, 0), oldSize, frameColor);
    drawRectangle(gl, IVec2(2, 2), oldSize - IVec2(4, 4), fillColor);

    drawRectangle(gl, IVec2(0, 0), IVec2(8, 4), markColor);
    drawRectangle(gl, oldSize - IVec2(16, 16), IVec2(8, 4), markColor);
    drawRectangle(gl, IVec2(0, oldSize.y() - 16), IVec2(8, 4), markColor);
    drawRectangle(gl, IVec2(oldSize.x() - 16, 0), IVec2(8, 4), markColor);
}

bool compareRectangles(const ConstPixelBufferAccess &rectA, const ConstPixelBufferAccess &rectB)
{
    const int width  = rectA.getWidth();
    const int height = rectA.getHeight();
    const int depth  = rectA.getDepth();

    if (rectB.getWidth() != width || rectB.getHeight() != height || rectB.getDepth() != depth)
        return false;

    for (int z = 0; z < depth; ++z)
        for (int y = 0; y < height; ++y)
            for (int x = 0; x < width; ++x)
                if (rectA.getPixel(x, y, z) != rectB.getPixel(x, y, z))
                    return false;

    return true;
}

// Check whether `oldSurface` and `newSurface` share a common corner.
bool compareCorners(const Surface &oldSurface, const Surface &newSurface)
{
    const int oldWidth  = oldSurface.getWidth();
    const int oldHeight = oldSurface.getHeight();
    const int newWidth  = newSurface.getWidth();
    const int newHeight = newSurface.getHeight();
    const int minWidth  = de::min(oldWidth, newWidth);
    const int minHeight = de::min(oldHeight, newHeight);

    for (int xCorner = 0; xCorner < 2; ++xCorner)
    {
        const int oldX = xCorner == 0 ? 0 : oldWidth - minWidth;
        const int newX = xCorner == 0 ? 0 : newWidth - minWidth;

        for (int yCorner = 0; yCorner < 2; ++yCorner)
        {
            const int oldY                   = yCorner == 0 ? 0 : oldHeight - minHeight;
            const int newY                   = yCorner == 0 ? 0 : newHeight - minHeight;
            ConstPixelBufferAccess oldAccess = getSubregion(oldSurface.getAccess(), oldX, oldY, minWidth, minHeight);
            ConstPixelBufferAccess newAccess = getSubregion(newSurface.getAccess(), newX, newY, minWidth, minHeight);

            if (compareRectangles(oldAccess, newAccess))
                return true;
        }
    }

    return false;
}

Surface readSurface(const glw::Functions &gl, IVec2 size)
{
    Surface ret(size.x(), size.y());
    gl.readPixels(0, 0, size.x(), size.y(), GL_RGBA, GL_UNSIGNED_BYTE, ret.getAccess().getDataPtr());

    GLU_EXPECT_NO_ERROR(gl.getError(), "glReadPixels() failed");
    return ret;
}

template <typename T>
inline bool hasBits(T bitSet, T requiredBits)
{
    return (bitSet & requiredBits) == requiredBits;
}

IVec2 getNativeSurfaceSize(const NativeWindow &nativeWindow, IVec2 reqSize)
{
    if (hasBits(nativeWindow.getCapabilities(), NativeWindow::CAPABILITY_GET_SURFACE_SIZE))
        return nativeWindow.getSurfaceSize();
    return reqSize; // assume we got the requested size
}

IVec2 checkSurfaceSize(const Library &egl, EGLDisplay eglDisplay, EGLSurface eglSurface,
                       const NativeWindow &nativeWindow, IVec2 reqSize, ResultCollector &status)
{
    const IVec2 nativeSize = getNativeSurfaceSize(nativeWindow, reqSize);
    IVec2 eglSize          = eglu::getSurfaceSize(egl, eglDisplay, eglSurface);
    ostringstream oss;

    oss << "Size of EGL surface " << eglSize << " differs from size of native window " << nativeSize;
    status.check(eglSize == nativeSize, oss.str());

    return eglSize;
}

IterateResult ChangeSurfaceSizeCase::iterate(void)
{
    const Library &egl = m_eglTestCtx.getLibrary();
    ScopedCurrentContext currentCtx(egl, m_display, **m_surface, **m_surface, **m_context);
    egl.swapBuffers(m_display, **m_surface);
    IVec2 oldEglSize = checkSurfaceSize(egl, m_display, **m_surface, *m_nativeWindow, m_oldSize, m_status);

    initSurface(m_gl, oldEglSize);

    this->resize(m_newSize);

    egl.swapBuffers(m_display, **m_surface);
    EGLU_CHECK_MSG(egl, "eglSwapBuffers()");
    checkSurfaceSize(egl, m_display, **m_surface, *m_nativeWindow, m_newSize, m_status);

    m_status.setTestContextResult(m_testCtx);
    return STOP;
}

class PreserveBackBufferCase : public ResizeTest
{
public:
    PreserveBackBufferCase(EglTestContext &eglTestCtx, const ResizeParams &params) : ResizeTest(eglTestCtx, params)
    {
    }

    IterateResult iterate(void);
    EGLenum surfaceType(void) const;
};

EGLenum PreserveBackBufferCase::surfaceType(void) const
{
    return EGL_WINDOW_BIT | EGL_SWAP_BEHAVIOR_PRESERVED_BIT;
}

IterateResult PreserveBackBufferCase::iterate(void)
{
    const Library &egl = m_eglTestCtx.getLibrary();
    ScopedCurrentContext currentCtx(egl, m_display, **m_surface, **m_surface, **m_context);

    EGLU_CHECK_CALL(egl, surfaceAttrib(m_display, **m_surface, EGL_SWAP_BEHAVIOR, EGL_BUFFER_PRESERVED));

    GLU_EXPECT_NO_ERROR(m_gl.getError(), "GL state erroneous upon initialization!");

    {
        const IVec2 oldEglSize = eglu::getSurfaceSize(egl, m_display, **m_surface);
        initSurface(m_gl, oldEglSize);

        m_gl.finish();
        GLU_EXPECT_NO_ERROR(m_gl.getError(), "glFinish() failed");

        {
            const Surface oldSurface = readSurface(m_gl, oldEglSize);

            egl.swapBuffers(m_display, **m_surface);
            this->resize(m_newSize);
            egl.swapBuffers(m_display, **m_surface);
            EGLU_CHECK_MSG(egl, "eglSwapBuffers()");

            {
                const IVec2 newEglSize   = eglu::getSurfaceSize(egl, m_display, **m_surface);
                const Surface newSurface = readSurface(m_gl, newEglSize);

                m_log << TestLog::ImageSet("Corner comparison", "Comparing old and new surfaces at all corners")
                      << TestLog::Image("Before resizing", "Before resizing", oldSurface)
                      << TestLog::Image("After resizing", "After resizing", newSurface) << TestLog::EndImageSet;

                m_status.checkResult(compareCorners(oldSurface, newSurface), QP_TEST_RESULT_QUALITY_WARNING,
                                     "Resizing the native window changed the contents of "
                                     "the EGL surface");
            }
        }
    }

    m_status.setTestContextResult(m_testCtx);
    return STOP;
}

typedef tcu::Vector<Interval, 2> IvVec2;

class UpdateResolutionCase : public ResizeTest
{
public:
    UpdateResolutionCase(EglTestContext &eglTestCtx, const ResizeParams &params) : ResizeTest(eglTestCtx, params)
    {
    }

    IterateResult iterate(void);

private:
    IvVec2 getNativePixelsPerInch(void);
};

IvVec2 ivVec2(const IVec2 &vec)
{
    return IvVec2(double(vec.x()), double(vec.y()));
}

Interval approximateInt(int i)
{
    const Interval margin(-1.0, 1.0); // The resolution may be rounded

    return (Interval(i) + margin) & Interval(0.0, TCU_INFINITY);
}

IvVec2 UpdateResolutionCase::getNativePixelsPerInch(void)
{
    const Library &egl    = m_eglTestCtx.getLibrary();
    const int inchPer10km = 254 * EGL_DISPLAY_SCALING;
    const IVec2 bufSize   = eglu::getSurfaceSize(egl, m_display, **m_surface);
    const IVec2 winSize   = m_nativeWindow->getScreenSize();
    const IVec2 bufPp10km = eglu::getSurfaceResolution(egl, m_display, **m_surface);
    const IvVec2 bufPpiI =
        (IvVec2(approximateInt(bufPp10km.x()), approximateInt(bufPp10km.y())) / Interval(inchPer10km));
    const IvVec2 winPpiI = ivVec2(winSize) * bufPpiI / ivVec2(bufSize);
    const IVec2 winPpi(int(winPpiI.x().midpoint()), int(winPpiI.y().midpoint()));

    m_log << TestLog::Message << "EGL surface size: " << bufSize << "\n"
          << "EGL surface pixel density (pixels / 10 km): " << bufPp10km << "\n"
          << "Native window size: " << winSize << "\n"
          << "Native pixel density (ppi): " << winPpi << "\n"
          << TestLog::EndMessage;

    m_status.checkResult(bufPp10km.x() >= 1 && bufPp10km.y() >= 1, QP_TEST_RESULT_QUALITY_WARNING,
                         "Surface pixel density is less than one pixel per 10 km. "
                         "Is the surface really visible from space?");

    return winPpiI;
}

IterateResult UpdateResolutionCase::iterate(void)
{
    const Library &egl = m_eglTestCtx.getLibrary();
    ScopedCurrentContext currentCtx(egl, m_display, **m_surface, **m_surface, **m_context);

    if (!hasBits(m_nativeWindow->getCapabilities(), NativeWindow::CAPABILITY_GET_SCREEN_SIZE))
        TCU_THROW(NotSupportedError, "Unable to determine surface size in screen pixels");

    {
        const IVec2 oldEglSize = eglu::getSurfaceSize(egl, m_display, **m_surface);
        initSurface(m_gl, oldEglSize);
    }
    {
        const IvVec2 oldPpi = this->getNativePixelsPerInch();
        this->resize(m_newSize);
        EGLU_CHECK_CALL(egl, swapBuffers(m_display, **m_surface));
        {
            const IvVec2 newPpi = this->getNativePixelsPerInch();
            m_status.check(oldPpi.x().intersects(newPpi.x()) && oldPpi.y().intersects(newPpi.y()),
                           "Window PPI differs after resizing");
        }
    }

    m_status.setTestContextResult(m_testCtx);
    return STOP;
}

ResizeTests::ResizeTests(EglTestContext &eglTestCtx)
    : TestCaseGroup(eglTestCtx, "resize", "Tests resizing the native surface")
{
}

template <class Case>
TestCaseGroup *createCaseGroup(EglTestContext &eglTestCtx, const string &name, const string &desc)
{
    const ResizeParams params[] = {
        {"shrink", "Shrink in both dimensions", IVec2(128, 128), IVec2(32, 32)},
        {"grow", "Grow in both dimensions", IVec2(32, 32), IVec2(128, 128)},
        {"stretch_width", "Grow horizontally, shrink vertically", IVec2(32, 128), IVec2(128, 32)},
        {"stretch_height", "Grow vertically, shrink horizontally", IVec2(128, 32), IVec2(32, 128)},
    };
    TestCaseGroup *const group = new TestCaseGroup(eglTestCtx, name.c_str(), desc.c_str());

    for (int ndx = 0; ndx < DE_LENGTH_OF_ARRAY(params); ++ndx)
        group->addChild(new Case(eglTestCtx, params[ndx]));

    return group;
}

void ResizeTests::init(void)
{
    addChild(createCaseGroup<ChangeSurfaceSizeCase>(m_eglTestCtx, "surface_size", "EGL surface size update"));
    addChild(createCaseGroup<PreserveBackBufferCase>(m_eglTestCtx, "back_buffer", "Back buffer contents"));
    addChild(createCaseGroup<UpdateResolutionCase>(m_eglTestCtx, "pixel_density", "Pixel density"));
}

} // namespace egl
} // namespace deqp
