// This source code is property of the Computer Graphics and Visualization
// chair of the TU Dresden. Do not distribute!
// Copyright (C) CGV TU Dresden - All Rights Reserved

#include "Viewer.h"

#include <nanogui/window.h>
#include <nanogui/layout.h>
#include <nanogui/checkbox.h>

#include <gui/SliderHelper.h>

#include <iostream>
#include <fstream>

#include "glsl.h"

using namespace nse::gui;

Viewer::Viewer()
	: AbstractViewer("CG1 Exercise 1")
{
	SetupGUI();

	CreateShaders();
	CreateVertexBuffers();

	view_uniform_id = glGetUniformLocation(program_id, "view");
	proj_uniform_id = glGetUniformLocation(program_id, "proj");

	julia_m_id = glGetUniformLocation(program_id, "m");
	julia_c_id = glGetUniformLocation(program_id, "c");

	julia_pos_id = glGetUniformLocation(program_id, "julia_positions");

	modelViewMatrix.setIdentity();
	projectionMatrix.setIdentity();

	camera().FocusOnBBox(nse::math::BoundingBox<float, 3>(Eigen::Vector3f(-1, -1, -1), Eigen::Vector3f(1, 1, 1)));
}

void Viewer::SetupGUI()
{
	auto mainWindow = SetupMainWindow();

	//Create GUI elements for the various options
	chkHasDepthTesting = new nanogui::CheckBox(mainWindow, "Perform Depth Testing");
	chkHasDepthTesting->setChecked(true);

	chkHasFaceCulling = new nanogui::CheckBox(mainWindow, "Perform backface Culling");
	chkHasFaceCulling->setChecked(true);

	sldJuliaCX = nse::gui::AddLabeledSliderWithDefaultDisplay(mainWindow, "JuliaC.X", std::make_pair(-1.0f, 1.0f), 0.45f, 2);
	sldJuliaCY = nse::gui::AddLabeledSliderWithDefaultDisplay(mainWindow, "JuliaC.Y", std::make_pair(-1.0f, 1.0f), -0.3f, 2);
	sldJuliaZoom = nse::gui::AddLabeledSliderWithDefaultDisplay(mainWindow, "Julia Zoom", std::make_pair(0.01f, 10.0f), 1.0f, 2);

	point1X = AddLabeledSliderWithDefaultDisplay(mainWindow, "Point1.X", std::make_pair(-3.0f, 3.0f), -0.8f, 2);
	point1Y = AddLabeledSliderWithDefaultDisplay(mainWindow, "Point1.Y", std::make_pair(-3.0f, 3.0f), 0.6f, 2);
	point2X = AddLabeledSliderWithDefaultDisplay(mainWindow, "Point2.X", std::make_pair(-3.0f, 3.0f), 0.5f, 2);
	point2Y = AddLabeledSliderWithDefaultDisplay(mainWindow, "Point2.Y", std::make_pair(-3.0f, 3.0f), -1.0f, 2);
	point3X = AddLabeledSliderWithDefaultDisplay(mainWindow, "Point3.X", std::make_pair(-3.0f, 3.0f), 0.8f, 2);
	point3Y = AddLabeledSliderWithDefaultDisplay(mainWindow, "Point3.Y", std::make_pair(-3.0f, 3.0f), 1.0f, 2);

	performLayout();
}

// Create and define the vertex array and add a number of vertex buffers
void Viewer::CreateVertexBuffers()
{
	/*** Begin of task 1.2.3 ***
	Fill the positions-array and your color array with 12 rows, each
	containing 4 entries, to define a tetrahedron. */

	// Define 3 vertices for one face
	GLfloat positions[] = {
		-1, -1, -1, 1, // 1
		 1, -1,  1, 1, // 6
		-1,  1,  1, 1, // 8

		-1, -1, -1, 1, // 1
		 1,  1, -1, 1, // 3
		 1, -1,  1, 1, // 6

		-1, -1, -1, 1, // 1
		-1,  1,  1, 1, // 8
		 1,  1, -1, 1, // 3

		 1,  1, -1, 1, // 3
		-1,  1,  1, 1, // 8
		 1, -1,  1, 1, // 6

	};

	GLfloat colors[] = {
		1, 0, 0, 1,
		1, 0, 0, 1,
		1, 0, 0, 1,

		0, 1, 0, 1,
		0, 1, 0, 1,
		0, 1, 0, 1,

		0, 0, 1, 1,
		0, 0, 1, 1,
		0, 0, 1, 1,

		1, 1, 0, 1,
		1, 1, 0, 1,
		1, 1, 0, 1,
	};

	// Generate the vertex array
	glGenVertexArrays(1, &vertex_array_id);
	glBindVertexArray(vertex_array_id);

	// Generate a position buffer to be appended to the vertex array
	glGenBuffers(1, &position_buffer_id);
	// Bind the buffer for subsequent settings
	glBindBuffer(GL_ARRAY_BUFFER, position_buffer_id);
	// Supply the position data
	glBufferData(GL_ARRAY_BUFFER, sizeof(positions), positions, GL_STATIC_DRAW);
	// The buffer shall now be linked to the shader attribute
	// "in_position". First, get the location of this attribute in
	// the shader program
	GLuint vid = glGetAttribLocation(program_id, "in_position");

	// Enable this vertex attribute array
	glEnableVertexAttribArray(vid);
	// Set the format of the data to match the type of "in_position"
	glVertexAttribPointer(vid, 4, GL_FLOAT, GL_FALSE, 0, 0);

	/*** Begin of task 1.2.2 (a) ***
	Create another buffer that will store color information. This works nearly
	similar to the code above that creates the position buffer. Store the buffer
	id into the variable "color_buffer_id" and bind the color buffer to the
	shader variable "in_color".


	/*** End of task 1.2.2 (a) ***/

	glGenBuffers(1, &color_buffer_id);
	glBindBuffer(GL_ARRAY_BUFFER, color_buffer_id);
	glBufferData(GL_ARRAY_BUFFER, sizeof(colors), colors, GL_STATIC_DRAW);

	GLuint cid = glGetAttribLocation(program_id, "in_color");
	glEnableVertexAttribArray(cid);
	glVertexAttribPointer(cid, 4, GL_FLOAT, GL_FALSE, 0, 0);

	// Unbind the vertex array to leave OpenGL in a clean state
	glBindVertexArray(0);
}

inline std::string loadShaderText(const char *path)
{
	std::ifstream is(path);
	std::string s_save;

	if (is.is_open())
	{
		s_save.assign(std::istreambuf_iterator<char>(is), std::istreambuf_iterator<char>());
	}
	else
	{
		std::cout << "could not open " << path << std::endl;
	}

	is.close();
	return s_save;
}

inline GLuint loadShader(const char *path, GLenum shadertype)
{
	std::string s_source = loadShaderText(path);

	if (s_source.empty())
		return 0;

	const char *c_source = s_source.c_str();

	GLuint shader = glCreateShader(shadertype);
	glShaderSource(shader, 1, (const char **)&c_source, NULL);
	glCompileShader(shader);

	GLint shaderCompiled = GL_FALSE;
	glGetShaderiv(shader, GL_COMPILE_STATUS, &shaderCompiled);
	if (shaderCompiled != GL_TRUE)
	{
		GLint log_length = 0;
		glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &log_length);

		char *log = new char[log_length];

		glGetShaderInfoLog(shader, (GLsizei)log_length, NULL, log);

		std::cout << "error compiling shader( " << path << " ): " << std::endl
				  << log << std::endl;

		delete[] log;

		glDeleteShader(shader);
		shader = 0;
	}

	return shader;
}

// Read, Compile and link the shader codes to a shader program
void Viewer::CreateShaders()
{
	const char *vs = "exercise1/glsl/shader.vert";
	const char *fs = "exercise1/glsl/shader.frag";

	vertex_shader_id = loadShader(vs, GL_VERTEX_SHADER);
	fragment_shader_id = loadShader(fs, GL_FRAGMENT_SHADER);

	if ((vertex_shader_id == 0) || (fragment_shader_id == 0))
	{
		std::cout << "error creating shader" << std::endl;
		abort();
	}

	program_id = glCreateProgram();
	glAttachShader(program_id, vertex_shader_id);
	glAttachShader(program_id, fragment_shader_id);
	glLinkProgram(program_id);

	/*** Begin of task 1.2.1 ***
	Use the appropriate OpenGL commands to create a shader object for
	the vertex shader, set the source code and let it compile. Store the
	ID of this shader object in the variable "vertex_shader_id". Repeat
	for the fragment shader. Store the ID in the variable "fragment_shader_id.
	Finally, create a shader program with its handle stored in "program_id",
	attach both shader objects and link them. For error checking, you can
	use the method "CheckShaderCompileStatus()" after the call to glCompileShader().
	*/
	/*** End of task 1.2.1 ***/
}

void Viewer::drawContents()
{
	Eigen::Vector2f juliaC(sldJuliaCX->value(), sldJuliaCY->value());
	float juliaZoom = sldJuliaZoom->value();

	//Get the transform matrices
	camera().ComputeCameraMatrices(modelViewMatrix, projectionMatrix);

	// If has_faceculling is set then enable backface culling
	// and disable it otherwise
	if (chkHasFaceCulling->checked())
		glEnable(GL_CULL_FACE);
	else
		glDisable(GL_CULL_FACE);

	// If has_depthtesting is set then enable depth testing
	// and disable it otherwise
	if (chkHasDepthTesting->checked())
		glEnable(GL_DEPTH_TEST);
	else
		glDisable(GL_DEPTH_TEST);

	// create uniform buffer for julia_positions
	float julia_positions[6] = {
		point1X->value(), point1Y->value(), 
		point2X->value(), point2Y->value(),
		point3X->value(), point3Y->value()
	};

	// Activate the shader program
	glUseProgram(program_id);

	/*** Begin of task 1.2.4 (b) ***
	Set the shader variables for the modelview and projection matrix.
	First, find the location of these variables using glGetUniformLocation and
	then set them with the command glUniformMatrix4fv.
	*/

	glUniformMatrix4fv(view_uniform_id, 1, false, modelViewMatrix.data());
	glUniformMatrix4fv(proj_uniform_id, 1, false, projectionMatrix.data());

	glUniform1f(julia_m_id, juliaZoom);
	glUniform2fv(julia_c_id, 1, juliaC.data());

	glUniform2fv(julia_pos_id, 6, julia_positions);

	// Bind the vertex array
	glBindVertexArray(vertex_array_id);
	// Draw the bound vertex array. Start at element 0 and draw 3 vertices
	glDrawArrays(GL_TRIANGLES, 0, 12);

	/*** End of task 1.2.4 (b) ***/

	// Unbind the vertex array
	glBindVertexArray(0);
	// Deactivate the shader program
	glUseProgram(0);
}