#include "opengl_widget.hh"

#include "load_obj.hh"

#include <QMouseEvent>
#include <QOpenGLBuffer>
#include <QSurfaceFormat>


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;
	QSurfaceFormat format;
	format.setProfile(QSurfaceFormat::CoreProfile);
	format.setDepthBufferSize(24);
	format.setSamples(4);
	setFormat(format);
}


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());
	}
	skybox_program.bindAttributeLocation("in_pos", 0);
	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::loadGround() {
	QOpenGLTexture *ground_tex = new QOpenGLTexture(QImage(":/img/ground.jpg").mirrored());
	ground_tex->generateMipMaps();
	ground_tex->setMagnificationFilter(QOpenGLTexture::Linear);
	ground_tex->setMinificationFilter(QOpenGLTexture::LinearMipMapLinear);
	ground_tex->setWrapMode(QOpenGLTexture::MirroredRepeat);
	ground = new OpenGLMesh({
			-1000, 0, 1000, 0, 1000, 0, 0, 1000,
			1000, 0, -1000, 0, 1000, 0, 1000, 0,
			-1000, 0, -1000, 0, 1000, 0, 0, 0,
			1000, 0, -1000, 0, 1000, 0, 1000, 0,
			-1000, 0, 1000, 0, 1000, 0, 0, 1000,
			1000, 0, 1000, 0, 1000, 0, 1000, 1000,
		}, ground_tex, &main_program);
}


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());
	}
	main_program.bindAttributeLocation("in_pos", 0);
	main_program.bindAttributeLocation("in_norm", 1);
	main_program.bindAttributeLocation("in_uv", 2);
	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();

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

	loadSkybox();
	loadGround();

	glClearColor(1, 1, 1, 0);
	glCullFace(GL_BACK);

	emit initialized();
}


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


void OpenGLWidget::paintGL() {
	glEnable(GL_CULL_FACE);
	glEnable(GL_DEPTH_TEST);
	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);

	line_program.bind();
	line_program.setUniformValue("proj", proj);
	line_program.setUniformValue("view", view);
	line_program.release();

	main_program.bind();
	main_program.setUniformValue("proj", proj);
	main_program.setUniformValue("view", view);
	main_program.setUniformValue("highlight", false);
	glActiveTexture(GL_TEXTURE0);
	glEnable(GL_POLYGON_OFFSET_FILL);
	glPolygonOffset(1, 1);
	ground->draw(this, QMatrix4x4());
	glDisable(GL_POLYGON_OFFSET_FILL);
	main_program.release();
	if (painter) painter->draw(this);
}


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();
}


void OpenGLWidget::setPainter(const Painter *p) {
	painter = p;
}


QOpenGLShaderProgram *OpenGLWidget::getMainProgram() {
	return &main_program;
}

QOpenGLShaderProgram *OpenGLWidget::getLineProgram() {
	return &line_program;
}


bool OpenGLWidget::project(const QVector3D &p, QPoint &point) const {
	QMatrix4x4 trans;
	trans.translate(0, 0, -cam_dist);
	QMatrix4x4 view = trans * rot;
	QVector3D projected = proj * view * p;
	if (projected.x() < -1 || projected.x() > 1
		|| projected.y() < -1 || projected.y() > 1
		|| projected.z() < -1 || projected.z() > 1) {
		return false;
	}
	point.setX((projected.x() / 2 + .5) * (float) width());
	point.setY(((projected.y() / 2 - .5) * -1) * (float) height());
	return true;
}
