/*-------------------------------------------------------------------------
 * drawElements Quality Program Tester Core
 * ----------------------------------------
 *
 * 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 Platform that uses X11 via GLX.
 *//*--------------------------------------------------------------------*/

#include "tcuX11GlxPlatform.hpp"
#include "tcuX11Platform.hpp"

#include "tcuRenderTarget.hpp"
#include "glwInitFunctions.hpp"
#include "deUniquePtr.hpp"

#include <sstream>
#include <iterator>
#include <set>

#define GLX_GLXEXT_PROTOTYPES
#include <GL/glx.h>

namespace tcu
{
namespace x11
{
namespace glx
{

using de::UniquePtr;
using de::MovePtr;
using glu::ApiType;
using glu::ContextFactory;
using glu::ContextType;
using glu::RenderConfig;
using glu::RenderContext;
using tcu::CommandLine;
using tcu::RenderTarget;
using std::string;
using std::set;
using std::istringstream;
using std::ostringstream;
using std::istream_iterator;

typedef RenderConfig::Visibility Visibility;


template<typename T>
static inline T checkGLX(T value, const char* expr, const char* file, int line)
{
	if (!value)
		throw tcu::TestError("GLX call failed", expr, file, line);
	return value;
}

#define TCU_CHECK_GLX(EXPR) checkGLX(EXPR, #EXPR, __FILE__, __LINE__)
#define TCU_CHECK_GLX_CONFIG(EXPR) checkGLX((EXPR) == Success, #EXPR, __FILE__, __LINE__)

class GlxContextFactory : public glu::ContextFactory
{
public:
							GlxContextFactory	(EventState& eventState);
							~GlxContextFactory	(void);
	RenderContext*			createContext		(const RenderConfig&	config,
												 const CommandLine&		cmdLine) const;

	EventState&				getEventState		(void) const { return m_eventState;}

	const PFNGLXCREATECONTEXTATTRIBSARBPROC
							m_glXCreateContextAttribsARB;

private:
	EventState&				m_eventState;
};

class GlxDisplay : public x11::Display
{
public:
							GlxDisplay				(EventState&	eventState,
													 const char*	name);
	int						getGlxMajorVersion		(void) const { return m_majorVersion; }
	int						getGlxMinorVersion		(void) const { return m_minorVersion; }
	bool					isGlxExtensionSupported (const char* extName) const;

private:
	int						m_errorBase;
	int						m_eventBase;
	int						m_majorVersion;
	int						m_minorVersion;
	set<string>				m_extensions;
};

class GlxVisual
{
public:
							GlxVisual			(GlxDisplay& display, GLXFBConfig fbConfig);
	int						getAttrib			(int attribute);
	Visual*					getXVisual			(void) { return m_visual; }
	GLXContext				createContext		(const GlxContextFactory&	factory,
												 const ContextType&			contextType);
	GLXWindow				createWindow		(::Window xWindow);
	GlxDisplay&				getGlxDisplay		(void) { return m_display; }
	::Display*				getXDisplay			(void) { return m_display.getXDisplay(); }

private:
	GlxDisplay&				m_display;
	::Visual*				m_visual;
	const GLXFBConfig		m_fbConfig;
};

class GlxDrawable
{
public:
	virtual					~GlxDrawable		(void) {}

	virtual void			processEvents		(void) {}
	virtual void			getDimensions		(int* width, int* height) = 0;
	int		 				getWidth			(void);
	int						getHeight			(void);
	void					swapBuffers			(void) { glXSwapBuffers(getXDisplay(), getGLXDrawable()); }

	virtual ::Display*		getXDisplay			(void) = 0;
	virtual GLXDrawable		getGLXDrawable		(void) = 0;

protected:
							GlxDrawable			() {}
	unsigned int			getAttrib			(int attribute);
};

class GlxWindow : public GlxDrawable
{
public:
							GlxWindow			(GlxVisual& visual, const RenderConfig& cfg);
							~GlxWindow			(void);
	void					processEvents		(void) { m_x11Window.processEvents(); }
	::Display*				getXDisplay			(void) { return m_x11Display.getXDisplay(); }
	void	 				getDimensions		(int* width, int* height);

protected:
	GLXDrawable				getGLXDrawable		() { return m_GLXDrawable; }

private:
	x11::Display&			m_x11Display;
	x11::Window				m_x11Window;
	const GLXDrawable		m_GLXDrawable;
};

class GlxRenderContext : public RenderContext
{
public:
										GlxRenderContext 	(const GlxContextFactory&	factory,
															 const RenderConfig&		config);
										~GlxRenderContext	(void);
	virtual ContextType					getType				(void) const;
	virtual void						postIterate			(void);
	void								makeCurrent			(void);
	void								clearCurrent		(void);
	virtual const glw::Functions&		getFunctions		(void) const;
	virtual const tcu::RenderTarget&	getRenderTarget		(void) const;

private:
	GlxDisplay							m_glxDisplay;
	GlxVisual							m_glxVisual;
	ContextType							m_type;
	GLXContext							m_GLXContext;
	UniquePtr<GlxDrawable>				m_glxDrawable;
	RenderTarget						m_renderTarget;
	glw::Functions						m_functions;
};

extern "C"
{
	static int tcuX11GlxErrorHandler (::Display* display, XErrorEvent* event)
	{
		char buf[80];
		XGetErrorText(display, event->error_code, buf, sizeof(buf));
		tcu::print("X operation %u:%u failed: %s\n",
				   event->request_code, event->minor_code, buf);
		return 0;
	}
}

GlxContextFactory::GlxContextFactory (EventState& eventState)
	: glu::ContextFactory			("glx", "X11 GLX OpenGL Context")
	, m_glXCreateContextAttribsARB	(
		reinterpret_cast<PFNGLXCREATECONTEXTATTRIBSARBPROC>(
			TCU_CHECK_GLX(
				glXGetProcAddress(
					reinterpret_cast<const GLubyte*>("glXCreateContextAttribsARB")))))
	, m_eventState					(eventState)
{
	XSetErrorHandler(tcuX11GlxErrorHandler);
}

RenderContext* GlxContextFactory::createContext (const RenderConfig&	config,
												 const CommandLine&		cmdLine) const
{
	DE_UNREF(cmdLine);
	GlxRenderContext* const renderContext = new GlxRenderContext(*this, config);
	return renderContext;
}

GlxContextFactory::~GlxContextFactory (void)
{
}

GlxDisplay::GlxDisplay (EventState& eventState, const char* name)
	: x11::Display	(eventState, name)
{
	const Bool supported = glXQueryExtension(m_display, &m_errorBase, &m_eventBase);
	if (!supported)
		TCU_THROW(NotSupportedError, "GLX protocol not supported by X server");

	TCU_CHECK_GLX(glXQueryVersion(m_display, &m_majorVersion, &m_minorVersion));

	{
		const int screen = XDefaultScreen(m_display);
		// nVidia doesn't seem to report client-side extensions correctly,
		// so only use server side
		const char* const extensions =
			TCU_CHECK_GLX(glXQueryServerString(m_display, screen, GLX_EXTENSIONS));
		istringstream extStream(extensions);
		m_extensions = set<string>(istream_iterator<string>(extStream),
								   istream_iterator<string>());
	}
}


bool GlxDisplay::isGlxExtensionSupported (const char* extName) const
{
	return m_extensions.find(extName) != m_extensions.end();
}

//! Throw `tcu::NotSupportedError` if `dpy` is not compatible with GLX
//! version `major`.`minor`.
static void checkGlxVersion (const GlxDisplay& dpy, int major, int minor)
{
	const int dpyMajor = dpy.getGlxMajorVersion();
	const int dpyMinor = dpy.getGlxMinorVersion();
	if (!(dpyMajor == major && dpyMinor >= minor))
	{
		ostringstream oss;
		oss << "Server GLX version "
			<< dpyMajor << "." << dpyMinor
			<< " not compatible with required version "
			<< major << "." << minor;
		TCU_THROW(NotSupportedError, oss.str().c_str());
	}
}

//! Throw `tcu::NotSupportedError` if `dpy` does not support extension `extName`.
static void checkGlxExtension (const GlxDisplay& dpy, const char* extName)
{
	if (!dpy.isGlxExtensionSupported(extName))
	{
		ostringstream oss;
		oss << "GLX extension \"" << extName << "\" not supported";
		TCU_THROW(NotSupportedError, oss.str().c_str());
	}
}

GlxVisual::GlxVisual (GlxDisplay& display, GLXFBConfig fbConfig)
	: m_display		(display)
	, m_visual		(DE_NULL)
	, m_fbConfig	(fbConfig)
{
	XVisualInfo* visualInfo = glXGetVisualFromFBConfig(getXDisplay(), fbConfig);
	if (visualInfo != DE_NULL)
	{
		m_visual = visualInfo->visual;
		XFree(visualInfo);
	}
}

int GlxVisual::getAttrib (int attribute)
{
	int fbvalue;
	TCU_CHECK_GLX_CONFIG(glXGetFBConfigAttrib(getXDisplay(), m_fbConfig, attribute, &fbvalue));
	return fbvalue;
}

GLXContext GlxVisual::createContext (const GlxContextFactory&	factory,
									 const ContextType&			contextType)
{
	int				profileMask	= 0;
	const ApiType	apiType		= contextType.getAPI();

	checkGlxVersion(m_display, 1, 4);
	checkGlxExtension(m_display, "GLX_ARB_create_context");
	checkGlxExtension(m_display, "GLX_ARB_create_context_profile");

	switch (apiType.getProfile())
	{
		case glu::PROFILE_ES:
			checkGlxExtension(m_display, "GLX_EXT_create_context_es2_profile");
			profileMask = GLX_CONTEXT_ES2_PROFILE_BIT_EXT;
			break;
		case glu::PROFILE_CORE:
			profileMask = GLX_CONTEXT_CORE_PROFILE_BIT_ARB;
			break;
		case glu::PROFILE_COMPATIBILITY:
			profileMask = GLX_CONTEXT_COMPATIBILITY_PROFILE_BIT_ARB;
			break;
		default:
			DE_ASSERT(!"Impossible context profile");
	}

	const int attribs[] =
	{
		GLX_CONTEXT_MAJOR_VERSION_ARB,	apiType.getMajorVersion(),
		GLX_CONTEXT_MINOR_VERSION_ARB,	apiType.getMinorVersion(),
		GLX_CONTEXT_FLAGS_ARB,			0,
		GLX_CONTEXT_PROFILE_MASK_ARB,	profileMask,
		None
	};
	return TCU_CHECK_GLX(factory.m_glXCreateContextAttribsARB(
							 getXDisplay(), m_fbConfig, DE_NULL, True, attribs));
}

GLXWindow GlxVisual::createWindow (::Window xWindow)
{
	return TCU_CHECK_GLX(glXCreateWindow(getXDisplay(), m_fbConfig, xWindow, NULL));
}

unsigned GlxDrawable::getAttrib (int attrib)
{
	unsigned int value = 0;
	glXQueryDrawable(getXDisplay(), getGLXDrawable(), attrib, &value);
	return value;
}

int GlxDrawable::getWidth (void)
{
	int width = 0;
	getDimensions(&width, DE_NULL);
	return width;
}

int GlxDrawable::getHeight (void)
{
	int height = 0;
	getDimensions(DE_NULL, &height);
	return height;
}

GlxWindow::GlxWindow (GlxVisual& visual, const RenderConfig& cfg)
	: m_x11Display	(visual.getGlxDisplay())
	, m_x11Window	(m_x11Display, cfg.width, cfg.height,
					 visual.getXVisual())
	, m_GLXDrawable	(visual.createWindow(m_x11Window.getXID()))
{
	m_x11Window.setVisibility(cfg.windowVisibility != RenderConfig::VISIBILITY_HIDDEN);
}

void GlxWindow::getDimensions (int* width, int* height)
{
	if (width != DE_NULL)
		*width = getAttrib(GLX_WIDTH);
	if (height != DE_NULL)
		*height = getAttrib(GLX_HEIGHT);

	// glXQueryDrawable may be buggy, so fall back to X geometry if needed
	if ((width != DE_NULL && *width == 0) || (height != DE_NULL && *height == 0))
		m_x11Window.getDimensions(width, height);
}

GlxWindow::~GlxWindow (void)
{
	glXDestroyWindow(m_x11Display.getXDisplay(), m_GLXDrawable);
}

static const struct Attribute
{
	int						glxAttribute;
	int	RenderConfig::*		cfgMember;
} s_attribs[] =
{
	{ GLX_RED_SIZE,		&RenderConfig::redBits		},
	{ GLX_GREEN_SIZE,	&RenderConfig::greenBits	},
	{ GLX_BLUE_SIZE,	&RenderConfig::blueBits		},
	{ GLX_ALPHA_SIZE,	&RenderConfig::alphaBits	},
	{ GLX_DEPTH_SIZE,	&RenderConfig::depthBits	},
	{ GLX_STENCIL_SIZE,	&RenderConfig::stencilBits	},
	{ GLX_SAMPLES,		&RenderConfig::numSamples	},
	{ GLX_FBCONFIG_ID,	&RenderConfig::id			},
};

static deUint32 surfaceTypeToDrawableBits (RenderConfig::SurfaceType type)
{
	switch (type)
	{
		case RenderConfig::SURFACETYPE_WINDOW:
			return GLX_WINDOW_BIT;
		case RenderConfig::SURFACETYPE_OFFSCREEN_NATIVE:
			return GLX_PIXMAP_BIT;
		case RenderConfig::SURFACETYPE_OFFSCREEN_GENERIC:
			return GLX_PBUFFER_BIT;
		case RenderConfig::SURFACETYPE_DONT_CARE:
			return GLX_WINDOW_BIT | GLX_PIXMAP_BIT | GLX_PBUFFER_BIT;
		default:
			DE_ASSERT(!"Impossible case");
	}
	return 0;
}

static bool configMatches (GlxVisual& visual, const RenderConfig& renderCfg)
{
	if (renderCfg.id != RenderConfig::DONT_CARE)
		return visual.getAttrib(GLX_FBCONFIG_ID) == renderCfg.id;

	for (const Attribute* it = DE_ARRAY_BEGIN(s_attribs); it != DE_ARRAY_END(s_attribs); it++)
	{
		const int requested = renderCfg.*it->cfgMember;
		if (requested != RenderConfig::DONT_CARE &&
			requested != visual.getAttrib(it->glxAttribute))
			return false;
	}

	{
		deUint32 bits = surfaceTypeToDrawableBits(renderCfg.surfaceType);

		if ((visual.getAttrib(GLX_DRAWABLE_TYPE) & bits) == 0)
			return false;

		// It shouldn't be possible to have GLX_WINDOW_BIT set without a visual,
		// but let's make sure.
		if (renderCfg.surfaceType == RenderConfig::SURFACETYPE_WINDOW &&
			visual.getXVisual() == DE_NULL)
			return false;
	}

	return true;
}

class Rank
{
public:
				Rank		(void) : m_value(0), m_bitsLeft(64) {}
	void		add			(size_t bits, deUint32 value);
	void		sub			(size_t bits, deUint32 value);
	deUint64	getValue	(void) { return m_value; }

private:
	deUint64	m_value;
	size_t		m_bitsLeft;
};

void Rank::add (size_t bits, deUint32 value)
{
	TCU_CHECK_INTERNAL(m_bitsLeft >= bits);
	m_bitsLeft -= bits;
	m_value = m_value << bits | de::min((1U << bits) - 1, value);
}

void Rank::sub (size_t bits, deUint32 value)
{
	TCU_CHECK_INTERNAL(m_bitsLeft >= bits);
	m_bitsLeft -= bits;
	m_value = m_value << bits | ((1U << bits) - 1 - de::min((1U << bits) - 1U, value));
}

static deUint64 configRank (GlxVisual& visual)
{
	// Sanity checks.
	if (visual.getAttrib(GLX_DOUBLEBUFFER)					== False	||
		(visual.getAttrib(GLX_RENDER_TYPE) & GLX_RGBA_BIT)	== 0)
		return 0;

	Rank rank;
	int caveat		= visual.getAttrib(GLX_CONFIG_CAVEAT);
	int redSize		= visual.getAttrib(GLX_RED_SIZE);
	int greenSize	= visual.getAttrib(GLX_GREEN_SIZE);
	int blueSize	= visual.getAttrib(GLX_BLUE_SIZE);
	int alphaSize	= visual.getAttrib(GLX_ALPHA_SIZE);
	int depthSize	= visual.getAttrib(GLX_DEPTH_SIZE);
	int stencilSize	= visual.getAttrib(GLX_STENCIL_SIZE);
	int minRGB		= de::min(redSize, de::min(greenSize, blueSize));

	// Prefer conformant configurations.
	rank.add(1, (caveat != GLX_NON_CONFORMANT_CONFIG));

	// Prefer non-transparent configurations.
	rank.add(1, visual.getAttrib(GLX_TRANSPARENT_TYPE) == GLX_NONE);

	// Avoid stereo
	rank.add(1, visual.getAttrib(GLX_STEREO) == False);

	// Avoid overlays
	rank.add(1, visual.getAttrib(GLX_LEVEL) == 0);

	// Prefer to have some alpha.
	rank.add(1, alphaSize > 0);

	// Prefer to have a depth buffer.
	rank.add(1, depthSize > 0);

	// Prefer to have a stencil buffer.
	rank.add(1, stencilSize > 0);

	// Avoid slow configurations.
	rank.add(1, (caveat != GLX_SLOW_CONFIG));

	// Prefer larger, evenly distributed color depths
	rank.add(4, de::min(minRGB, alphaSize));

	// If alpha is low, choose best RGB
	rank.add(4, minRGB);

	// Prefer larger depth and stencil buffers
	rank.add(6, deUint32(depthSize + stencilSize));

	// Avoid excessive sampling
	rank.sub(5, visual.getAttrib(GLX_SAMPLES));

	// Prefer True/DirectColor
	int visualType = visual.getAttrib(GLX_X_VISUAL_TYPE);
	rank.add(1, visualType == GLX_TRUE_COLOR || visualType == GLX_DIRECT_COLOR);

	return rank.getValue();
}

static GlxVisual chooseVisual (GlxDisplay& display, const RenderConfig& cfg)
{
	::Display*	dpy			= display.getXDisplay();
	deUint64	maxRank		= 0;
	GLXFBConfig	maxConfig	= DE_NULL;
	int			numElems	= 0;

	GLXFBConfig* const fbConfigs = glXGetFBConfigs(dpy, DefaultScreen(dpy), &numElems);
	TCU_CHECK_MSG(fbConfigs != DE_NULL, "Couldn't query framebuffer configurations");

	for (int i = 0; i < numElems; i++)
	{
		GlxVisual visual(display, fbConfigs[i]);

		if (!configMatches(visual, cfg))
			continue;

		deUint64 cfgRank = configRank(visual);

		if (cfgRank > maxRank)
		{
			maxRank		= cfgRank;
			maxConfig	= fbConfigs[i];
		}
	}
	XFree(fbConfigs);

	if (maxRank == 0)
		TCU_THROW(NotSupportedError, "Requested GLX configuration not found or unusable");

	return GlxVisual(display, maxConfig);
}

GlxDrawable* createDrawable (GlxVisual& visual, const RenderConfig& config)
{
	RenderConfig::SurfaceType surfaceType = config.surfaceType;

	if (surfaceType == RenderConfig::SURFACETYPE_DONT_CARE)
	{
		if (visual.getXVisual() == DE_NULL)
			// No visual, cannot create X window
			surfaceType = RenderConfig::SURFACETYPE_OFFSCREEN_NATIVE;
		else
			surfaceType = RenderConfig::SURFACETYPE_WINDOW;
	}

	switch (surfaceType)
	{
		case RenderConfig::SURFACETYPE_DONT_CARE:
			DE_ASSERT(!"Impossible case");

		case RenderConfig::SURFACETYPE_WINDOW:
			return new GlxWindow(visual, config);
			break;

		case RenderConfig::SURFACETYPE_OFFSCREEN_NATIVE:
			// \todo [2013-11-28 lauri] Pixmaps

		case RenderConfig::SURFACETYPE_OFFSCREEN_GENERIC:
			// \todo [2013-11-28 lauri] Pbuffers

		default:
			TCU_THROW(NotSupportedError, "Unsupported surface type");
	}

	return DE_NULL;
}

struct GlxFunctionLoader : public glw::FunctionLoader
{
							GlxFunctionLoader	(void) {}

	glw::GenericFuncType	get					(const char* name) const
	{
		return glXGetProcAddress(reinterpret_cast<const GLubyte*>(name));
	}
};

GlxRenderContext::GlxRenderContext (const GlxContextFactory&	factory,
									const RenderConfig&			config)
	: m_glxDisplay		(factory.getEventState(), DE_NULL)
	, m_glxVisual		(chooseVisual(m_glxDisplay, config))
	, m_type			(config.type)
	, m_GLXContext		(m_glxVisual.createContext(factory, config.type))
	, m_glxDrawable		(createDrawable(m_glxVisual, config))
	, m_renderTarget	(m_glxDrawable->getWidth(), m_glxDrawable->getHeight(),
						 PixelFormat(m_glxVisual.getAttrib(GLX_RED_SIZE),
									 m_glxVisual.getAttrib(GLX_GREEN_SIZE),
									 m_glxVisual.getAttrib(GLX_BLUE_SIZE),
									 m_glxVisual.getAttrib(GLX_ALPHA_SIZE)),
						 m_glxVisual.getAttrib(GLX_DEPTH_SIZE),
						 m_glxVisual.getAttrib(GLX_STENCIL_SIZE),
						 m_glxVisual.getAttrib(GLX_SAMPLES))
{
	const GlxFunctionLoader loader;
	makeCurrent();
	glu::initFunctions(&m_functions, &loader, config.type.getAPI());
}

GlxRenderContext::~GlxRenderContext (void)
{
	clearCurrent();
	if (m_GLXContext != DE_NULL)
		glXDestroyContext(m_glxDisplay.getXDisplay(), m_GLXContext);
}

void GlxRenderContext::makeCurrent (void)
{
	const GLXDrawable drawRead = m_glxDrawable->getGLXDrawable();
	TCU_CHECK_GLX(glXMakeContextCurrent(m_glxDisplay.getXDisplay(),
										drawRead, drawRead, m_GLXContext));
}

void GlxRenderContext::clearCurrent (void)
{
	TCU_CHECK_GLX(glXMakeContextCurrent(m_glxDisplay.getXDisplay(),
										None, None, DE_NULL));
}

ContextType GlxRenderContext::getType (void) const
{
	return m_type;
}

void GlxRenderContext::postIterate (void)
{
	m_glxDrawable->swapBuffers();
	m_glxDrawable->processEvents();
	m_glxDisplay.processEvents();
}

const RenderTarget& GlxRenderContext::getRenderTarget (void) const
{
	return m_renderTarget;
}

const glw::Functions& GlxRenderContext::getFunctions (void) const
{
	return m_functions;
}

MovePtr<ContextFactory> createContextFactory (EventState& eventState)
{
	return MovePtr<ContextFactory>(new GlxContextFactory(eventState));
}

} // glx
} // x11
} // tcu