/*-------------------------------------------------------------------------
 * drawElements Quality Program OpenGL ES 3.0 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 Object lifetime tests.
 *//*--------------------------------------------------------------------*/

#include "es3fLifetimeTests.hpp"

#include "deRandom.hpp"
#include "deUniquePtr.hpp"
#include "tcuRenderTarget.hpp"
#include "tcuSurface.hpp"
#include "gluDrawUtil.hpp"
#include "gluObjectWrapper.hpp"
#include "gluPixelTransfer.hpp"
#include "gluShaderProgram.hpp"
#include "glsLifetimeTests.hpp"
#include "glwEnums.hpp"
#include "glwFunctions.hpp"

#include <vector>

namespace deqp
{
namespace gles3
{
namespace Functional
{
namespace
{

using std::vector;
using de::MovePtr;
using de::Random;
using tcu::RenderTarget;
using tcu::Surface;
using tcu::TestContext;
using tcu::TestLog;
using glu::CallLogWrapper;
using glu::RenderContext;
using glu::ProgramSources;
using glu::VertexArray;
using glu::Buffer;
namespace lt = gls::LifetimeTests;
using namespace lt;
using namespace glw;
typedef TestCase::IterateResult IterateResult;

enum { VIEWPORT_SIZE = 128 };

class ScaleProgram : public glu::ShaderProgram
{
public:
							ScaleProgram	(lt::Context& ctx);
	void 					draw			(GLuint vao, GLfloat scale, bool tf, Surface* dst);
	void					setPos			(GLuint buffer, GLuint vao);

private:
	ProgramSources			getSources 		(void);

	const RenderContext&	m_renderCtx;
	GLint					m_scaleLoc;
	GLint					m_posLoc;
};

enum { NUM_COMPONENTS = 4, NUM_VERTICES = 3 };

ScaleProgram::ScaleProgram (lt::Context& ctx)
	: glu::ShaderProgram	(ctx.getRenderContext(), getSources())
	, m_renderCtx			(ctx.getRenderContext())
{
	const Functions& gl = m_renderCtx.getFunctions();
	TCU_CHECK(isOk());
	m_scaleLoc = gl.getUniformLocation(getProgram(), "scale");
	m_posLoc = gl.getAttribLocation(getProgram(), "pos");
}

#define GLSL(VERSION, BODY) ("#version " #VERSION "\n" #BODY "\n")

static const char* const s_vertexShaderSrc = GLSL(
	100,
	attribute vec4 pos;
	uniform float scale;
	void main ()
	{
		gl_Position = vec4(scale * pos.xy, pos.zw);
	}
	);

static const char* const s_fragmentShaderSrc = GLSL(
	100,
	void main ()
	{
		gl_FragColor = vec4(1.0, 0.0, 1.0, 1.0);
	}
	);

ProgramSources ScaleProgram::getSources (void)
{
	using namespace glu;
	ProgramSources sources;
	sources << VertexSource(s_vertexShaderSrc)
			<< FragmentSource(s_fragmentShaderSrc)
			<< TransformFeedbackMode(GL_INTERLEAVED_ATTRIBS)
			<< TransformFeedbackVarying("gl_Position");
	return sources;
}

void ScaleProgram::draw (GLuint vao, GLfloat scale, bool tf, Surface* dst)
{
	const Functions&	gl			= m_renderCtx.getFunctions();
	de::Random			rnd			(vao);
	Rectangle			viewport	= randomViewport(m_renderCtx,
													 VIEWPORT_SIZE, VIEWPORT_SIZE, rnd);
	setViewport(m_renderCtx, viewport);
	gl.clearColor(0, 0, 0, 1);
	gl.clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

	gl.bindVertexArray(vao);
	gl.enableVertexAttribArray(m_posLoc);
	GLU_CHECK_CALL_ERROR(gl.useProgram(getProgram()),
						 gl.getError());

	gl.uniform1f(m_scaleLoc, scale);

	if (tf)
		gl.beginTransformFeedback(GL_TRIANGLES);
	GLU_CHECK_CALL_ERROR(gl.drawArrays(GL_TRIANGLES, 0, 3), gl.getError());
	if (tf)
		gl.endTransformFeedback();

	if (dst != DE_NULL)
		readRectangle(m_renderCtx, viewport, *dst);

	gl.bindVertexArray(0);
}

void ScaleProgram::setPos (GLuint buffer, GLuint vao)
{
	const Functions& gl = m_renderCtx.getFunctions();

	gl.bindBuffer(GL_ARRAY_BUFFER, buffer);
	gl.bindVertexArray(vao);
	GLU_CHECK_CALL_ERROR(
		gl.vertexAttribPointer(m_posLoc, NUM_COMPONENTS, GL_FLOAT, false, 0, DE_NULL),
		gl.getError());
	gl.bindVertexArray(0);
	gl.bindBuffer(GL_ARRAY_BUFFER, 0);
	GLU_CHECK_ERROR(gl.getError());
}

class VertexArrayBinder : public SimpleBinder
{
public:
						VertexArrayBinder	(lt::Context& ctx)
							: SimpleBinder	(ctx, 0, GL_NONE, GL_VERTEX_ARRAY_BINDING, true) {}
	void				bind			(GLuint name) { glBindVertexArray(name); }
};

class SamplerBinder : public Binder
{
public:
						SamplerBinder	(lt::Context& ctx) : Binder(ctx) {}
	void				bind			(GLuint name) { glBindSampler(0, name); }
	GLuint				getBinding		(void)
	{
		GLint arr[32] = {};
		glGetIntegerv(GL_SAMPLER_BINDING, arr);
		log() << TestLog::Message << "// First output integer: " << arr[0]
			  << TestLog::EndMessage;
		return arr[0];
	}
	bool				genRequired		(void) const { return true; }
};

class QueryBinder : public Binder
{
public:
						QueryBinder		(lt::Context& ctx) : Binder(ctx) {}
	void				bind			(GLuint name)
	{
		if (name != 0)
			glBeginQuery(GL_ANY_SAMPLES_PASSED, name);
		else
			glEndQuery(GL_ANY_SAMPLES_PASSED);
	}
	GLuint				getBinding		(void) { return 0; }
};

class BufferVAOAttacher : public Attacher
{
public:
						BufferVAOAttacher	(lt::Context& ctx, Type& elementType,
											 Type& varrType, ScaleProgram& program)
							: Attacher		(ctx, elementType, varrType)
							, m_program		(program) {}
	void				initAttachment		(GLuint seed, GLuint element);
	void				attach				(GLuint element, GLuint container);
	void				detach				(GLuint element, GLuint container);
	bool				canAttachDeleted	(void) const { return false; }
	ScaleProgram&		getProgram			(void) { return m_program; }
	GLuint				getAttachment		(GLuint container);

private:
	ScaleProgram&		m_program;
};

static const GLfloat s_varrData[NUM_VERTICES * NUM_COMPONENTS] =
{
	-1.0,  0.0, 0.0, 1.0,
	 1.0,  1.0, 0.0, 1.0,
	 0.0, -1.0, 0.0, 1.0
};

void initBuffer (const Functions& gl, GLuint seed, GLenum usage, GLuint buffer)
{
	gl.bindBuffer(GL_ARRAY_BUFFER, buffer);
	if (seed == 0)
		gl.bufferData(GL_ARRAY_BUFFER, sizeof(s_varrData), s_varrData, usage);
	else
	{
		Random	rnd	(seed);
		GLfloat data[DE_LENGTH_OF_ARRAY(s_varrData)];

		for (int ndx = 0; ndx < NUM_VERTICES; ndx++)
		{
			GLfloat* vertex = &data[ndx * NUM_COMPONENTS];
			vertex[0] = 2.0f * (rnd.getFloat() - 0.5f);
			vertex[1] = 2.0f * (rnd.getFloat() - 0.5f);
			DE_STATIC_ASSERT(NUM_COMPONENTS == 4);
			vertex[2] = 0.0f;
			vertex[3] = 1.0f;
		}
		gl.bufferData(GL_ARRAY_BUFFER, sizeof(data), data, usage);
	}
	gl.bindBuffer(GL_ARRAY_BUFFER, 0);
	GLU_CHECK_ERROR(gl.getError());
}

void BufferVAOAttacher::initAttachment (GLuint seed, GLuint buffer)
{
	initBuffer(gl(), seed, GL_STATIC_DRAW, buffer);
	log() << TestLog::Message << "// Initialized buffer " << buffer << " from seed " << seed
		  << TestLog::EndMessage;
}

void BufferVAOAttacher::attach (GLuint buffer, GLuint vao)
{
	m_program.setPos(buffer, vao);
	log() << TestLog::Message
		  << "// Set the `pos` attribute in VAO " << vao << " to buffer " << buffer
		  << TestLog::EndMessage;
}

void BufferVAOAttacher::detach (GLuint buffer, GLuint varr)
{
	DE_UNREF(buffer);
	attach(0, varr);
}

GLuint BufferVAOAttacher::getAttachment (GLuint varr)
{
	GLint name = 0;
	gl().bindVertexArray(varr);
	gl().getVertexAttribiv(0, GL_VERTEX_ATTRIB_ARRAY_BUFFER_BINDING, &name);
	gl().bindVertexArray(0);
	GLU_CHECK_ERROR(gl().getError());
	return GLuint(name);
}

class BufferVAOInputAttacher : public InputAttacher
{
public:
						BufferVAOInputAttacher	(BufferVAOAttacher& attacher)
							: InputAttacher		(attacher)
							, m_program			(attacher.getProgram()) {}
	void				drawContainer			(GLuint container, Surface& dst);

private:
	ScaleProgram&		m_program;
};

void BufferVAOInputAttacher::drawContainer (GLuint vao, Surface& dst)
{
	m_program.draw(vao, 1.0, false, &dst);
	log() << TestLog::Message << "// Drew an output image with VAO " << vao
		  << TestLog::EndMessage;
};

class BufferTfAttacher : public Attacher
{
public:
				BufferTfAttacher	(lt::Context& ctx, Type& bufferType, Type& tfType)
					: Attacher		(ctx, bufferType, tfType) {}
	void		initAttachment		(GLuint seed, GLuint element);
	void		attach				(GLuint buffer, GLuint tf);
	void		detach				(GLuint buffer, GLuint tf);
	bool		canAttachDeleted	(void) const { return false; }
	GLuint		getAttachment		(GLuint tf);
};

void BufferTfAttacher::initAttachment (GLuint seed, GLuint buffer)
{
	initBuffer(gl(), seed, GL_DYNAMIC_READ, buffer);
	log() << TestLog::Message << "// Initialized buffer " << buffer << " from seed " << seed
		  << TestLog::EndMessage;
}

void BufferTfAttacher::attach (GLuint buffer, GLuint tf)
{
	glBindTransformFeedback(GL_TRANSFORM_FEEDBACK, tf);
	glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, buffer);
	glBindTransformFeedback(GL_TRANSFORM_FEEDBACK, 0);
	GLU_CHECK_ERROR(gl().getError());
}

void BufferTfAttacher::detach (GLuint buffer, GLuint tf)
{
	DE_UNREF(buffer);
	glBindTransformFeedback(GL_TRANSFORM_FEEDBACK, tf);
	glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, 0);
	glBindTransformFeedback(GL_TRANSFORM_FEEDBACK, 0);
	GLU_CHECK_ERROR(gl().getError());
}

GLuint BufferTfAttacher::getAttachment (GLuint tf)
{
	GLint ret = 0;
	gl().bindTransformFeedback(GL_TRANSFORM_FEEDBACK, tf);
	gl().getIntegeri_v(GL_TRANSFORM_FEEDBACK_BUFFER_BINDING, 0, &ret);
	gl().bindTransformFeedback(GL_TRANSFORM_FEEDBACK, 0);
	GLU_CHECK_ERROR(gl().getError());
	return GLuint(ret);
}

class BufferTfOutputAttacher : public OutputAttacher
{
public:
				BufferTfOutputAttacher	(BufferTfAttacher&	attacher, ScaleProgram& program)
					: OutputAttacher	(attacher)
					, m_program			(program) {}
	void		setupContainer		(GLuint seed, GLuint container);
	void		drawAttachment		(GLuint attachment, Surface& dst);

private:
	ScaleProgram&	m_program;
};

void BufferTfOutputAttacher::drawAttachment (GLuint buffer, Surface& dst)
{
	VertexArray vao(getRenderContext());

	m_program.setPos(buffer, *vao);
	m_program.draw(*vao, 1.0, false, &dst);
	log() << TestLog::Message
		  << "// Drew output image with vertices from buffer " << buffer
		  << TestLog::EndMessage;
	GLU_CHECK_ERROR(gl().getError());
}

void BufferTfOutputAttacher::setupContainer (GLuint seed, GLuint tf)
{
	Buffer		posBuf	(getRenderContext());
	VertexArray	vao		(getRenderContext());

	initBuffer(gl(), seed, GL_STATIC_DRAW, *posBuf);
	m_program.setPos(*posBuf, *vao);

	glBindTransformFeedback(GL_TRANSFORM_FEEDBACK, tf);
	m_program.draw(*vao, -1.0, true, DE_NULL);
	log() << TestLog::Message
		  << "// Drew an image with seed " << seed << " with transform feedback to " << tf
		  << TestLog::EndMessage;
	glBindTransformFeedback(GL_TRANSFORM_FEEDBACK, 0);
	GLU_CHECK_ERROR(gl().getError());
}

class ES3Types : public ES2Types
{
public:
							ES3Types		(lt::Context& ctx);
private:
	ScaleProgram			m_program;
	QueryBinder				m_queryBind;
	SimpleType				m_queryType;
	SimpleBinder			m_tfBind;
	SimpleType				m_tfType;
	VertexArrayBinder		m_varrBind;
	SimpleType				m_varrType;
	SamplerBinder			m_samplerBind;
	SimpleType				m_samplerType;
	BufferVAOAttacher		m_bufVarrAtt;
	BufferVAOInputAttacher	m_bufVarrInAtt;
	BufferTfAttacher		m_bufTfAtt;
	BufferTfOutputAttacher	m_bufTfOutAtt;
};

ES3Types::ES3Types (lt::Context& ctx)
	: ES2Types		(ctx)
	, m_program		(ctx)
	, m_queryBind	(ctx)
	, m_queryType	(ctx, "query", &CallLogWrapper::glGenQueries,
					 &CallLogWrapper::glDeleteQueries,
					 &CallLogWrapper::glIsQuery, &m_queryBind)
	, m_tfBind		(ctx, &CallLogWrapper::glBindTransformFeedback, GL_TRANSFORM_FEEDBACK,
					 GL_TRANSFORM_FEEDBACK_BINDING, true)
	, m_tfType		(ctx, "transform_feedback", &CallLogWrapper::glGenTransformFeedbacks,
					 &CallLogWrapper::glDeleteTransformFeedbacks,
					 &CallLogWrapper::glIsTransformFeedback, &m_tfBind)
	, m_varrBind	(ctx)
	, m_varrType	(ctx, "vertex_array", &CallLogWrapper::glGenVertexArrays,
					 &CallLogWrapper::glDeleteVertexArrays,
					 &CallLogWrapper::glIsVertexArray, &m_varrBind)
	, m_samplerBind	(ctx)
	, m_samplerType	(ctx, "sampler", &CallLogWrapper::glGenSamplers,
					 &CallLogWrapper::glDeleteSamplers,
					 &CallLogWrapper::glIsSampler, &m_samplerBind, true)
	, m_bufVarrAtt	(ctx, m_bufferType, m_varrType, m_program)
	, m_bufVarrInAtt(m_bufVarrAtt)
	, m_bufTfAtt	(ctx, m_bufferType, m_tfType)
	, m_bufTfOutAtt	(m_bufTfAtt, m_program)
{
	Type* types[] = { &m_queryType, &m_tfType, &m_varrType, &m_samplerType };
	m_types.insert(m_types.end(), DE_ARRAY_BEGIN(types), DE_ARRAY_END(types));

	m_attachers.push_back(&m_bufVarrAtt);
	m_attachers.push_back(&m_bufTfAtt);

	m_inAttachers.push_back(&m_bufVarrInAtt);
	m_outAttachers.push_back(&m_bufTfOutAtt);
}

class TfDeleteActiveTest : public TestCase, private CallLogWrapper
{
	public:
						TfDeleteActiveTest	(gles3::Context& context,
											 const char* name, const char* description);
	IterateResult		iterate				(void);
};

TfDeleteActiveTest::TfDeleteActiveTest (gles3::Context& context,
										const char* name, const char* description)
	: TestCase			(context, name, description)
	, CallLogWrapper	(context.getRenderContext().getFunctions(),
						 context.getTestContext().getLog())
{
	enableLogging(true);
}

class ScopedTransformFeedbackFeedback
{
public:
							ScopedTransformFeedbackFeedback		(glu::CallLogWrapper& gl, GLenum type);
							~ScopedTransformFeedbackFeedback	(void);

private:
	glu::CallLogWrapper&	m_gl;
};

ScopedTransformFeedbackFeedback::ScopedTransformFeedbackFeedback (glu::CallLogWrapper& gl, GLenum type)
	: m_gl(gl)
{
	m_gl.glBeginTransformFeedback(type);
	GLU_EXPECT_NO_ERROR(m_gl.glGetError(), "glBeginTransformFeedback");
}

ScopedTransformFeedbackFeedback::~ScopedTransformFeedbackFeedback (void)
{
	m_gl.glEndTransformFeedback();
}

IterateResult TfDeleteActiveTest::iterate (void)
{
	static const char* const s_xfbVertexSource =	"#version 300 es\n"
													"void main ()\n"
													"{\n"
													"	gl_Position = vec4(float(gl_VertexID) / 2.0, float(gl_VertexID % 2) / 2.0, 0.0, 1.0);\n"
													"}\n";
	static const char* const s_xfbFragmentSource =	"#version 300 es\n"
													"layout(location=0) out mediump vec4 dEQP_FragColor;\n"
													"void main ()\n"
													"{\n"
													"	dEQP_FragColor = vec4(1.0, 1.0, 0.0, 1.0);\n"
													"}\n";

	glu::Buffer			buf			(m_context.getRenderContext());
	GLuint				tf			= 0;
	glu::ShaderProgram	program		(m_context.getRenderContext(),
									 glu::ProgramSources()
										<< glu::VertexSource(s_xfbVertexSource)
										<< glu::FragmentSource(s_xfbFragmentSource)
										<< glu::TransformFeedbackMode(GL_INTERLEAVED_ATTRIBS)
										<< glu::TransformFeedbackVarying("gl_Position"));

	if (!program.isOk())
	{
		m_testCtx.getLog() << program;
		throw tcu::TestError("failed to build program");
	}

	try
	{
		GLU_CHECK_CALL(glUseProgram(program.getProgram()));
		GLU_CHECK_CALL(glGenTransformFeedbacks(1, &tf));
		GLU_CHECK_CALL(glBindTransformFeedback(GL_TRANSFORM_FEEDBACK, tf));
		GLU_CHECK_CALL(glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, *buf));
		GLU_CHECK_CALL(glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, 3 * sizeof(glw::GLfloat[4]), DE_NULL, GL_DYNAMIC_COPY));

		{
			ScopedTransformFeedbackFeedback xfb(static_cast<glu::CallLogWrapper&>(*this), GL_TRIANGLES);

			glDeleteTransformFeedbacks(1, &tf);
			{
				GLenum err = glGetError();
				if (err != GL_INVALID_OPERATION)
					getTestContext().setTestResult(
						QP_TEST_RESULT_FAIL,
						"Deleting active transform feedback did not produce GL_INVALID_OPERATION");
				else
					getTestContext().setTestResult(QP_TEST_RESULT_PASS, "Pass");
			}
		}
		GLU_CHECK(); // ScopedTransformFeedbackFeedback::dtor might modify error state

		GLU_CHECK_CALL(glDeleteTransformFeedbacks(1, &tf));
	}
	catch (const glu::Error&)
	{
		glDeleteTransformFeedbacks(1, &tf);
		throw;
	}

	return STOP;
}

class TestGroup : public TestCaseGroup
{
public:
							TestGroup		(gles3::Context& context)
								: TestCaseGroup	(context, "lifetime", "Object lifetime tests")
							{}
	void					init			(void);
private:
	MovePtr<Types>			m_types;
};

void TestGroup::init (void)
{
	gles3::Context&	ctx		= getContext();
	lt::Context		ltCtx	(ctx.getRenderContext(), ctx.getTestContext());

	m_types	= MovePtr<Types>(new ES3Types(ltCtx));

	addTestCases(*this, *m_types);

	TestCaseGroup* deleteActiveGroup =
		new TestCaseGroup(ctx, "delete_active", "Delete active object");
	addChild(deleteActiveGroup);
	deleteActiveGroup->addChild(
		new TfDeleteActiveTest(ctx, "transform_feedback", "Transform Feedback"));
}

} // anonymous

TestCaseGroup* createLifetimeTests (Context& context)
{
	return new TestGroup(context);
}

} // Functional
} // gles3
} // deqp