#include "tr_local.h"

#define MAX_FBO_COLOR_BUFFERS		8

/*
===============
Render buffer
Can be either a zbuffer, a stencil buffer or a multisampled color buffer
===============
*/

struct fboRenderBuffer_s {
	GLuint			id;
	int				internalFormat;

	int				width;
	int				height;
	qboolean		msaa;
};

typedef fboRenderBuffer_t fboZBuffer_t;
typedef fboRenderBuffer_t fboStencilBuffer_t;

/*
===============
Color buffer

If anti-aliased we'll have a texture, a render buffer
and two frame buffers needed for the MSAA resolve.

Otherwise, only the texture is initialized.
===============
*/

struct fboColorBuffer_s {
	fboRenderBuffer_t	*buf;
	image_t				*tex;
	GLuint				fboResolve[2];
	qboolean			dirty;
};

/*
===============
Frame buffer

Has one or more color buffers, an optional zbuffer
and an optional stencil buffer
===============
*/

struct fbo_s {
	GLuint				id;
	
	int					numColorBufs;

	fboColorBuffer_t*	color[MAX_FBO_COLOR_BUFFERS];
	fboZBuffer_t*		depth;
	fboStencilBuffer_t*	stencil;

	char				name[MAX_QPATH];
};


qboolean R_FBO_CheckStatus(void)
{
	GLenum status = qglCheckFramebufferStatusEXT(GL_FRAMEBUFFER_EXT);
	const char* msg = "unknown error";
	
	switch (status)
	{
	case GL_FRAMEBUFFER_COMPLETE_EXT:
		return qtrue;

	case GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT_EXT:
		msg = "incomplete attachment";
		break;
	
	case GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT_EXT:
		msg = "missing attachment";
		break;
	
	//case GL_FRAMEBUFFER_INCOMPLETE_DUPLICATE_ATTACHMENT_EXT:
	//	msg = "duplicate attachment";
	//	break;
	
	case GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS_EXT:
		msg = "mismatched dimensions";
		break;
	
	case GL_FRAMEBUFFER_INCOMPLETE_FORMATS_EXT:
		msg = "mismatched formats";
		break;
	
	case GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER_EXT:
		msg = "incomplete draw buffer";
		break;
	
	case GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER_EXT:
		msg = "incomplete read buffer";
		break;

	case GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE_EXT:
		msg = "mismatched multisample settings";
		break;
	
	case GL_FRAMEBUFFER_UNSUPPORTED_EXT:
		msg = "unsupported";
		break;

	default:
		msg = va("0x%x", status);
		break;
	}

	ri.Error(ERR_FATAL, "Framebuffer setup error: %s\n", msg);
	
	return qfalse;
}

void R_FBO_InitRenderBuffer(fboRenderBuffer_t* buf)
{
	buf->id = 0;
	qglGenRenderbuffersEXT(1, &buf->id);
	qglBindRenderbufferEXT(GL_RENDERBUFFER_EXT, buf->id);
	if (buf->msaa)
	{
		qglRenderbufferStorageMultisampleEXT(GL_RENDERBUFFER_EXT, tr.fbo.samples, buf->internalFormat, buf->width, buf->height);
	}
	else
	{
		qglRenderbufferStorageEXT(GL_RENDERBUFFER_EXT, buf->internalFormat, buf->width, buf->height);
	}
	qglBindRenderbufferEXT(GL_RENDERBUFFER_EXT, 0);
}

void R_FBO_ReleaseRenderBuffer(fboRenderBuffer_t* buf)
{
	if (buf->id == 0)
		return;

	qglDeleteRenderbuffersEXT(1, &buf->id);
	buf->id = 0;
}

fboRenderBuffer_t *R_FBO_CreateRenderBuffer(int width, int height, int pixelformat, qboolean msaa)
{
	fboRenderBuffer_t* ret = NULL;
	if (tr.fbo.numRenderBuffers >= ARRAY_SIZE(tr.fbo.renderBuffers))
	{
		ri.Error(ERR_FATAL, "Too many FBO render buffers\n");
		return NULL;
	}

	ret = ri.Hunk_Alloc(sizeof(*ret), h_low);
	memset(ret, 0, sizeof(*ret));

	ret->width = width;
	ret->height = height;
	ret->internalFormat = pixelformat;
	ret->msaa = (msaa && tr.fbo.samples > 1);
	
	R_FBO_InitRenderBuffer(ret);

	tr.fbo.renderBuffers[tr.fbo.numRenderBuffers++] = ret;
	
	return ret;
}

fboColorBuffer_t *R_FBO_CreateColorBuffer(const char* name, int width, int height, qboolean msaa, qboolean mipmap, int clamp)
{
	if (tr.fbo.numColorBuffers>= ARRAY_SIZE(tr.fbo.colorBuffers))
	{
		ri.Error(ERR_FATAL, "Too many FBO color buffers\n");
		return NULL;
	}
	else
	{
		qboolean			realmsaa	= msaa && tr.fbo.samples > 1;
		image_t				*texture	= R_CreateRenderTarget(name, width, height, mipmap, clamp);
		fboRenderBuffer_t	*buf		= realmsaa ? R_FBO_CreateRenderBuffer(width, height, texture->internalFormat, qtrue) : NULL;
		fboColorBuffer_t	*ret		= ri.Hunk_Alloc(sizeof(*ret), h_low);

		Com_Memset(ret, 0, sizeof(*ret));

		ret->buf = buf;
		ret->tex = texture;
		ret->dirty = qfalse;

		if (realmsaa)
		{
			qglGenFramebuffersEXT(2, ret->fboResolve);
			
			qglBindFramebufferEXT(GL_FRAMEBUFFER_EXT, ret->fboResolve[0]);
			qglFramebufferRenderbufferEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, GL_RENDERBUFFER_EXT, ret->buf->id);
			R_FBO_CheckStatus();

			qglBindFramebufferEXT(GL_FRAMEBUFFER_EXT, ret->fboResolve[1]);
			qglFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, GL_TEXTURE_2D, ret->tex->texnum, 0);
			R_FBO_CheckStatus();

			qglBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0);
		}
		else
		{
			ret->fboResolve[0] = ret->fboResolve[1] = 0;
		}

		tr.fbo.colorBuffers[tr.fbo.numColorBuffers++] = ret;
		
		return ret;
	}
}

void R_FBO_ReleaseColorBuffer(fboColorBuffer_t* color)
{
	if (color->buf)
	{
		qglDeleteFramebuffersEXT(2, color->fboResolve);
	}
}

fboZBuffer_t *R_FBO_CreateZBuffer(int width, int height, qboolean msaa)
{
	return R_FBO_CreateRenderBuffer(width, height, GL_DEPTH_COMPONENT24, msaa);
}

void R_FBO_Init(fbo_t* fbo)
{
	memset(fbo, 0, sizeof(*fbo));
	qglGenFramebuffersEXT(1, &fbo->id);
}

void R_FBO_Release(fbo_t* fbo)
{
	if (fbo->id == 0)
		return;

	qglDeleteFramebuffersEXT(1, &fbo->id);
	fbo->id = 0;
}

void R_FBO_AddColorBuffer(fbo_t* fbo, fboColorBuffer_t* color)
{
	GLenum attach;
	if (fbo->numColorBufs >= ARRAY_SIZE(fbo->color))
	{
		ri.Error(ERR_FATAL, "Max FBO color buffers exceeded.\n");
		return;
	}
	fbo->color[fbo->numColorBufs++] = color;
	
	qglBindFramebufferEXT(GL_FRAMEBUFFER_EXT, fbo->id);
	attach = GL_COLOR_ATTACHMENT0_EXT + fbo->numColorBufs - 1;
	if (color->buf)
	{
		qglFramebufferRenderbufferEXT(GL_FRAMEBUFFER_EXT, attach, GL_RENDERBUFFER_EXT, color->buf->id);
	}
	else
	{
		qglFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, attach, GL_TEXTURE_2D, color->tex->texnum, 0);
	}
}

/*
===============
R_FBO_Bind
===============
*/

fbo_t* R_FBO_Bind(fbo_t* fbo)
{
	fbo_t* old = glState.currentFBO;
	GLuint id = 0;
	if (!glRefConfig.framebufferObject)
	{
		glState.currentFBO = NULL;
		return NULL;
	}
	
	if (old == fbo)
		return old;

	glState.currentFBO = fbo;
	if (fbo)
		id = fbo->id;

	qglBindFramebufferEXT(GL_FRAMEBUFFER_EXT, id);

	if (fbo)
	{
		int i;
		for (i=0; i<fbo->numColorBufs; ++i)
		{
			fboColorBuffer_t* color = fbo->color[i];
			color->dirty = (color->buf != NULL);
		}
		
		if (1)
			R_FBO_CheckStatus();
	}

	return old;
}

/*
===============
R_FBO_BindColorBuffer
===============
*/

void R_FBO_BindColorBuffer(fbo_t* fbo, int index)
{
	fboColorBuffer_t* color = fbo->color[index];
	if (color->dirty)
	{
		color->dirty = qfalse;
		
		// resolve
		if (color->buf)
		{
			int id;
			int width = color->buf->width;
			int height = color->buf->height;
			
			qglBindFramebufferEXT(GL_READ_FRAMEBUFFER_EXT, color->fboResolve[0]);
			qglBindFramebufferEXT(GL_DRAW_FRAMEBUFFER_EXT, color->fboResolve[1]);
			qglBlitFramebufferEXT(0, 0, width, height, 0, 0, width, height, GL_COLOR_BUFFER_BIT, GL_LINEAR);
			
			if (glState.currentFBO)
				id = glState.currentFBO->id;
			else
				id = 0;

			qglBindFramebufferEXT(GL_DRAW_FRAMEBUFFER_EXT, id);
			qglBindFramebufferEXT(GL_READ_FRAMEBUFFER_EXT, id);
		}
	}
	GL_Bind(color->tex);
}

/*
===============
R_FBO_CreateEx
===============
*/

fbo_t* R_FBO_CreateEx(const char* name, int numColorBuffers, fboColorBuffer_t** colorBuffers, fboZBuffer_t* depth, fboStencilBuffer_t* stencil)
{
	fbo_t* fbo = NULL;

	if (tr.fbo.numFBOs >= ARRAY_SIZE(tr.fbo.fbos))
	{
		ri.Error(ERR_FATAL, "Too many FBO's\n");
		return NULL;
	}

	ri.Printf(PRINT_DEVELOPER, "Creating FBO %s: %d color/%d depth/%d stencil\n", name, numColorBuffers, depth != NULL, stencil != NULL);

	fbo = ri.Hunk_Alloc(sizeof(*fbo), h_low);

	R_FBO_Init(fbo);

	Q_strncpyz(fbo->name, name, ARRAY_SIZE(fbo->name));

	while (numColorBuffers-- > 0)
		R_FBO_AddColorBuffer(fbo, *colorBuffers++);

	fbo->depth = depth;
	fbo->stencil = stencil;

	qglBindFramebufferEXT(GL_FRAMEBUFFER_EXT, fbo->id);

	if (fbo->depth)
		qglFramebufferRenderbufferEXT(GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT, GL_RENDERBUFFER_EXT, fbo->depth->id);

	if (fbo->stencil)
		qglFramebufferRenderbufferEXT(GL_FRAMEBUFFER_EXT, GL_STENCIL_ATTACHMENT_EXT, GL_RENDERBUFFER_EXT, fbo->stencil->id);
	
	qglBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0);

	tr.fbo.fbos[tr.fbo.numFBOs++] = fbo;

	return fbo;
}

fbo_t* R_FBO_CreateSimple(const char* name, fboColorBuffer_t* color, fboZBuffer_t* depth, fboStencilBuffer_t* stencil)
{
	return R_FBO_CreateEx(name, color != NULL, &color, depth, stencil);
}

/*
===============
R_FBO_CreateDefaultBuffers
===============
*/

static void R_FBO_CreateDefaultBuffers(void)
{
	qboolean msaa = tr.fbo.samples > 1;

	tr.fbo.full[0] = R_FBO_CreateSimple(
		"main",
		R_FBO_CreateColorBuffer("*framebuffer0", glConfig.vidWidth, glConfig.vidHeight, msaa, qfalse, GL_CLAMP_TO_EDGE),
		R_FBO_CreateZBuffer(glConfig.vidWidth, glConfig.vidHeight, msaa),
		NULL);

	tr.fbo.full[1] = R_FBO_CreateSimple(
		"postprocess",
		R_FBO_CreateColorBuffer("*framebuffer1", glConfig.vidWidth, glConfig.vidHeight, msaa, qfalse, GL_CLAMP_TO_EDGE),
		tr.fbo.full[0]->depth,
		NULL);

	tr.fbo.quarter[0] = R_FBO_CreateSimple(
		"quarter0",
		R_FBO_CreateColorBuffer("*quarterBuffer0", glConfig.vidWidth/2, glConfig.vidHeight/2, qfalse, qfalse, GL_CLAMP_TO_EDGE),
		NULL,
		NULL);
	
	tr.fbo.quarter[1] = R_FBO_CreateSimple(
		"quarter1",
		R_FBO_CreateColorBuffer("*quarterBuffer1", glConfig.vidWidth/2, glConfig.vidHeight/2, qfalse, qfalse, GL_CLAMP_TO_EDGE),
		NULL,
		NULL);
	
	tr.fbo.tiny[0] = R_FBO_CreateSimple(
		"tiny0",
		R_FBO_CreateColorBuffer("*tinyBuffer0", glConfig.vidWidth/4, glConfig.vidHeight/4, qfalse, qfalse, GL_CLAMP_TO_EDGE),
		NULL,
		NULL);
	
	tr.fbo.tiny[1] = R_FBO_CreateSimple(
		"tiny1",
		R_FBO_CreateColorBuffer("*tinyBuffer1", glConfig.vidWidth/4, glConfig.vidHeight/4, qfalse, qfalse, GL_CLAMP_TO_EDGE),
		NULL,
		NULL);
	
	ri.Printf(PRINT_DEVELOPER, "...created %d FBOs\n", tr.fbo.numFBOs);
}

/*
===============
R_InitFBOs
===============
*/

void R_InitFBOs(void)
{
	GLint maxSamples = 0;
	if (!glRefConfig.framebufferObject)
		return;

	ri.Printf(PRINT_ALL, "------- R_InitFBOs -------\n");

	if (glRefConfig.framebufferMultisample && glRefConfig.framebufferBlit)
	{
		qglGetIntegerv(GL_MAX_SAMPLES_EXT, &maxSamples);
		tr.fbo.samples = maxSamples < r_ext_multisample->integer ? maxSamples : r_ext_multisample->integer;
		if (tr.fbo.samples < 2)
			tr.fbo.samples = 0;
	}
	else
	{
		tr.fbo.samples = 0;
	}

	if (tr.fbo.samples > 1)
		ri.Printf(PRINT_ALL, "MSAA enabled with %d samples (max %d)\n", tr.fbo.samples, maxSamples);

	R_FBO_CreateDefaultBuffers();

	R_FBO_Bind(tr.fbo.full[0]);
}


/*
===============
R_ShutDownFBOs
===============
*/

void R_ShutDownFBOs(void)
{
	if (!glRefConfig.framebufferObject)
		return;

	ri.Printf(PRINT_ALL, "------- R_ShutdownFBOs -------\n");

	R_FBO_Bind(NULL);

	while (tr.fbo.numRenderBuffers>0)
		R_FBO_ReleaseRenderBuffer(tr.fbo.renderBuffers[--tr.fbo.numRenderBuffers]);

	while (tr.fbo.numColorBuffers>0)
		R_FBO_ReleaseColorBuffer(tr.fbo.colorBuffers[--tr.fbo.numColorBuffers]);
	
	while (tr.fbo.numFBOs>0)
		R_FBO_Release(tr.fbo.fbos[--tr.fbo.numFBOs]);
}

