/**
 * Copyright (c) 2006-2013 LOVE Development Team
 *
 * This software is provided 'as-is', without any express or implied
 * warranty.  In no event will the authors be held liable for any damages
 * arising from the use of this software.
 *
 * Permission is granted to anyone to use this software for any purpose,
 * including commercial applications, and to alter it and redistribute it
 * freely, subject to the following restrictions:
 *
 * 1. The origin of this software must not be misrepresented; you must not
 *    claim that you wrote the original software. If you use this software
 *    in a product, an acknowledgment in the product documentation would be
 *    appreciated but is not required.
 * 2. Altered source versions must be plainly marked as such, and must not be
 *    misrepresented as being the original software.
 * 3. This notice may not be removed or altered from any source distribution.
 **/

#include "Canvas.h"
#include "Graphics.h"
#include "common/Matrix.h"

#include <cstring> // For memcpy

namespace love
{
namespace graphics
{
namespace opengl
{

// strategy for fbo creation, interchangable at runtime:
// none, opengl >= 3.0, extensions
struct FramebufferStrategy
{
	/// create a new framebuffer, depth_stencil and texture
	/**
	 * @param[out] framebuffer   Framebuffer name
	 * @param[out] depth_stencil Name for packed depth and stencil buffer
	 * @param[out] img           Texture name
	 * @param[in]  width         Width of framebuffer
	 * @param[in]  height        Height of framebuffer
	 * @param[in]  texture_type  Type of the canvas texture.
	 * @return Creation status
	 */
	virtual GLenum createFBO(GLuint &, GLuint &, GLuint &, int, int, Canvas::TextureType)
	{
		return GL_FRAMEBUFFER_UNSUPPORTED;
	}
	/// remove objects
	/**
	 * @param[in] framebuffer   Framebuffer name
	 * @param[in] depth_stencil Name for packed depth and stencil buffer
	 * @param[in] img           Texture name
	 */
	virtual void deleteFBO(GLuint, GLuint, GLuint) {}
	virtual void bindFBO(GLuint) {}
};

struct FramebufferStrategyGL3 : public FramebufferStrategy
{
	virtual GLenum createFBO(GLuint &framebuffer, GLuint &depth_stencil,  GLuint &img, int width, int height, Canvas::TextureType texture_type)
	{
		// get currently bound fbo to reset to it later
		GLint current_fbo;
		glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &current_fbo);

		// create framebuffer
		glGenFramebuffers(1, &framebuffer);
		glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);

		// create stencil buffer
		glGenRenderbuffers(1, &depth_stencil);
		glBindRenderbuffer(GL_RENDERBUFFER, depth_stencil);
		glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_STENCIL, width, height);
		glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT,
			GL_RENDERBUFFER, depth_stencil);
		glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT,
			GL_RENDERBUFFER, depth_stencil);

		// generate texture save target
		GLint internalFormat;
		GLenum format;
		switch (texture_type)
		{
			case Canvas::TYPE_HDR:
				internalFormat = GL_RGBA16F;
				format = GL_FLOAT;
				break;
			case Canvas::TYPE_NORMAL:
			default:
				internalFormat = GL_RGBA8;
				format = GL_UNSIGNED_BYTE;
		}

		glGenTextures(1, &img);
		bindTexture(img);

		setTextureFilter(Image::getDefaultFilter());

		glTexImage2D(GL_TEXTURE_2D, 0, internalFormat, width, height,
			0, GL_RGBA, format, NULL);
		bindTexture(0);
		glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
			GL_TEXTURE_2D, img, 0);

		// check status
		GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);

		// unbind framebuffer
		glBindRenderbuffer(GL_RENDERBUFFER, 0);
		glBindFramebuffer(GL_FRAMEBUFFER, (GLuint) current_fbo);
		return status;
	}
	virtual void deleteFBO(GLuint framebuffer, GLuint depth_stencil,  GLuint img)
	{
		deleteTexture(img);
		glDeleteRenderbuffers(1, &depth_stencil);
		glDeleteFramebuffers(1, &framebuffer);
	}

	virtual void bindFBO(GLuint framebuffer)
	{
		glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
	}
};

struct FramebufferStrategyPackedEXT : public FramebufferStrategy
{
	virtual GLenum createFBO(GLuint &framebuffer, GLuint &depth_stencil, GLuint &img, int width, int height, Canvas::TextureType texture_type)
	{
		GLint current_fbo;
		glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING_EXT, &current_fbo);

		// create framebuffer
		glGenFramebuffersEXT(1, &framebuffer);
		glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, framebuffer);

		// create stencil buffer
		glGenRenderbuffersEXT(1, &depth_stencil);
		glBindRenderbufferEXT(GL_RENDERBUFFER_EXT, depth_stencil);
		glRenderbufferStorageEXT(GL_RENDERBUFFER_EXT, GL_DEPTH_STENCIL_EXT,
			width, height);
		glFramebufferRenderbufferEXT(GL_FRAMEBUFFER_EXT, GL_STENCIL_ATTACHMENT_EXT,
			GL_RENDERBUFFER_EXT, depth_stencil);
		glFramebufferRenderbufferEXT(GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT,
			GL_RENDERBUFFER_EXT, depth_stencil);

		// generate texture save target
		GLint internalFormat;
		GLenum format;
		switch (texture_type)
		{
			case Canvas::TYPE_HDR:
				internalFormat = GL_RGBA16F;
				format = GL_FLOAT;
				break;
			case Canvas::TYPE_NORMAL:
			default:
				internalFormat = GL_RGBA8;
				format = GL_UNSIGNED_BYTE;
		}

		glGenTextures(1, &img);
		bindTexture(img);

		setTextureFilter(Image::getDefaultFilter());

		glTexImage2D(GL_TEXTURE_2D, 0, internalFormat, width, height,
			0, GL_RGBA, format, NULL);
		bindTexture(0);
		glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT,
			GL_TEXTURE_2D, img, 0);

		// check status
		GLenum status = glCheckFramebufferStatusEXT(GL_FRAMEBUFFER_EXT);

		// unbind framebuffer
		glBindRenderbufferEXT(GL_RENDERBUFFER_EXT, 0);
		glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, (GLuint) current_fbo);
		return status;
	}

	virtual void deleteFBO(GLuint framebuffer, GLuint depth_stencil, GLuint img)
	{
		deleteTexture(img);
		glDeleteRenderbuffersEXT(1, &depth_stencil);
		glDeleteFramebuffersEXT(1, &framebuffer);
	}

	virtual void bindFBO(GLuint framebuffer)
	{
		glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, framebuffer);
	}
};

struct FramebufferStrategyEXT : public FramebufferStrategyPackedEXT
{
	virtual GLenum createFBO(GLuint &framebuffer, GLuint &stencil, GLuint &img, int width, int height, Canvas::TextureType texture_type)
	{
		GLint current_fbo;
		glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING_EXT, &current_fbo);

		// create framebuffer
		glGenFramebuffersEXT(1, &framebuffer);
		glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, framebuffer);

		// create stencil buffer
		glGenRenderbuffersEXT(1, &stencil);
		glBindRenderbufferEXT(GL_RENDERBUFFER_EXT, stencil);
		glRenderbufferStorageEXT(GL_RENDERBUFFER_EXT, GL_STENCIL_INDEX,
			width, height);
		glFramebufferRenderbufferEXT(GL_FRAMEBUFFER_EXT, GL_STENCIL_ATTACHMENT_EXT,
			GL_RENDERBUFFER_EXT, stencil);

		// generate texture save target
		GLint internalFormat;
		GLenum format;
		switch (texture_type)
		{
			case Canvas::TYPE_HDR:
				internalFormat = GL_RGBA16F;
				format = GL_FLOAT;
				break;
			case Canvas::TYPE_NORMAL:
			default:
				internalFormat = GL_RGBA8;
				format = GL_UNSIGNED_BYTE;
		}

		glGenTextures(1, &img);
		bindTexture(img);

		setTextureFilter(Image::getDefaultFilter());

		glTexImage2D(GL_TEXTURE_2D, 0, internalFormat, width, height,
			0, GL_RGBA, format, NULL);
		bindTexture(0);
		glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT,
			GL_TEXTURE_2D, img, 0);

		// check status
		GLenum status = glCheckFramebufferStatusEXT(GL_FRAMEBUFFER_EXT);

		// unbind framebuffer
		glBindRenderbufferEXT(GL_RENDERBUFFER_EXT, 0);
		glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, (GLuint) current_fbo);
		return status;
	}

	bool isSupported()
	{
		GLuint fb, stencil, img;
		GLenum status = createFBO(fb, stencil, img, 2, 2, Canvas::TYPE_NORMAL);
		deleteFBO(fb, stencil, img);
		return status == GL_FRAMEBUFFER_COMPLETE;
	}
};

FramebufferStrategy *strategy = NULL;

FramebufferStrategy strategyNone;

FramebufferStrategyGL3 strategyGL3;

FramebufferStrategyPackedEXT strategyPackedEXT;

FramebufferStrategyEXT strategyEXT;

Canvas *Canvas::current = NULL;

static void getStrategy()
{
	if (!strategy)
	{
		if (GLEE_VERSION_3_0 || GLEE_ARB_framebuffer_object)
			strategy = &strategyGL3;
		else if (GLEE_EXT_framebuffer_object && GLEE_EXT_packed_depth_stencil)
			strategy = &strategyPackedEXT;
		else if (GLEE_EXT_framebuffer_object && strategyEXT.isSupported())
			strategy = &strategyEXT;
		else
			strategy = &strategyNone;
	}
}

Canvas::Canvas(int width, int height, TextureType texture_type)
	: width(width)
	, height(height)
	, texture_type(texture_type)
{
	float w = static_cast<float>(width);
	float h = static_cast<float>(height);

	// world coordinates
	vertices[0].x = 0;
	vertices[0].y = h;
	vertices[1].x = 0;
	vertices[1].y = 0;
	vertices[2].x = w;
	vertices[2].y = 0;
	vertices[3].x = w;
	vertices[3].y = h;

	// texture coordinates
	vertices[0].s = 0;
	vertices[0].t = 0;
	vertices[1].s = 0;
	vertices[1].t = 1;
	vertices[2].s = 1;
	vertices[2].t = 1;
	vertices[3].s = 1;
	vertices[3].t = 0;

	settings.filter = Image::getDefaultFilter();

	getStrategy();

	loadVolatile();
}

Canvas::~Canvas()
{
	// reset framebuffer if still using this one
	if (current == this)
		stopGrab();

	unloadVolatile();
}

bool Canvas::isSupported()
{
	getStrategy();
	return (strategy != &strategyNone);
}

bool Canvas::isHdrSupported()
{
	return GLEE_VERSION_3_0 || GLEE_ARB_texture_float;
}

void Canvas::bindDefaultCanvas()
{
	if (current != NULL)
		current->stopGrab();
}

void Canvas::startGrab()
{
	// already grabbing
	if (current == this)
		return;

	// cleanup after previous fbo
	if (current != NULL)
		current->stopGrab();

	// bind buffer and clear screen
	glPushAttrib(GL_VIEWPORT_BIT | GL_TRANSFORM_BIT);
	strategy->bindFBO(fbo);
	glViewport(0, 0, width, height);

	// Reset the projection matrix
	glMatrixMode(GL_PROJECTION);
	glPushMatrix();
	glLoadIdentity();

	// Set up orthographic view (no depth)
	glOrtho(0.0, width, height, 0.0, -1.0, 1.0);

	// Switch back to modelview matrix
	glMatrixMode(GL_MODELVIEW);

	// indicate we are using this fbo
	current = this;
}

void Canvas::stopGrab()
{
	// i am not grabbing. leave me alone
	if (current != this)
		return;

	// bind default
	strategy->bindFBO(0);
	glMatrixMode(GL_PROJECTION);
	glPopMatrix();
	glPopAttrib();
	current = NULL;
}


void Canvas::clear(const Color &c)
{
	GLuint previous = 0;
	if (current != NULL)
		previous = current->fbo;

	strategy->bindFBO(fbo);
	glPushAttrib(GL_COLOR_BUFFER_BIT);
	glClearColor((float)c.r/255.0f, (float)c.g/255.0f, (float)c.b/255.0f, (float)c.a/255.0f);
	glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
	glPopAttrib();

	strategy->bindFBO(previous);
}

void Canvas::draw(float x, float y, float angle, float sx, float sy, float ox, float oy, float kx, float ky) const
{
	static Matrix t;
	t.setTransformation(x, y, angle, sx, sy, ox, oy, kx, ky);

	drawv(t, vertices);
}

void Canvas::drawq(love::graphics::Quad *quad, float x, float y, float angle, float sx, float sy, float ox, float oy, float kx, float ky) const
{
	const vertex *v = quad->getVertices();

	// mirror quad on x axis
	vertex w[4];
	memcpy(w, v, sizeof(vertex)*4);
	for (size_t i = 0; i < 4; ++i)
		w[i].t = 1. - w[i].t;

	static Matrix t;
	t.setTransformation(x, y, angle, sx, sy, ox, oy, kx, ky);

	drawv(t, w);
}

love::image::ImageData *Canvas::getImageData(love::image::Image *image)
{
	int row = 4 * width;
	int size = row * height;
	GLubyte *pixels  = new GLubyte[size];
	GLubyte *flipped = new GLubyte[size];

	strategy->bindFBO(fbo);
	glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
	if (current)
		strategy->bindFBO(current->fbo);
	else
		strategy->bindFBO(0);

	GLubyte *src = pixels, *dst = flipped + size - row;
	for (int i = 0; i < height; ++i, dst -= row, src += row)
	{
		memcpy(dst, src, row);
	}

	love::image::ImageData *img = image->newImageData(width, height, (void *)flipped);

	delete[] pixels;

	return img;
}

void Canvas::getPixel(unsigned char* pixel_rgba, int x, int y)
{
	strategy->bindFBO( fbo );

	glReadPixels(x, height - y, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixel_rgba);

	if (current)
		strategy->bindFBO( current->fbo );
	else
		strategy->bindFBO( 0 );
}

void Canvas::setFilter(const Image::Filter &f)
{
	bindTexture(img);
	setTextureFilter(f);
}

Image::Filter Canvas::getFilter() const
{
	bindTexture(img);
	return getTextureFilter();
}

void Canvas::setWrap(const Image::Wrap &w)
{
	bindTexture(img);
	setTextureWrap(w);
}

Image::Wrap Canvas::getWrap() const
{
	bindTexture(img);
	return getTextureWrap();
}

bool Canvas::loadVolatile()
{
	status = strategy->createFBO(fbo, depth_stencil, img, width, height, texture_type);
	if (status != GL_FRAMEBUFFER_COMPLETE)
		return false;

	setFilter(settings.filter);
	setWrap(settings.wrap);
	Color c;
	c.r = c.g = c.b = c.a = 0;
	clear(c);
	return true;
}

void Canvas::unloadVolatile()
{
	settings.filter = getFilter();
	settings.wrap   = getWrap();
	strategy->deleteFBO(fbo, depth_stencil, img);
}

int Canvas::getWidth()
{
	return width;
}

int Canvas::getHeight()
{
	return height;
}

void Canvas::drawv(const Matrix &t, const vertex *v) const
{
	glPushMatrix();

	glMultMatrixf((const GLfloat *)t.getElements());

	bindTexture(img);

	glEnableClientState(GL_VERTEX_ARRAY);
	glEnableClientState(GL_TEXTURE_COORD_ARRAY);
	glVertexPointer(2, GL_FLOAT, sizeof(vertex), (GLvoid *)&v[0].x);
	glTexCoordPointer(2, GL_FLOAT, sizeof(vertex), (GLvoid *)&v[0].s);
	glDrawArrays(GL_QUADS, 0, 4);
	glDisableClientState(GL_TEXTURE_COORD_ARRAY);
	glDisableClientState(GL_VERTEX_ARRAY);

	glPopMatrix();
}

bool Canvas::getConstant(const char *in, Canvas::TextureType &out)
{
	return textureTypes.find(in, out);
}

bool Canvas::getConstant(Canvas::TextureType in, const char *&out)
{
	return textureTypes.find(in, out);
}

StringMap<Canvas::TextureType, Canvas::TYPE_MAX_ENUM>::Entry Canvas::textureTypeEntries[] =
{
	{"normal", Canvas::TYPE_NORMAL},
	{"hdr",    Canvas::TYPE_HDR},
};
StringMap<Canvas::TextureType, Canvas::TYPE_MAX_ENUM> Canvas::textureTypes(Canvas::textureTypeEntries, sizeof(Canvas::textureTypeEntries));

} // opengl
} // graphics
} // love
