/*-------------------------------------------------------------------------
 * 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 Memory object allocation stress tests
 *//*--------------------------------------------------------------------*/

#include "teglMemoryStressTests.hpp"

#include "tcuTestLog.hpp"
#include "tcuCommandLine.hpp"

#include "deRandom.hpp"
#include "deClock.h"
#include "deString.h"

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

#include "egluUtil.hpp"

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

#include <vector>
#include <string>

using std::string;
using std::vector;
using tcu::TestLog;

using namespace eglw;

namespace deqp
{
namespace egl
{

namespace
{

enum ObjectType
{
    OBJECTTYPE_PBUFFER = (1 << 0),
    OBJECTTYPE_CONTEXT = (1 << 1),

    //    OBJECTTYPE_WINDOW,
    //    OBJECTTYPE_PIXMAP,
};

class MemoryAllocator
{
public:
    MemoryAllocator(EglTestContext &eglTestCtx, EGLDisplay display, EGLConfig config, int seed, ObjectType types,
                    int minWidth, int minHeight, int maxWidth, int maxHeight, bool use);
    ~MemoryAllocator(void);

    bool allocateUntilFailure(void);
    int getAllocationCount(void) const
    {
        return (int)(m_pbuffers.size() + m_contexts.size());
    }
    int getContextCount(void) const
    {
        return (int)m_contexts.size();
    }
    int getPBufferCount(void) const
    {
        return (int)m_pbuffers.size();
    }
    const string &getErrorString(void) const
    {
        return m_errorString;
    }

private:
    void allocatePBuffer(void);
    void allocateContext(void);

    EglTestContext &m_eglTestCtx;
    EGLDisplay m_display;
    EGLConfig m_config;
    glw::Functions m_gl;

    de::Random m_rnd;
    bool m_failed;
    string m_errorString;

    ObjectType m_types;
    int m_minWidth;
    int m_minHeight;
    int m_maxWidth;
    int m_maxHeight;
    bool m_use;

    vector<EGLSurface> m_pbuffers;
    vector<EGLContext> m_contexts;
};

MemoryAllocator::MemoryAllocator(EglTestContext &eglTestCtx, EGLDisplay display, EGLConfig config, int seed,
                                 ObjectType types, int minWidth, int minHeight, int maxWidth, int maxHeight, bool use)
    : m_eglTestCtx(eglTestCtx)
    , m_display(display)
    , m_config(config)

    , m_rnd(seed)
    , m_failed(false)

    , m_types(types)
    , m_minWidth(minWidth)
    , m_minHeight(minHeight)
    , m_maxWidth(maxWidth)
    , m_maxHeight(maxHeight)
    , m_use(use)
{
    m_eglTestCtx.initGLFunctions(&m_gl, glu::ApiType::es(2, 0));
}

MemoryAllocator::~MemoryAllocator(void)
{
    const Library &egl = m_eglTestCtx.getLibrary();

    for (vector<EGLSurface>::const_iterator iter = m_pbuffers.begin(); iter != m_pbuffers.end(); ++iter)
        egl.destroySurface(m_display, *iter);

    m_pbuffers.clear();

    for (vector<EGLContext>::const_iterator iter = m_contexts.begin(); iter != m_contexts.end(); ++iter)
        egl.destroyContext(m_display, *iter);

    m_contexts.clear();
}

bool MemoryAllocator::allocateUntilFailure(void)
{
    const uint64_t timeLimitUs = 10000000; // 10s
    uint64_t beginTimeUs       = deGetMicroseconds();
    vector<ObjectType> types;

    if ((m_types & OBJECTTYPE_CONTEXT) != 0)
        types.push_back(OBJECTTYPE_CONTEXT);

    if ((m_types & OBJECTTYPE_PBUFFER) != 0)
        types.push_back(OBJECTTYPE_PBUFFER);

    // If objects should be used. Create one of both at beginning to allow using them.
    if (m_contexts.size() == 0 && m_pbuffers.size() == 0 && m_use)
    {
        allocateContext();
        allocatePBuffer();
    }

    while (!m_failed)
    {
        ObjectType type = m_rnd.choose<ObjectType>(types.begin(), types.end());

        switch (type)
        {
        case OBJECTTYPE_PBUFFER:
            allocatePBuffer();
            break;

        case OBJECTTYPE_CONTEXT:
            allocateContext();
            break;

        default:
            DE_ASSERT(false);
        }

        if (deGetMicroseconds() - beginTimeUs > timeLimitUs)
            return true;
    }

    return false;
}

void MemoryAllocator::allocatePBuffer(void)
{
    // Reserve space for new allocations
    try
    {
        m_pbuffers.reserve(m_pbuffers.size() + 1);
    }
    catch (const std::bad_alloc &)
    {
        m_errorString = "std::bad_alloc when allocating more space for testcase. Out of host memory.";
        m_failed      = true;
        return;
    }

    // Allocate pbuffer
    try
    {
        const Library &egl        = m_eglTestCtx.getLibrary();
        const EGLint width        = m_rnd.getInt(m_minWidth, m_maxWidth);
        const EGLint height       = m_rnd.getInt(m_minHeight, m_maxHeight);
        const EGLint attribList[] = {EGL_WIDTH, width, EGL_HEIGHT, height, EGL_NONE};

        EGLSurface surface = egl.createPbufferSurface(m_display, m_config, attribList);
        EGLU_CHECK_MSG(egl, "eglCreatePbufferSurface");

        DE_ASSERT(surface != EGL_NO_SURFACE);

        m_pbuffers.push_back(surface);

        if (m_use && m_contexts.size() > 0)
        {
            EGLContext context = m_rnd.choose<EGLContext>(m_contexts.begin(), m_contexts.end());
            const float red    = m_rnd.getFloat();
            const float green  = m_rnd.getFloat();
            const float blue   = m_rnd.getFloat();
            const float alpha  = m_rnd.getFloat();

            EGLU_CHECK_CALL(egl, makeCurrent(m_display, surface, surface, context));

            m_gl.clearColor(red, green, blue, alpha);
            GLU_EXPECT_NO_ERROR(m_gl.getError(), "glClearColor()");

            m_gl.clear(GL_COLOR_BUFFER_BIT);
            GLU_EXPECT_NO_ERROR(m_gl.getError(), "glClear()");

            EGLU_CHECK_CALL(egl, makeCurrent(m_display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT));
        }
    }
    catch (const eglu::Error &error)
    {
        if (error.getError() == EGL_BAD_ALLOC)
        {
            m_errorString = "eglCreatePbufferSurface returned EGL_BAD_ALLOC";
            m_failed      = true;
            return;
        }
        else
            throw;
    }
}

void MemoryAllocator::allocateContext(void)
{
    // Reserve space for new allocations
    try
    {
        m_contexts.reserve(m_contexts.size() + 1);
    }
    catch (const std::bad_alloc &)
    {
        m_errorString = "std::bad_alloc when allocating more space for testcase. Out of host memory.";
        m_failed      = true;
        return;
    }

    // Allocate context
    try
    {
        const Library &egl        = m_eglTestCtx.getLibrary();
        const EGLint attribList[] = {EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE};

        EGLU_CHECK_CALL(egl, bindAPI(EGL_OPENGL_ES_API));
        EGLContext context = egl.createContext(m_display, m_config, EGL_NO_CONTEXT, attribList);
        EGLU_CHECK_MSG(egl, "eglCreateContext");

        DE_ASSERT(context != EGL_NO_CONTEXT);

        m_contexts.push_back(context);

        if (m_use && m_pbuffers.size() > 0)
        {
            EGLSurface surface = m_rnd.choose<EGLSurface>(m_pbuffers.begin(), m_pbuffers.end());
            const float red    = m_rnd.getFloat();
            const float green  = m_rnd.getFloat();
            const float blue   = m_rnd.getFloat();
            const float alpha  = m_rnd.getFloat();

            EGLU_CHECK_CALL(egl, makeCurrent(m_display, surface, surface, context));

            m_gl.clearColor(red, green, blue, alpha);
            GLU_EXPECT_NO_ERROR(m_gl.getError(), "glClearColor()");

            m_gl.clear(GL_COLOR_BUFFER_BIT);
            GLU_EXPECT_NO_ERROR(m_gl.getError(), "glClear()");

            EGLU_CHECK_CALL(egl, makeCurrent(m_display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT));
        }
    }
    catch (const eglu::Error &error)
    {
        if (error.getError() == EGL_BAD_ALLOC)
        {
            m_errorString = "eglCreateContext returned EGL_BAD_ALLOC";
            m_failed      = true;
            return;
        }
        else
            throw;
    }
}

} // namespace

class MemoryStressCase : public TestCase
{
public:
    struct Spec
    {
        ObjectType types;
        int minWidth;
        int minHeight;
        int maxWidth;
        int maxHeight;
        bool use;
    };

    MemoryStressCase(EglTestContext &eglTestCtx, Spec spec, const char *name, const char *description);
    void init(void);
    void deinit(void);
    IterateResult iterate(void);

private:
    Spec m_spec;
    vector<int> m_allocationCounts;
    MemoryAllocator *m_allocator;

    int m_iteration;
    int m_iterationCount;
    int m_seed;
    EGLDisplay m_display;
    EGLConfig m_config;
};

MemoryStressCase::MemoryStressCase(EglTestContext &eglTestCtx, Spec spec, const char *name, const char *description)
    : TestCase(eglTestCtx, name, description)
    , m_spec(spec)
    , m_allocator(NULL)
    , m_iteration(0)
    , m_iterationCount(10)
    , m_seed(deStringHash(name))
    , m_display(EGL_NO_DISPLAY)
    , m_config(DE_NULL)
{
}

void MemoryStressCase::init(void)
{
    const Library &egl        = m_eglTestCtx.getLibrary();
    EGLint configCount        = 0;
    const EGLint attribList[] = {EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, EGL_NONE};

    if (!m_testCtx.getCommandLine().isOutOfMemoryTestEnabled())
    {
        m_testCtx.getLog()
            << TestLog::Message
            << "Tests that exhaust memory are disabled, use --deqp-test-oom=enable command line option to enable."
            << TestLog::EndMessage;
        throw tcu::NotSupportedError("OOM tests disabled");
    }

    m_display = eglu::getAndInitDisplay(m_eglTestCtx.getNativeDisplay());

    EGLU_CHECK_CALL(egl, chooseConfig(m_display, attribList, &m_config, 1, &configCount));

    TCU_CHECK(configCount != 0);
}

void MemoryStressCase::deinit(void)
{
    delete m_allocator;
    m_allocator = DE_NULL;

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

TestCase::IterateResult MemoryStressCase::iterate(void)
{
    TestLog &log = m_testCtx.getLog();

    if (m_iteration < m_iterationCount)
    {
        try
        {
            if (!m_allocator)
                m_allocator =
                    new MemoryAllocator(m_eglTestCtx, m_display, m_config, m_seed, m_spec.types, m_spec.minWidth,
                                        m_spec.minHeight, m_spec.maxWidth, m_spec.maxHeight, m_spec.use);

            if (m_allocator->allocateUntilFailure())
            {
                log << TestLog::Message << "Couldn't exhaust memory before timeout. Allocated "
                    << m_allocator->getAllocationCount() << " objects." << TestLog::EndMessage;
                m_testCtx.setTestResult(QP_TEST_RESULT_PASS, "Pass");

                delete m_allocator;
                m_allocator = NULL;

                return STOP;
            }

            log << TestLog::Message << "Iteration " << m_iteration << ": Allocated "
                << m_allocator->getAllocationCount() << " objects; " << m_allocator->getContextCount() << " contexts, "
                << m_allocator->getPBufferCount() << " PBuffers." << TestLog::EndMessage;
            log << TestLog::Message << "Got expected error: " << m_allocator->getErrorString() << TestLog::EndMessage;
            m_allocationCounts.push_back(m_allocator->getAllocationCount());

            delete m_allocator;
            m_allocator = NULL;

            m_iteration++;

            return CONTINUE;
        }
        catch (...)
        {
            log << TestLog::Message << "Iteration " << m_iteration << ": Allocated "
                << m_allocator->getAllocationCount() << " objects; " << m_allocator->getContextCount() << " contexts, "
                << m_allocator->getPBufferCount() << " PBuffers." << TestLog::EndMessage;
            log << TestLog::Message << "Unexpected error" << TestLog::EndMessage;
            throw;
        }
    }
    else
    {
        // Analyze number of passed allocations.
        int min = m_allocationCounts[0];
        int max = m_allocationCounts[0];

        float threshold = 50.0f;

        for (int allocNdx = 0; allocNdx < (int)m_allocationCounts.size(); allocNdx++)
        {
            min = deMin32(m_allocationCounts[allocNdx], min);
            max = deMax32(m_allocationCounts[allocNdx], max);
        }

        if (min == 0 && max != 0)
        {
            log << TestLog::Message << "Allocation count zero" << TestLog::EndMessage;
            m_testCtx.setTestResult(QP_TEST_RESULT_FAIL, "Fail");
        }
        else
        {
            float change = (float)(min - max) / ((float)(max));

            if (change > threshold)
            {
                log << TestLog::Message << "Allocated objects max: " << max << ", min: " << min
                    << ", difference: " << change << "% threshold: " << threshold << "%" << TestLog::EndMessage;
                m_testCtx.setTestResult(QP_TEST_RESULT_QUALITY_WARNING, "Allocation count variation");
            }
            else
                m_testCtx.setTestResult(QP_TEST_RESULT_PASS, "Pass");
        }

        return STOP;
    }
}

MemoryStressTests::MemoryStressTests(EglTestContext &eglTestCtx)
    : TestCaseGroup(eglTestCtx, "memory", "Memory allocation stress tests")
{
}

void MemoryStressTests::init(void)
{
    // Check small pbuffers 256x256
    {
        MemoryStressCase::Spec spec;

        spec.types     = OBJECTTYPE_PBUFFER;
        spec.minWidth  = 256;
        spec.minHeight = 256;
        spec.maxWidth  = 256;
        spec.maxHeight = 256;
        spec.use       = false;

        addChild(new MemoryStressCase(m_eglTestCtx, spec, "pbuffer_256x256", "PBuffer allocation stress tests"));
    }

    // Check small pbuffers 256x256 and use them
    {
        MemoryStressCase::Spec spec;

        spec.types     = OBJECTTYPE_PBUFFER;
        spec.minWidth  = 256;
        spec.minHeight = 256;
        spec.maxWidth  = 256;
        spec.maxHeight = 256;
        spec.use       = true;

        addChild(new MemoryStressCase(m_eglTestCtx, spec, "pbuffer_256x256_use", "PBuffer allocation stress tests"));
    }

    // Check big pbuffers 1024x1024
    {
        MemoryStressCase::Spec spec;

        spec.types     = OBJECTTYPE_PBUFFER;
        spec.minWidth  = 1024;
        spec.minHeight = 1024;
        spec.maxWidth  = 1024;
        spec.maxHeight = 1024;
        spec.use       = false;

        addChild(new MemoryStressCase(m_eglTestCtx, spec, "pbuffer_1024x1024", "PBuffer allocation stress tests"));
    }

    // Check big pbuffers 1024x1024 and use them
    {
        MemoryStressCase::Spec spec;

        spec.types     = OBJECTTYPE_PBUFFER;
        spec.minWidth  = 1024;
        spec.minHeight = 1024;
        spec.maxWidth  = 1024;
        spec.maxHeight = 1024;
        spec.use       = true;

        addChild(new MemoryStressCase(m_eglTestCtx, spec, "pbuffer_1024x1024_use", "PBuffer allocation stress tests"));
    }

    // Check different sized pbuffers
    {
        MemoryStressCase::Spec spec;

        spec.types     = OBJECTTYPE_PBUFFER;
        spec.minWidth  = 64;
        spec.minHeight = 64;
        spec.maxWidth  = 1024;
        spec.maxHeight = 1024;
        spec.use       = false;

        addChild(new MemoryStressCase(m_eglTestCtx, spec, "pbuffer", "PBuffer allocation stress tests"));
    }

    // Check different sized pbuffers and use them
    {
        MemoryStressCase::Spec spec;

        spec.types     = OBJECTTYPE_PBUFFER;
        spec.minWidth  = 64;
        spec.minHeight = 64;
        spec.maxWidth  = 1024;
        spec.maxHeight = 1024;
        spec.use       = true;

        addChild(new MemoryStressCase(m_eglTestCtx, spec, "pbuffer_use", "PBuffer allocation stress tests"));
    }

    // Check contexts
    {
        MemoryStressCase::Spec spec;

        spec.types     = OBJECTTYPE_CONTEXT;
        spec.minWidth  = 1024;
        spec.minHeight = 1024;
        spec.maxWidth  = 1024;
        spec.maxHeight = 1024;
        spec.use       = false;

        addChild(new MemoryStressCase(m_eglTestCtx, spec, "context", "Context allocation stress tests"));
    }

    // Check contexts and use them
    {
        MemoryStressCase::Spec spec;

        spec.types     = OBJECTTYPE_CONTEXT;
        spec.minWidth  = 1024;
        spec.minHeight = 1024;
        spec.maxWidth  = 1024;
        spec.maxHeight = 1024;
        spec.use       = true;

        addChild(new MemoryStressCase(m_eglTestCtx, spec, "context_use", "Context allocation stress tests"));
    }

    // Check contexts and pbuffers
    {
        MemoryStressCase::Spec spec;

        spec.types     = (ObjectType)(OBJECTTYPE_PBUFFER | OBJECTTYPE_CONTEXT);
        spec.minWidth  = 64;
        spec.minHeight = 64;
        spec.maxWidth  = 1024;
        spec.maxHeight = 1024;
        spec.use       = false;

        addChild(
            new MemoryStressCase(m_eglTestCtx, spec, "pbuffer_context", "PBuffer and context allocation stress tests"));
    }

    // Check contexts and pbuffers and use
    {
        MemoryStressCase::Spec spec;

        spec.types     = (ObjectType)(OBJECTTYPE_PBUFFER | OBJECTTYPE_CONTEXT);
        spec.minWidth  = 64;
        spec.minHeight = 64;
        spec.maxWidth  = 1024;
        spec.maxHeight = 1024;
        spec.use       = true;

        addChild(new MemoryStressCase(m_eglTestCtx, spec, "pbuffer_context_use",
                                      "PBuffer and context allocation stress tests"));
    }
}

} // namespace egl
} // namespace deqp
