/*
 * particle engine on the GPU
 * from Nopper ogl 3.2 tutorial 08
*/

#include "demo_gpu_particles.h"
#include <ctime>


#include <glm/gtx/transform.hpp>
// glm::perspective
#include <glm/gtc/matrix_projection.hpp>
// glm::translate, glm::rotate, glm::scale
#include <glm/gtc/matrix_transform.hpp>
// glm::value_ptr
#include <glm/gtc/type_ptr.hpp> 

demo_gpu_particles::demo_gpu_particles() :
	particle_lookup_width(16),
	max_particles(1000)
{
	points = std::vector<GLfloat>(particle_lookup_width * particle_lookup_width * 2);
	particles = std::vector<GLfloat>(particle_lookup_width * particle_lookup_width * 4);
	flip=0;
	title = "GPU-based particle engine (based on Nopper Tutorial 08)";
}

demo_gpu_particles::~demo_gpu_particles()
{
}

bool demo_gpu_particles::init()
{
	srand(time(0));

	if (!init_window())
	{
		return false;
	}

	if (!loadGLTextures())
	{
		std::cout << "failed to load textures!\n";
		return false;
	}

		//cam
	cam_eye = glm::vec3(0.0f, 0.0f, 10.f);
	cam_center = glm::vec3(0.0f, 0.0f, 0.0f);
	cam_up = glm::vec3(0.0f, 1.0f, 0.0f);

	//do opengl initialization here
	glClearColor(0.0f, 0.0f, 0.0f, 0.5f);
	glEnable(GL_DEPTH_TEST);
	glClearDepth(1.0f);
	glDepthFunc(GL_LEQUAL);
	glDisable(GL_DEPTH_TEST);
	glEnable(GL_BLEND);
	glBlendFunc(GL_ONE, GL_ONE);

	render = std::auto_ptr<Shader>( new Shader ("../../shader/render") );
	if (!(*render)) {
		cerr << "Could not compile minimal shader program." << endl;
		return 1;
	}

	render->bind_frag_data_location("out_color");
	
	update = std::auto_ptr<Shader>( new Shader ("../../shader/update") );
	if (!(*update)) {
		cerr << "Could not compile sandbox shader program." << endl;
		return 1;
	}

	update->bind_frag_data_location("frag_particle");

	//set handles for render shader
	texture_location = render->get_uniform_location("firstTexture");
	lookup_texture_location = render->get_uniform_location("readTexture");
	vertex_location = render->get_attrib_location("vertex");

	//set handles for update shader
	lookup_texture_update_location = update->get_uniform_location("readTexture");
	size_location = update->get_uniform_location("size");
	time_location = update->get_uniform_location("time");
	vertex_update_location = update->get_attrib_location("vertex");

	init_particles();

	glGenVertexArrays(1, &vao_id);
	
	glBindVertexArray(vao_id);

	//texel indices vbo
	glGenBuffers(1, &vertex_vbo_id);
	glBindBuffer(GL_ARRAY_BUFFER, vertex_vbo_id);
	glBufferData(GL_ARRAY_BUFFER, points.size() * sizeof(GLfloat), &points[0], GL_STATIC_DRAW);

	//setup lookup texture
	glGenTextures(2, texture_lookup);
	glBindTexture(GL_TEXTURE_2D, texture_lookup[0]);
	// Generate The Texture
	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, particle_lookup_width, particle_lookup_width, 0, GL_RGBA, GL_FLOAT, &particles[0]);
	glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_NEAREST);	// needs GL_NEAREST for particle storage to work properly!
	glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_NEAREST);	// needs GL_NEAREST for particle storage to work properly!
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); 
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); 

	
	glBindTexture(GL_TEXTURE_2D, texture_lookup[1]);
	// Generate The Texture
	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, particle_lookup_width, particle_lookup_width, 0, GL_RGBA, GL_FLOAT, &particles[0]);
	glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_NEAREST);	// needs GL_NEAREST for particle storage to work properly!
	glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_NEAREST);	// needs GL_NEAREST for particle storage to work properly!
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); 
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); 

	//generate framebuffers for the textures
	glGenFramebuffers(2, framebuffer_lookup);

	glBindFramebuffer(GL_FRAMEBUFFER, framebuffer_lookup[0]);
	glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture_lookup[0], 0);

	glBindFramebuffer(GL_FRAMEBUFFER, framebuffer_lookup[1]);
	glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture_lookup[1], 0);

	render->bind();

	glBindBuffer(GL_ARRAY_BUFFER, vertex_vbo_id);
	glVertexAttribPointer(vertex_location, 2, GL_FLOAT, GL_FALSE, 0, 0);
	glEnableVertexAttribArray(vertex_location);

	glUniform1i(texture_location, 0);

	glActiveTexture(GL_TEXTURE0);
	glBindTexture(GL_TEXTURE_2D, texture_particle);

	update->bind();

	glUniform1f(size_location, static_cast<GLfloat>(particle_lookup_width));
	
	glBindBuffer(GL_ARRAY_BUFFER, vertex_vbo_id);
	glVertexAttribPointer(vertex_update_location, 2, GL_FLOAT, GL_FALSE, 0, 0);
	glEnableVertexAttribArray(vertex_update_location);

	//glBindFramebuffer(GL_FRAMEBUFFER, 0);

	return true;
}

void demo_gpu_particles::draw()
{
	load_identity(modelview_matrix);

	look_at(cam_eye, cam_center, cam_up);

	glEnable(GL_VERTEX_PROGRAM_POINT_SIZE);


	//update particles;
	update->bind();

	//glBindVertexArray(vao_id);
	glBindFramebuffer(GL_FRAMEBUFFER, framebuffer_lookup[1 - flip]);

	
	glBindVertexArray(vao_id);

	//set viewport for framebuffer
	glViewport(0, 0, particle_lookup_width, particle_lookup_width);

	////need some sort of timing here
	glUniform1f(time_location, delta()*5);

	glUniform1i(lookup_texture_update_location, 1);
	//
	glActiveTexture(GL_TEXTURE1);
	glBindTexture(GL_TEXTURE_2D, texture_lookup[flip]);

	glDisable(GL_BLEND);

	glDrawArrays(GL_POINTS, 0, particle_lookup_width * particle_lookup_width);

	glBindFramebuffer(GL_FRAMEBUFFER, 0);
	
	//render particles
	glViewport(0,0,width,height);
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

	render->bind();

	
	glBindVertexArray(vao_id);

	glActiveTexture(GL_TEXTURE1);
	
	glBindTexture(GL_TEXTURE_2D, texture_lookup[flip]);

	glUniform1i(lookup_texture_location, 1);

	glEnable(GL_BLEND);

	glBlendFunc(GL_SRC_ALPHA, GL_ONE);

	// Set our matrix uniform
	GLint modelview_location = render->get_uniform_location("modelview_matrix");
	glUniformMatrix4fv( modelview_location, 1, GL_FALSE, glm::value_ptr(modelview_matrix));

	// Set our matrix uniform
	GLint perspective_location = render->get_uniform_location("perspective_matrix");
	glUniformMatrix4fv( perspective_location, 1, GL_FALSE, glm::value_ptr(perspective_matrix));

	glDrawArrays(GL_POINTS, 0, particle_lookup_width * particle_lookup_width);

	glDisable(GL_VERTEX_PROGRAM_POINT_SIZE);

	//flip framebuffers
	flip = 1 - flip;
}

void demo_gpu_particles::cleanup()
{
	glDeleteVertexArrays(1, &vao_id);

	glDeleteBuffers(1, &vertex_vbo_id);

	vao_id = 0;
	vertex_vbo_id = 0;
}


void demo_gpu_particles::init_particles()
{
	//create points for referencing texels
	for (int y=0; y < particle_lookup_width; y++ ) 
	{
		for (int x=0; x < particle_lookup_width; x++ ) 
		{
			points[x*2+0 + y*particle_lookup_width*2] = static_cast<GLfloat>(x) / static_cast<GLfloat>(particle_lookup_width);
			points[x*2+1 + y*particle_lookup_width*2] = static_cast<GLfloat>(y) / static_cast<GLfloat>(particle_lookup_width);
			
		}
	}

	//set initial values for each particle
	for (int y=0; y < particle_lookup_width; y++ ) 
	{
		for (int x=0; x < particle_lookup_width; x++ ) 
		{
			particles[x*4 +0 + y*particle_lookup_width*4] = 0.0f+x;
			particles[x*4 +1 + y*particle_lookup_width*4] = 0.0f+y;
			particles[x*4 +2 + y*particle_lookup_width*4] = 0.0f;
			particles[x*4 +3 + y*particle_lookup_width*4] = (rand()%101)/100.0f;
		}
	}




}

bool demo_gpu_particles::loadGLTextures() {
	bool status = false;
	tga::Texture tex;

	if (tga::LoadTGA(&tex, "../../textures/particle.tga"))
	{
		status = true;
		glGenTextures(1, &texture_particle);					// Create The Texture

		// Typical Texture Generation Using Data From The Bitmap
		glBindTexture(GL_TEXTURE_2D, texture_particle);
		// Generate The Texture
		glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, tex.width, tex.height, 0, tex.type, GL_UNSIGNED_BYTE, tex.imageData);
		glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);	// Linear Filtering
		glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);	// Linear Filtering
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); 
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); 
		if (tex.type == GL_RGB)
		{
			std::cout << "RGB\n";
		}
		else if (tex.type == GL_RGBA)
		{
			std::cout << "RGBA\n";
		}
		else
		{
			std::cout << "smth else\n";
		}
	}

	if (tex.imageData)					// If Texture Image Exists
	{
		free(tex.imageData);				// Free The Texture Image Memory
	}

	return status;
}

void demo_gpu_particles::process_keys()
{

	//factor determines the speed of the camera movement
	float factor = static_cast<float>(delta()) * 5;

	glm::vec3 view_dir = glm::normalize(cam_eye - cam_center);
	glm::vec3 side_dir = glm::cross(view_dir, glm::vec3(0.0f, 1.0f, 0.0f));
	if (glfwGetKey('D'))
	{
		cam_eye -= factor*side_dir;
		cam_center -= factor*side_dir;
	}	
	if (glfwGetKey('A'))
	{
		cam_eye += factor*side_dir;
		cam_center += factor*side_dir;
	}
	if (glfwGetKey(GLFW_KEY_SPACE))
	{
		cam_eye[1] += factor;
		cam_center[1] += factor;
	}	
	if (glfwGetKey(GLFW_KEY_LSHIFT))
	{
		cam_eye[1] -= factor;
		cam_center[1] -= factor;
	}
	
	if (glfwGetKey('S'))
	{
		cam_eye += factor*view_dir;
		cam_center += factor*view_dir;
	}	
	if (glfwGetKey('W'))
	{
		cam_eye -= factor*view_dir;
		cam_center -= factor*view_dir;
	}

	int newx, newy;
	glfwGetMousePos(&newx, &newy);
	if (glfwGetMouseButton(GLFW_MOUSE_BUTTON_2))
	{
	if (newx != mousex)
	{
		glm::vec4 temp = 
			glm::translate(cam_eye) *
			glm::rotate(static_cast<GLfloat>((mousex - newx)*0.2), 0.0f, 1.0f, 0.0f) * 
			glm::translate(-cam_eye) *
			glm::vec4(cam_center, 1.0f);
		cam_center = glm::vec3(temp.x, temp.y, temp.z);
	}
	}
	mousex = newx;

}