#include <tgmath.h>

#include "meshviewerwidget.h"


const GLchar *vertex_shader_source = R"glsl(
#version 150 core

in vec3 pos;

uniform mat4 proj;
uniform mat4 view;
uniform mat4 model;

void main() {
	gl_Position = proj * view * model * vec4(pos, 1.0);
}
)glsl";

const GLchar *fragment_shader_source = R"glsl(
#version 150 core

out vec4 out_color;

uniform vec3 col;
uniform float alpha;

void main() {
	out_color = vec4(col, alpha);
}
)glsl";


MeshViewerWidget::MeshViewerWidget(QWidget *parent) : QOpenGLWidget(parent) {
	setMouseTracking(true);
	setFocus();
}


void GLAPIENTRY
opengl_debug_cb(GLenum source,
				GLenum type,
				GLuint id,
				GLenum severity,
				GLsizei length,
				const GLchar* message,
				const void* userParam) {
	(void) source;
	(void) type;
	(void) id;
	(void) severity;
	(void) length;
	(void) userParam;
	qDebug() << "OpenGL debug output:" << message;
}


void MeshViewerWidget::initializeGL() {
	initializeOpenGLFunctions();

	GLint major, minor;
	glGetIntegerv(GL_MAJOR_VERSION, &major);
	glGetIntegerv(GL_MINOR_VERSION, &minor);
	qDebug("OpenGL version %d.%d", major, minor);

	glEnable(GL_DEBUG_OUTPUT);
	glDebugMessageCallback(opengl_debug_cb, 0);

	/* Compile the vertex shader. */
	GLuint vertex_shader = glCreateShader(GL_VERTEX_SHADER);
	glShaderSource(vertex_shader, 1, &vertex_shader_source, NULL);
	glCompileShader(vertex_shader);
	GLint status;
	glGetShaderiv(vertex_shader, GL_COMPILE_STATUS, &status);
	if (status != GL_TRUE) {
		char log[1024];
		glGetShaderInfoLog(vertex_shader, sizeof log, NULL, log);
		fprintf(stderr, "Failed to compile the vertex shader: %s\n", log);
		exit(1);
	}

	/* Compile the fragment shader. */
	GLuint fragment_shader = glCreateShader(GL_FRAGMENT_SHADER);
	glShaderSource(fragment_shader, 1, &fragment_shader_source, NULL);
	glCompileShader(fragment_shader);
	glGetShaderiv(fragment_shader, GL_COMPILE_STATUS, &status);
	if (status != GL_TRUE) {
		char log[1024];
		glGetShaderInfoLog(fragment_shader, sizeof log, NULL, log);
		fprintf(stderr, "Failed to compile the fragment shader: %s\n", log);
		exit(1);
	}

	/* Link the shader program. */
	GLuint shader_program = glCreateProgram();
	glAttachShader(shader_program, vertex_shader);
	glAttachShader(shader_program, fragment_shader);
	glBindFragDataLocation(shader_program, 0, "out_color");
	glLinkProgram(shader_program);
	glGetProgramiv(shader_program, GL_LINK_STATUS, &status);
	if (status != GL_TRUE) {
		char log[1024];
		glGetProgramInfoLog(shader_program, sizeof log, NULL, log);
		fprintf(stderr, "Failed to link the shader program: %s\n", log);
		exit(1);
	}

	/* Use it. */
	glUseProgram(shader_program);

	/* Get the position attribute. */
	pos_attr = glGetAttribLocation(shader_program, "pos");
	proj_attr = glGetUniformLocation(shader_program, "proj");
	view_attr = glGetUniformLocation(shader_program, "view");
	model_attr = glGetUniformLocation(shader_program, "model");
	col_attr = glGetUniformLocation(shader_program, "col");
	alpha_attr = glGetUniformLocation(shader_program, "alpha");

	glUniform1f(alpha_attr, 1);

	glClearColor(1, 1, 1, 0);

	glEnable(GL_DEPTH_TEST);
	glEnable(GL_MULTISAMPLE);

	emit initialized();
}


void MeshViewerWidget::resizeGL(int w, int h) {
	QMatrix4x4 projection;
	projection.perspective(FOV, (float) w/h, .01, 100);
	glUniformMatrix4fv(proj_attr, 1, GL_FALSE, projection.data());
}


void MeshViewerWidget::paintGL() {
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
	QMatrix4x4 trans;
	trans.translate(0, 0, -cam_dist);
	QMatrix4x4 view = trans * rot;
	glUniformMatrix4fv(view_attr, 1, GL_FALSE, view.data());
	for (const MeshView mv : mesh_views) {
		glUniformMatrix4fv(model_attr, 1, GL_FALSE, mv.mat.data());

		/* Mesh */
		glUniform3f(col_attr, mv.col.redF(), mv.col.greenF(), mv.col.blueF());
		glBindVertexArray(mv.vao);
		glEnable(GL_POLYGON_OFFSET_FILL);
		glPolygonOffset(1.0, 2);
		glDrawArrays(GL_TRIANGLES, 0, mv.nverts);
		glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
		glDisable(GL_POLYGON_OFFSET_FILL);

		/* Wireframe */
		glUniform3f(col_attr, WIREFRAME_COLOR);
		glDrawArrays(GL_TRIANGLES, 0, mv.nverts);
		glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
		glLineWidth(1);
	}
}


void MeshViewerWidget::add(size_t nverts, const GLfloat *verts, QMatrix4x4 mat, QColor col) {
	makeCurrent();

	GLuint vao, vbo;
    glGenVertexArrays(1, &vao);
	glGenBuffers(1, &vbo);

    glBindVertexArray(vao);
	glBindBuffer(GL_ARRAY_BUFFER, vbo);
	glBufferData(GL_ARRAY_BUFFER, nverts * 3 * sizeof (GLfloat), verts, GL_DYNAMIC_DRAW);
	glVertexAttribPointer(pos_attr, 3, GL_FLOAT, GL_FALSE, 0, 0);
	glEnableVertexAttribArray(pos_attr);

	glBindVertexArray(0);

	doneCurrent();
	update();
}


void MeshViewerWidget::addFromMesh(MyMesh* mesh, QMatrix4x4 mat, QColor col) {
    GLfloat *verts = new GLfloat[mesh->n_faces() * 3 * 3];
	size_t i = 0;

	for (MyMesh::FaceHandle face : mesh->faces()) {
		for (MyMesh::VertexHandle vec : mesh->fv_range(face)) {
			verts[3*i+0] = mesh->point(vec)[0];
			verts[3*i+1] = mesh->point(vec)[1];
			verts[3*i+2] = mesh->point(vec)[2];
			i++;
		}
    }

    add(i, verts, mat, col);
    delete[] verts;
	return ret;
}


void MeshViewerWidget::mousePressEvent(QMouseEvent *e) {
	if (e->button() == Qt::LeftButton) {
		mouse_pos = e->pos();
	}
}


void MeshViewerWidget::mouseReleaseEvent(QMouseEvent *e) {
	(void) e;
	rot_start = rot;
}


void MeshViewerWidget::mouseMoveEvent(QMouseEvent *e) {
	if (e->buttons() & Qt::LeftButton) {
		QPoint delta = e->pos() - mouse_pos;
		rot = rot_start;
		rot.rotate(delta.x() / 5., 0, 1, 0);
		rot.rotate(delta.y() / 5., QVector3D(1, 0, 0) * rot);
		update();
	}
}


void MeshViewerWidget::wheelEvent(QWheelEvent *e) {
	cam_dist -= e->angleDelta().y() / 1000. * cam_dist;
	update();
}
