/* gltest - small OpenGL tearing test program
 * Copyright (C) 2012-2013 Ralf Jung <post@ralfj.de>
 *
 * This program 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 2 of the License, or
 * (at your option) any later version.
 *
 * This program 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 this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */

#include "glutil.h"

#include <stdio.h>
#include <stdlib.h>
#include <string>
#include <sstream>

// extension functions we use
typedef GLuint (*GLCREATESHADERPROC) (GLenum type);
typedef void (*GLGETSHADERIVPROC) (GLuint shader, GLenum pname, GLint *params);
typedef void (*GLGETSHADERINFOLOGPROC) (GLuint shader, GLsizei bufSize, GLsizei *length, GLchar *infoLog);
typedef void (*GLSHADERSOURCEPROC) (GLuint shader, GLsizei count, const GLchar* const *string, const GLint *length);
typedef void (*GLCOMPILESHADERPROC) (GLuint shader);
typedef GLuint (*GLCREATEPROGRAMPROC) (void);
typedef void (*GLATTACHSHADERPROC) (GLuint program, GLuint shader);
typedef void (*GLLINKPROGRAMPROC) (GLuint program);
typedef GLint (*GLGETATTRIBLOCATIONPROC) (GLuint program, const GLchar *name);
typedef void (*GLUSEPROGRAMPROC) (GLuint program);

typedef void (*GLVERTEXATTRIBPOINTERPROC) (GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const GLvoid *pointer);
typedef void (*GLENABLEVERTEXATTRIBARRAYPROC) (GLuint index);
typedef void (*GLGENBUFFERSPROC) (GLsizei n, GLuint *buffers);
typedef void (*GLBINDBUFFERPROC) (GLenum target, GLuint buffer);
typedef void (*GLBUFFERDATAPROC) (GLenum target, GLsizeiptr size, const GLvoid *data, GLenum usage);
typedef void (*GLDELETEBUFFERSPROC) (GLsizei n, const GLuint *buffers);

#ifdef CON_GL3
// VAOs are only supported in GL3+, and not needed in earler contexts
typedef void (*GLBINDVERTEXARRAYPROC) (GLuint array);
typedef void (*GLDELETEVERTEXARRAYSPROC) (GLsizei n, const GLuint *arrays);
typedef void (*GLGENVERTEXARRAYSPROC) (GLsizei n, GLuint *arrays);
#endif

GLGETSHADERIVPROC p_glGetShaderiv = NULL;
GLGETSHADERINFOLOGPROC p_glGetShaderInfoLog = NULL;
GLCREATESHADERPROC p_glCreateShader = NULL;
GLSHADERSOURCEPROC p_glShaderSource = NULL;
GLCOMPILESHADERPROC p_glCompileShader = NULL;
GLCREATEPROGRAMPROC p_glCreateProgram = NULL;
GLATTACHSHADERPROC p_glAttachShader = NULL;
GLLINKPROGRAMPROC p_glLinkProgram = NULL;
GLGETATTRIBLOCATIONPROC p_glGetAttribLocation = NULL;
GLUSEPROGRAMPROC p_glUseProgram = NULL;

GLVERTEXATTRIBPOINTERPROC p_glVertexAttribPointer = NULL;
GLENABLEVERTEXATTRIBARRAYPROC p_glEnableVertexAttribArray = NULL;
GLGENBUFFERSPROC p_glGenBuffers = NULL;
GLBINDBUFFERPROC p_glBindBuffer = NULL;
GLBUFFERDATAPROC p_glBufferData = NULL;
GLDELETEBUFFERSPROC p_glDeleteBuffers = NULL;

#ifdef CON_GL3
GLBINDVERTEXARRAYPROC p_glBindVertexArray = NULL;
GLDELETEVERTEXARRAYSPROC p_glDeleteVertexArrays = NULL;
GLGENVERTEXARRAYSPROC p_glGenVertexArrays = NULL;
#endif

// resolve function pointers
static T_proc resolveFunctionPointer(T_glGetProcAddress p_glGetProcAddress, const char *name)
{
	T_proc proc = p_glGetProcAddress(name);
	if (proc == NULL) {
		die("Error resolvung function %s\n", name);
	}
	return proc;
}

void resolveFunctionPointers(T_glGetProcAddress p_glGetProcAddress)
{
#ifdef CON_GL1
	// we need to check if we have GL 2.0 or greater
	std::string version = (char*)glGetString(GL_VERSION);
	std::stringstream ssv(version);
	std::string major;
	std::getline(ssv, major, '.');
	std::stringstream ssm(major);
	int majorVersion;
	ssm >> majorVersion;
	if (majorVersion < 2) {
		die("Need at least GL 2.0 to function properly, but detected version %d\n", majorVersion);
	}
#endif
	
	// now get the function pointers
	p_glGetShaderiv = (GLGETSHADERIVPROC)resolveFunctionPointer(p_glGetProcAddress, "glGetShaderiv");
	p_glGetShaderInfoLog = (GLGETSHADERINFOLOGPROC)resolveFunctionPointer(p_glGetProcAddress, "glGetShaderInfoLog");
	p_glCreateShader = (GLCREATESHADERPROC)resolveFunctionPointer(p_glGetProcAddress, "glCreateShader");
	p_glShaderSource = (GLSHADERSOURCEPROC)resolveFunctionPointer(p_glGetProcAddress, "glShaderSource");
	p_glCompileShader = (GLCOMPILESHADERPROC)resolveFunctionPointer(p_glGetProcAddress, "glCompileShader");
	p_glCreateProgram = (GLCREATEPROGRAMPROC)resolveFunctionPointer(p_glGetProcAddress, "glCreateProgram");
	p_glAttachShader = (GLATTACHSHADERPROC)resolveFunctionPointer(p_glGetProcAddress, "glAttachShader");
	p_glLinkProgram = (GLLINKPROGRAMPROC)resolveFunctionPointer(p_glGetProcAddress, "glLinkProgram");
	p_glGetAttribLocation = (GLGETATTRIBLOCATIONPROC)resolveFunctionPointer(p_glGetProcAddress, "glGetAttribLocation");

#ifdef CON_GL3
	p_glBindVertexArray = (GLBINDVERTEXARRAYPROC)resolveFunctionPointer(p_glGetProcAddress, "glBindVertexArray");
	p_glDeleteVertexArrays = (GLDELETEVERTEXARRAYSPROC)resolveFunctionPointer(p_glGetProcAddress, "glDeleteVertexArrays");
	p_glGenVertexArrays = (GLGENVERTEXARRAYSPROC)resolveFunctionPointer(p_glGetProcAddress, "glGenVertexArrays");
#endif

	p_glUseProgram = (GLUSEPROGRAMPROC)resolveFunctionPointer(p_glGetProcAddress, "glUseProgram");
	p_glVertexAttribPointer = (GLVERTEXATTRIBPOINTERPROC)resolveFunctionPointer(p_glGetProcAddress, "glVertexAttribPointer");
	p_glEnableVertexAttribArray = (GLENABLEVERTEXATTRIBARRAYPROC)resolveFunctionPointer(p_glGetProcAddress, "glEnableVertexAttribArray");
	p_glGenBuffers = (GLGENBUFFERSPROC)resolveFunctionPointer(p_glGetProcAddress, "glGenBuffers");
	p_glBindBuffer = (GLBINDBUFFERPROC)resolveFunctionPointer(p_glGetProcAddress, "glBindBuffer");
	p_glBufferData = (GLBUFFERDATAPROC)resolveFunctionPointer(p_glGetProcAddress, "glBufferData");
	p_glDeleteBuffers = (GLDELETEBUFFERSPROC)resolveFunctionPointer(p_glGetProcAddress, "glDeleteBuffers");
}

// shaders
static const char *vertex_shader_source =
"#version 100 \n\
precision mediump float;\n\
precision mediump int;\n\
attribute vec2 position; \n\
attribute vec3 color; \n\
varying vec3 frag_color; \n\
void main() \n\
{ \n\
	frag_color = color; \n\
	gl_Position = vec4(position * vec2(2) - vec2(1), 0.0, 1.0); \n\
}";
static const char *fragment_shader_source =
"#version 100 \n\
precision mediump float;\n\
precision mediump int;\n\
varying vec3 frag_color; \n\
void main(void) \n\
{ \n\
	gl_FragColor = vec4(frag_color, 1.0); \n\
}";


// OpenGL IDs (yeah, this is a bad hack... do we care?)
static GLuint program;
static struct {
	GLint position;
	GLint color;
} attributes;

// some local hlper functions
static void show_info_log(GLuint object)
{
	GLint log_length;
	p_glGetShaderiv(object, GL_INFO_LOG_LENGTH, &log_length);
	char *log = new char[log_length];
	p_glGetShaderInfoLog(object, log_length, NULL, log);
	fprintf(stderr, "%s", log);
	delete[] log;
}

static GLuint compile_shader(GLenum shader_type, const char *source)
{
	GLuint shader = p_glCreateShader(shader_type);
	p_glShaderSource(shader, 1, &source, NULL);
	p_glCompileShader(shader);
	GLint shader_ok;
	p_glGetShaderiv(shader, GL_COMPILE_STATUS, &shader_ok);
	if (!shader_ok) {
		fprintf(stderr, "Failed to compile shader\n");
		show_info_log(shader);
		exit(1);
	}
	checkGlError("Compiling shader");
	return shader;
}

static GLuint createArrayBuffer(GLsizeiptr size, GLfloat *data)
{
	GLuint buffer;
	p_glGenBuffers(1, &buffer);
	p_glBindBuffer(GL_ARRAY_BUFFER, buffer);
	p_glBufferData(GL_ARRAY_BUFFER, size, data, GL_STATIC_DRAW);
	checkGlError("Creating array buffer");
	return buffer;
}

// initialisation
void initialise2dProjection()
{
	GLuint vertex_shader = compile_shader(GL_VERTEX_SHADER, vertex_shader_source);
	GLuint fragment_shader = compile_shader(GL_FRAGMENT_SHADER, fragment_shader_source);
	program = p_glCreateProgram();
	p_glAttachShader(program, vertex_shader);
	p_glAttachShader(program, fragment_shader);
	p_glLinkProgram(program);
	attributes.position = p_glGetAttribLocation(program, "position");
	attributes.color = p_glGetAttribLocation(program, "color");
}

// draw a quad
void drawQuad(GLfloat red, GLfloat green, GLfloat blue, GLfloat x1, GLfloat y1, GLfloat x2, GLfloat y2)
{
	p_glUseProgram(program);
	checkGlError("Enabling the shaders");

#ifdef CON_GL3
	// create a vertex array obect and use it
	GLuint vao;
	p_glGenVertexArrays(1, &vao);
	p_glBindVertexArray(vao);
	checkGlError("Creating a VAO");
#endif
	
	// send vertex data to card with given attribute
	GLfloat vertex_buffer_data[] = {
		x1, y1,
		x2, y1,
		x2, y2,
		x1, y2,
	};
	GLuint vertex_buffer = createArrayBuffer(sizeof(vertex_buffer_data), vertex_buffer_data);
	p_glVertexAttribPointer(
		attributes.position,              /* attribute */
		2,                                /* size */
		GL_FLOAT,                         /* type */
		GL_FALSE,                         /* normalized? */
		0,                                /* stride */
		(void*)0                          /* array buffer offset */
	);
	checkGlError("Preparing vertex data");
	p_glEnableVertexAttribArray(attributes.position);
	checkGlError("Sending vertex data");
	
	// same with color data
	GLfloat color_buffer_data[] = {
		red, green, blue,
		red, green, blue,
		red, green, blue,
		red, green, blue,
	};
	GLuint color_buffer = createArrayBuffer(sizeof(color_buffer_data), color_buffer_data);
	p_glVertexAttribPointer(
		attributes.color,              /* attribute */
		3,                             /* size */
		GL_FLOAT,                      /* type */
		GL_FALSE,                      /* normalized? */
		0,                             /* stride */
		(void*)0                       /* array buffer offset */
	);
	checkGlError("Preparing color data");
	p_glEnableVertexAttribArray(attributes.color);
	checkGlError("Sending color data");
	
	// draw
	glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
	checkGlError("Drawing");
	
	// cleanup
	p_glDeleteBuffers(1, &vertex_buffer);
	p_glDeleteBuffers(1, &color_buffer);
#ifdef CON_GL3
	p_glDeleteVertexArrays(1, &vao);
#endif
	checkGlError("Doing cleanup");
}
