/*-------------------------------------------------------------------------
 * 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 "gluDefs.hpp"
#include "tcuTestLog.hpp"
#include "tcuCommandLine.hpp"

#include "deRandom.hpp"

#include "deClock.h"
#include "deString.h"

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

#include <vector>
#include <string>

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

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.getGLFunctions(m_gl, glu::ApiType::es(2,0));
}

MemoryAllocator::~MemoryAllocator (void)
{
	for (vector<EGLSurface>::const_iterator iter = m_pbuffers.begin(); iter != m_pbuffers.end(); ++iter)
		TCU_CHECK_EGL_CALL(eglDestroySurface(m_display, *iter));

	m_pbuffers.clear();

	for (vector<EGLContext>::const_iterator iter = m_contexts.begin(); iter != m_contexts.end(); ++iter)
	{
		TCU_CHECK_EGL_CALL(eglDestroyContext(m_display, *iter));
	}

	m_contexts.clear();
}

bool MemoryAllocator::allocateUntilFailure (void)
{
	const deUint64		timeLimitUs		= 10000000; // 10s
	deUint64			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 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 = eglCreatePbufferSurface(m_display, m_config, attribList);
		TCU_CHECK_EGL_MSG("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();

			TCU_CHECK_EGL_CALL(eglMakeCurrent(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()");

			TCU_CHECK_EGL_CALL(eglMakeCurrent(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 EGLint attribList[] = {
			EGL_CONTEXT_CLIENT_VERSION, 2,
			EGL_NONE
		};

		TCU_CHECK_EGL_CALL(eglBindAPI(EGL_OPENGL_ES_API));
		EGLContext context = eglCreateContext(m_display, m_config, EGL_NO_CONTEXT, attribList);
		TCU_CHECK_EGL_MSG("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();

			TCU_CHECK_EGL_CALL(eglMakeCurrent(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()");

			TCU_CHECK_EGL_CALL(eglMakeCurrent(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;
	}
}

} // anonymous

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)
{
	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 = m_eglTestCtx.getDisplay().getEGLDisplay();

	TCU_CHECK_EGL_CALL(eglChooseConfig(m_display, attribList, &m_config, 1, &configCount));

	TCU_CHECK(configCount != 0);
}

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

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 = (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"));
	}
}

} // egl
} // deqp