#include "opengl_widget.hh"

#include "load_obj.hh"

#include <QMouseEvent>
#include <QOpenGLBuffer>


static 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;
	// Those are a bit too verbose
	if (!QString((const char *) message).startsWith("Shader Stats:")) {
		qDebug() << "OpenGL debug output:" << message;
	}
}


OpenGLWidget *OpenGLWidget::instance = nullptr;


OpenGLWidget::OpenGLWidget(QWidget *parent)
	:QOpenGLWidget(parent) {
	OpenGLWidget::instance = this;
}


OpenGLWidget::~OpenGLWidget() {
	OpenGLWidget::instance = nullptr;
}


void OpenGLWidget::loadSkybox() {
	// Shader program
	if (!skybox_program.addShaderFromSourceFile(QOpenGLShader::Vertex, ":/shaders/skybox.vert")) {
		qFatal("Error compiling skybox.vert: %s", skybox_program.log().toLocal8Bit().constData());
	}
	if (!skybox_program.addShaderFromSourceFile(QOpenGLShader::Fragment, ":/shaders/skybox.frag")) {
		qFatal("Error compiling skybox.frag: %s", skybox_program.log().toLocal8Bit().constData());
	}
	if (!skybox_program.link()) {
		qFatal("Error linking the skybox shader program: %s", skybox_program.log().toLocal8Bit().constData());
	}
	skybox_program.bind();
	skybox_program.setUniformValue("skybox", 0);

	QVector<GLfloat> skybox_verts {
		-1.0,  1.0, -1.0, -1.0, -1.0, -1.0, 1.0, -1.0, -1.0, 1.0, -1.0, -1.0, 1.0,  1.0, -1.0, -1.0,  1.0, -1.0,
        -1.0, -1.0,  1.0, -1.0, -1.0, -1.0, -1.0,  1.0, -1.0, -1.0,  1.0, -1.0, -1.0,  1.0,  1.0, -1.0, -1.0,  1.0,
		1.0, -1.0, -1.0, 1.0, -1.0,  1.0, 1.0,  1.0,  1.0, 1.0,  1.0,  1.0, 1.0,  1.0, -1.0, 1.0, -1.0, -1.0,
        -1.0, -1.0,  1.0, -1.0,  1.0,  1.0, 1.0,  1.0,  1.0, 1.0,  1.0,  1.0, 1.0, -1.0,  1.0, -1.0, -1.0,  1.0,
        -1.0,  1.0, -1.0, 1.0,  1.0, -1.0, 1.0,  1.0,  1.0, 1.0,  1.0,  1.0, -1.0,  1.0,  1.0, -1.0,  1.0, -1.0,
        -1.0, -1.0, -1.0, -1.0, -1.0,  1.0, 1.0, -1.0, -1.0, 1.0, -1.0, -1.0, -1.0, -1.0,  1.0, 1.0, -1.0,  1.0
	};
	// QVector<GLfloat> skybox_verts = load_obj(":/mdl/cube.obj", 0);

	// VAO
	glGenVertexArrays(1, &skybox_vao);
	glBindVertexArray(skybox_vao);
	glGenBuffers(1, &skybox_vbo);
	glBindBuffer(GL_ARRAY_BUFFER, skybox_vbo);
	glBufferData(GL_ARRAY_BUFFER, skybox_verts.size() * sizeof (GLfloat), skybox_verts.data(), GL_STATIC_DRAW);
	glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);
	glEnableVertexAttribArray(0);
	glBindVertexArray(0);

	// Skybox texture images
	QVector<QImage> skybox_img {
		QImage(":/img/clouds1_west.jpg").convertToFormat(QImage::Format_RGB888),
		QImage(":/img/clouds1_east.jpg").convertToFormat(QImage::Format_RGB888),
		QImage(":/img/clouds1_up.jpg").convertToFormat(QImage::Format_RGB888),
		QImage(":/img/clouds1_down.jpg").convertToFormat(QImage::Format_RGB888),
		QImage(":/img/clouds1_south.jpg").convertToFormat(QImage::Format_RGB888),
		QImage(":/img/clouds1_north.jpg").convertToFormat(QImage::Format_RGB888)
	};
	size_t width = skybox_img[0].width();
	size_t height = skybox_img[0].height();
	glGenTextures(1, &skybox_tex);
    glBindTexture(GL_TEXTURE_CUBE_MAP, skybox_tex);
	for (int i = 0; i < 6; i++) {
		glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE,
					 (const GLvoid *) skybox_img[i].constBits());
	}
	glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
    glBindTexture(GL_TEXTURE_CUBE_MAP, 0);

	skybox_program.release();
}


void OpenGLWidget::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);

	if (!main_program.addShaderFromSourceFile(QOpenGLShader::Vertex, ":/shaders/main.vert")) {
		qFatal("Error compiling main.vert: %s", main_program.log().toLocal8Bit().constData());
	}
	if (!main_program.addShaderFromSourceFile(QOpenGLShader::Fragment, ":/shaders/main.frag")) {
		qFatal("Error compiling main.frag: %s", main_program.log().toLocal8Bit().constData());
	}
	if (!main_program.link()) {
		qFatal("Error linking the main shader program: %s", main_program.log().toLocal8Bit().constData());
	}
	main_program.bind();
	main_program.setUniformValue("tex", 0);
	main_program.release();

	loadSkybox();

	glClearColor(1, 1, 1, 0);
	glEnable(GL_DEPTH_TEST);
	glEnable(GL_MULTISAMPLE);

	emit initialized();
}


void OpenGLWidget::resizeGL(int w, int h) {
	proj.setToIdentity();
	proj.perspective(FOV, (float) w/h, .01, 1000);
}


void OpenGLWidget::paintGL() {
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
	QMatrix4x4 trans;
	trans.translate(0, 0, -cam_dist);
	QMatrix4x4 view = trans * rot;

	glDepthMask(GL_FALSE);
	glDepthFunc(GL_LEQUAL);
	skybox_program.bind();
	skybox_program.setUniformValue("proj", proj);
	skybox_program.setUniformValue("view", view);
	glBindVertexArray(skybox_vao);
	glActiveTexture(GL_TEXTURE0);
	glBindTexture(GL_TEXTURE_CUBE_MAP, skybox_tex);
	glDrawArrays(GL_TRIANGLES, 0, 36);
	glBindTexture(GL_TEXTURE_CUBE_MAP, 0);
	skybox_program.release();
	glDepthMask(GL_TRUE);
	glDepthFunc(GL_LESS);

	main_program.bind();
	main_program.setUniformValue("proj", proj);
	main_program.setUniformValue("view", view);
	glActiveTexture(GL_TEXTURE0);

	for (const OpenGLMesh &mesh : meshes) {
		main_program.setUniformValue("model", mesh.mat);
		glBindVertexArray(mesh.vao);
		mesh.tex->bind();
		glDrawArrays(GL_TRIANGLES, 0, mesh.nverts);
		mesh.tex->release();
	}
	main_program.release();

}


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


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


void OpenGLWidget::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 OpenGLWidget::wheelEvent(QWheelEvent *e) {
	cam_dist -= e->angleDelta().y() / 1000. * cam_dist;
	update();
}
