/*
 *  Copyright (C) 2008 Hannu Saransaari
 *
 *  This program is distributed under the terms of the
 *  GNU General Public License.
 *
 *  This file is part of Dave the Ordinary Spaceman.
 *
 *  Dave the Ordinary Spaceman is free software: you can redistribute
 *  it and/or modify it under the terms of the GNU General Public
 *  License as published by the Free Software Foundation, either
 *  version 3 of the License, or (at your option) any later version.
 *
 *  Dave the Ordinary Spaceman is distributed in the hope that it
 *  will be useful, but WITHOUT ANY WARRANTY; without even the
 *  implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
 *  PURPOSE.  See the GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with Dave the Ordinary Spaceman. If not, see
 *  <http://www.gnu.org/licenses/>.
 *
 */

#ifndef _opengl_h_
#define _opengl_h_

#ifndef GL_GLEXT_PROTOTYPES
#  define GL_GLEXT_PROTOTYPES
#endif

#if defined(__MACOS__) || defined(__APPLE__)
#  include <OpenGL/gl.h>
#else
#  include <GL/glew.h>
#endif
#include <stdio.h>
#include <assert.h>
//#include "vector3.h"
//#include "vector2.h"

#ifndef NDEBUG

#  define CHECK_GL_ERRORS()\
	do {\
		GLenum err = glGetError();\
		assert(err == GL_NO_ERROR);\
	} while(false)
#  define CHECK_GL_FB_ERRORS()\
	do {\
		GLenum status = glCheckFramebufferStatusEXT(GL_FRAMEBUFFER_EXT);\
		assert(status == GL_FRAMEBUFFER_COMPLETE_EXT);\
	} while(false)

#else

#  define CHECK_GL_ERRORS() do {} while(false)
#  define CHECK_GL_FB_ERRORS() do {} while(false)

#endif

class GLProgram
{
public:
	GLProgram(const char* vertFile, const char* fragFile);
	~GLProgram();

	void use();
	static void unuse();

	void set_int(const char*, int);
	void set_float(const char*, float);
	void set_vector3(const char*, const float*);
	void set_vector2(const char*, const float*);
	void set_matrix4(const char* n, const float* m);

private:
	GLhandleARB prog;
};

class GLTexture
{
	friend class GLFramebuffer;
public:
	GLTexture(GLenum target = GL_TEXTURE_2D) : target(target)
	{
		assert(target == GL_TEXTURE_1D ||
				target == GL_TEXTURE_2D ||
				target == GL_TEXTURE_3D ||
				target == GL_TEXTURE_CUBE_MAP);

		glGenTextures(1, &id);

		assert(target == GL_TEXTURE_2D);

		parameter(GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
		parameter(GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
		parameter(GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);

		parameter(GL_TEXTURE_MIN_FILTER, GL_NEAREST);
		parameter(GL_TEXTURE_MAG_FILTER, GL_NEAREST);
	}

	~GLTexture()
	{
		glDeleteTextures(1, &id);
	}

	void bind(int unit)
	{
		assert(unit >= 0 && unit < 8);
		glActiveTexture(GL_TEXTURE0 + unit);
		glBindTexture(target, id);
	}

	void unbind(int unit)
	{
		assert(unit >= 0 && unit < 8);
		glActiveTexture(GL_TEXTURE0 + unit);
		glBindTexture(target, 0);
	}

	void set_auto_mipmaps()
	{
		bind(0);
		glGenerateMipmapEXT(GL_TEXTURE_2D);
	}

	void parameter(GLenum p, GLint val)
	{
		glBindTexture(target, id);
		glTexParameteri(target, p, val);
	}

protected:
	GLenum target;
	GLuint id;
};

class GLDepthTexture : public GLTexture
{
	public:
		GLDepthTexture(int width, int height, bool high=true) : GLTexture(GL_TEXTURE_2D)
	{
		bind(0);
		glTexImage2D(target, 0, high ? GL_DEPTH_COMPONENT24_ARB : GL_DEPTH_COMPONENT16_ARB, width, height, 0, GL_DEPTH_COMPONENT, GL_UNSIGNED_INT, NULL);
		unbind(0);
		CHECK_GL_ERRORS();
	}
};

class GLColorTexture2D : public GLTexture
{
public:
	GLColorTexture2D(GLenum format, int width, int height, bool alpha = false)
		: GLTexture(GL_TEXTURE_2D)
	{
		bind(0);
		glTexImage2D(target, 0, format, width, height, 0,
				alpha ? GL_RGB : GL_RGBA, GL_UNSIGNED_BYTE, NULL);
		CHECK_GL_ERRORS();
		unbind(0);
		CHECK_GL_ERRORS();
	}
};

class GLRenderbuffer
{
	friend class GLFramebuffer;
protected:
	GLRenderbuffer()
	{
		assert(glGenRenderbuffersEXT);
		glGenRenderbuffersEXT(1, &id);
		CHECK_GL_ERRORS();
	}

	~GLRenderbuffer()
	{
		glDeleteRenderbuffersEXT(1, &id);
	}

public:
	void bind()
	{
		glBindRenderbufferEXT(GL_RENDERBUFFER_EXT, id);
	}

	static void unbind()
	{
		glBindRenderbufferEXT(GL_RENDERBUFFER_EXT, 0);
	}

	private:
	GLuint id;
};

class GLDepthRenderbuffer : public GLRenderbuffer
{
public:
	GLDepthRenderbuffer(int width, int height)
	{
		bind();
		glRenderbufferStorageEXT(GL_RENDERBUFFER_EXT, GL_DEPTH_COMPONENT24_ARB,
				width, height);
		unbind();
		CHECK_GL_ERRORS();
	}
};

class GLFramebuffer
{
public:
	GLFramebuffer(bool screen = false)
	{
		assert(screen == false);
		glGenFramebuffersEXT(1, &id);
		bind();
		glDrawBuffer(GL_NONE);
		glReadBuffer(GL_NONE);
		unbind();
		color_mask = 0;

		CHECK_GL_ERRORS();
	}

	void set_depth(GLDepthRenderbuffer& rb)
	{
		CHECK_GL_ERRORS();
		bind();
		glFramebufferRenderbufferEXT(GL_FRAMEBUFFER_EXT,
				GL_DEPTH_ATTACHMENT_EXT, GL_RENDERBUFFER_EXT, rb.id);
		unbind();
		CHECK_GL_FB_ERRORS();
		CHECK_GL_ERRORS();
	}

	void set_depth(GLDepthTexture& tex)
	{
		bind();
		glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT,
				GL_DEPTH_ATTACHMENT_EXT,
				GL_TEXTURE_2D,
				tex.id,
				0);
		unbind();
		CHECK_GL_FB_ERRORS();
		CHECK_GL_ERRORS();
	}

	void set_color(GLTexture& tex, int i)
	{
		assert(tex.target == GL_TEXTURE_2D);
		assert(i >= 0 && i < 16);
		bind();

		glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT,
				GL_COLOR_ATTACHMENT0_EXT+i,
				GL_TEXTURE_2D,
				tex.id,
				0);

		color_mask |= 1U << i;

		CHECK_GL_ERRORS();

		GLenum m[16];
		int n = 0;
		for (int j = 0; j < 16; j++)
			if (color_mask & (1U << j))
				m[n++] = GL_COLOR_ATTACHMENT0_EXT+j;

#ifndef NDEBUG
		GLint maxDrawBuffers;
		glGetIntegerv(GL_MAX_DRAW_BUFFERS_ARB, &maxDrawBuffers);
		assert(n <= maxDrawBuffers);
#endif
		if (n == 1)
			glDrawBuffer(m[0]);
		else
			glDrawBuffersARB(n, m);

		unbind();

		CHECK_GL_FB_ERRORS();
		CHECK_GL_ERRORS();
	}

	void bind()
	{
		glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, id);
	}

	static void unbind()
	{
		glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0);
	}

private:
	GLuint id;
	unsigned int color_mask;
};

void draw_fullscreen_quad(bool i = false);

#endif
