/*
    QGLView
    Ivan Pelipenko peri4ko@yandex.ru

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Lesser General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Lesser General Public License for more details.

    You should have received a copy of the GNU Lesser General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

#include "gltypes.h"
#include "qglview.h"

const char qgl_vertex_head[] =
	//"in vec3 _qgl_Vertex;\n"
	"in vec3 qgl_Normal;\n"
	"in vec3 qgl_Tangent;\n"
	"in vec3 qgl_Bitangent;\n"
	"in vec2 qgl_Texture;\n"
	"in vec4 qgl_Color;\n"
	"out vec2 qgl_FragTexture;\n"
	"out vec4 qgl_FragColor;\n"
	//"vec4 qgl_Vertex = vec4(_qgl_Vertex, 1);\n"
	"in vec3 qgl_Vertex;\n"
	"vec4 qgl_ftransform() {return qgl_ModelViewProjectionMatrix * vec4(qgl_Vertex, 1);}\n";

const char qgl_fragment_head[] = "in vec2 qgl_FragTexture;\n"
								 "in vec4 qgl_FragColor;\n"
								 "out vec4 qgl_FragData[gl_MaxDrawBuffers];\n";

const char qgl_uniform[]       = "uniform mat4 qgl_ModelViewMatrix;\n"
								 "uniform mat4 qgl_ProjectionMatrix;\n"
								 "uniform mat4 qgl_ModelViewProjectionMatrix;\n"
								 "uniform mat3 qgl_NormalMatrix;\n"
								 "uniform mat4 qgl_ModelViewMatrixInverse;\n"
								 "uniform mat4 qgl_ProjectionMatrixInverse;\n"
								 "uniform mat4 qgl_ModelViewProjectionMatrixInverse;\n"
								 "uniform mat4 qgl_ModelViewMatrixTranspose;\n"
								 "uniform mat4 qgl_ProjectionMatrixTranspose;\n"
								 "uniform mat4 qgl_ModelViewProjectionMatrixTranspose;\n"
								 "uniform mat4 qgl_ModelViewMatrixInverseTranspose;\n"
								 "uniform mat4 qgl_ProjectionMatrixInverseTranspose;\n"
								 "uniform mat4 qgl_ModelViewProjectionMatrixInverseTranspose;\n";

const char qgl_structs[]       = "const int qgl_MaxLights = 8;\n"
								 "struct QGLLight {\n"
								 "	vec4 color;\n"
								 "	vec4 position;\n"
								 "	vec4 direction;\n"
								 "	float intensity;\n"
								 "	float startAngle;\n"
								 "	float startAngleCos;\n"
								 "	float endAngle;\n"
								 "	float endAngleCos;\n"
								 "	float constantAttenuation;\n"
								 "	float linearAttenuation;\n"
								 "	float quadraticAttenuation;\n"
								 "	sampler2DShadow shadow;\n"
						   //"	sampler2D shadowColor\n"
						   "	mat4 shadowMatrix;\n"
						   //"	vec4 shadowDir0;\n"
                           //"	vec4 shadowDir1;\n"
						   "};\n"
						   "struct QGLMap {\n"
						   "	float offset;\n"
						   "	float amount;\n"
						   "	vec2 scale;\n"
						   "	sampler2D map;\n"
						   "};\n"
						   "struct QGLMaterial {\n"
						   "	float transparency;\n"
						   "	float reflectivity;\n"
						   "	float iof;\n"
						   "	float dispersion;\n"
						   "	vec4 color_diffuse;\n"
						   "	vec4 color_specular;\n"
						   "	vec4 color_self_illumination;\n"
						   "	QGLMap map_diffuse;\n"
						   "	QGLMap map_normal;\n"
						   "	QGLMap map_relief;\n"
						   "	QGLMap map_self_illumination;\n"
						   "	QGLMap map_specularity;\n"
						   "	QGLMap map_specular;\n"
						   "};\n"
						   "uniform QGLLight qgl_AmbientLight;\n"
						   "uniform QGLLight qgl_Light[qgl_MaxLights];\n"
						   "uniform QGLMaterial qgl_Material;\n";


QString loadShaderFile(QOpenGLShaderProgram * prog, QOpenGLShader::ShaderType type, const QString & file) {
	QFile f(file);
	if (!f.open(QIODevice::ReadOnly)) return "";
	QString all     = QString::fromUtf8(f.readAll());
	int i           = all.indexOf("#version");
	QString version = all.mid(i + 8, all.indexOf("\n", i) - i - 8).trimmed();
	if (version.toInt() >= 150) {
		int ip = all.indexOf("\n", i);
		if (ip < 0) return all;
		if (type == QOpenGLShader::Vertex) {
			all.insert(ip + 1, qgl_vertex_head);
		}
		if (type == QOpenGLShader::Fragment) {
			all.insert(ip + 1, qgl_fragment_head);
		}
		all.insert(ip + 1, qgl_structs);
		all.insert(ip + 1, qgl_uniform);
	}
	prog->addShaderFromSourceCode(type, all);
	// qDebug() << "********" << all;
	return all;
}


bool loadShaders(QOpenGLShaderProgram * prog, const QString & name, const QString & dir) {
	prog->removeAllShaders();
	QDir d(dir);
	QFileInfoList sl;
	// qDebug() << "[QGLView] Shader \"" + name + "\" load shaders from" << d.absolutePath();
	sl = d.entryInfoList(QStringList(name + ".geom"), QDir::Files | QDir::NoDotAndDotDot);
	foreach(const QFileInfo & i, sl) {
		// qDebug() << "[QGLView] Shader \"" + name + "\" add geometry shader:" << i.fileName();
		loadShaderFile(prog, QOpenGLShader::Geometry, i.absoluteFilePath());
	}
	sl = d.entryInfoList(QStringList(name + ".vert"), QDir::Files | QDir::NoDotAndDotDot);
	foreach(const QFileInfo & i, sl) {
		// qDebug() << "[QGLView] Shader \"" + name + "\" add vertex shader:" << i.fileName();
		loadShaderFile(prog, QOpenGLShader::Vertex, i.absoluteFilePath());
	}
	sl = d.entryInfoList(QStringList(name + ".frag"), QDir::Files | QDir::NoDotAndDotDot);
	foreach(const QFileInfo & i, sl) {
		// qDebug() << "[QGLView] Shader \"" + name + "\" add fragment shader:" << i.fileName();
		loadShaderFile(prog, QOpenGLShader::Fragment, i.absoluteFilePath());
	}
	if (!prog->link()) {
		qDebug() << "[QGLView] Shader \"" + name + "\" link error: " + prog->log();
		return false;
	}
	return true;
}


void setUniformMatrices(QOpenGLShaderProgram * prog, QMatrix4x4 proj, QMatrix4x4 view, QMatrix4x4 prevproj, QMatrix4x4 prevview) {
	if (!prog) return;
	if (!prog->isLinked()) return;
	QMatrix4x4 mvpm  = proj * view;
	QMatrix4x4 pmvpm = prevproj * prevview;
	QMatrix3x3 nm    = view.normalMatrix();
	// nm.in;
	prog->setUniformValue("qgl_ModelViewMatrix", view);
	prog->setUniformValue("qgl_ProjectionMatrix", proj);
	prog->setUniformValue("prev_ModelViewProjectioMatrix", pmvpm);
	prog->setUniformValue("prev_ModelViewMatrix", prevview);
	prog->setUniformValue("qgl_ModelViewProjectionMatrix", mvpm);
	prog->setUniformValue("qgl_NormalMatrix", nm);
	// prog->setUniformValue("qgl_BumpMatrix", nm.);
	prog->setUniformValue("qgl_ModelViewMatrixTranspose", view.transposed());
	prog->setUniformValue("qgl_ProjectionMatrixTranspose", proj.transposed());
	prog->setUniformValue("qgl_ModelViewProjectionMatrixTranspose", mvpm.transposed());
}


void setUniformMap(QOpenGLShaderProgram * prog, QString map_name, const Map & map, int channel, int def_channel) {
	if (!prog) return;
	if (!prog->isLinked()) return;
	prog->setUniformValue(("qgl_Material." + map_name + ".offset").toLatin1().constData(), map.color_offset);
	prog->setUniformValue(("qgl_Material." + map_name + ".amount").toLatin1().constData(), map.color_amount);
	prog->setUniformValue(("qgl_Material." + map_name + ".scale").toLatin1().constData(), map.bitmap_scale);
	prog->setUniformValue(("qgl_Material." + map_name + ".map").toLatin1().constData(), map.bitmap_id > 0 ? channel : def_channel);
}


void setUniformMaterial(QOpenGLShaderProgram * prog, const Material & mat) {
	if (!prog) return;
	if (!prog->isLinked()) return;
	QOpenGLFunctions * glFuncs = QOpenGLContext::currentContext()->functions();
	GLfloat mat_diffuse[4]     = {1.0f, 1.0f, 1.0f, 1.0f};
	mat_diffuse[0]             = mat.color_diffuse.redF();
	mat_diffuse[1]             = mat.color_diffuse.greenF();
	mat_diffuse[2]             = mat.color_diffuse.blueF();
	mat_diffuse[3]             = mat.color_diffuse.alphaF() * (1.f - mat.transparency);
	glFuncs->glVertexAttrib4f(prog->attributeLocation("qgl_Color"), mat_diffuse[0], mat_diffuse[1], mat_diffuse[2], mat_diffuse[3]);
	prog->setUniformValue("qgl_Material.transparency", mat.transparency);
	prog->setUniformValue("qgl_Material.reflectivity", mat.reflectivity);
	prog->setUniformValue("qgl_Material.iof", mat.iof);
	prog->setUniformValue("qgl_Material.dispersion", mat.dispersion);
	prog->setUniformValue("qgl_Material.color_diffuse", mat.color_diffuse);
	prog->setUniformValue("qgl_Material.color_self_illumination", mat.color_self_illumination);
	prog->setUniformValue("qgl_Material.color_specular", mat.color_specular);
	setUniformMap(prog, "map_diffuse", mat.map_diffuse, 0, 6);
	setUniformMap(prog, "map_normal", mat.map_normal, 1, 7);
	setUniformMap(prog, "map_relief", mat.map_relief, 2, 6);
	setUniformMap(prog, "map_self_illumination", mat.map_self_illumination, 3, 6);
	setUniformMap(prog, "map_specularity", mat.map_specularity, 4, 6);
	setUniformMap(prog, "map_specular", mat.map_specular, 5, 6);
}


void setUniformLights(QOpenGLShaderProgram * prog, const QVector<Light *> & lights, const QMatrix4x4 & mat, int shadow_start) {
	for (int i = 0; i < lights.size(); ++i)
		setUniformLight(prog, lights[i], QString("qgl_Light[%1]").arg(i), mat, shadow_start + i);
}
/*
"	vec3 position;\n"
"	vec3 direction;\n"
"	vec4 color;\n"
"	float intensity;\n"
"	float startAngle;\n"
"	float endAngle;\n"
"	float constantAttenuation;\n"
"	float linearAttenuation;\n"
"	float quadraticAttenuation;\n"
"	sampler2DShadow shadow;\n"
"	mat4 shadowMatrix;\n"
*/
void setUniformLight(QOpenGLShaderProgram * prog, Light * light, QString ulightn, const QMatrix4x4 & mat, int shadow) {
	if (!prog) return;
	if (!prog->isLinked()) return;
	QMatrix4x4 m = mat * light->worldTransform();
	QVector4D pos(0, 0, 0, 1.), dir(light->direction, 1); //, dir0(light->dir0), dir1(light->dir1);
	pos             = m * pos;
	dir             = ((m * dir) - pos).normalized();
	float ang_start = light->angle_start / 2.f, ang_end = light->angle_end / 2.f;
	if (light->light_type == Light::Omni) ang_start = ang_end = 180.;
	// qDebug() << "light" << light->name() << ulightn << pos;
	prog->setUniformValue((ulightn + ".position").toLatin1().constData(), pos);
	prog->setUniformValue((ulightn + ".direction").toLatin1().constData(), dir);
	prog->setUniformValue((ulightn + ".intensity").toLatin1().constData(), GLfloat(light->intensity));
	prog->setUniformValue((ulightn + ".startAngle").toLatin1().constData(), GLfloat(ang_start));
	prog->setUniformValue((ulightn + ".startAngleCos").toLatin1().constData(), GLfloat(cosf(ang_start * deg2rad)));
	prog->setUniformValue((ulightn + ".endAngle").toLatin1().constData(), GLfloat(ang_end));
	prog->setUniformValue((ulightn + ".endAngleCos").toLatin1().constData(), GLfloat(cosf(ang_end * deg2rad)));
	prog->setUniformValue((ulightn + ".color").toLatin1().constData(), light->color());
	prog->setUniformValue((ulightn + ".constantAttenuation").toLatin1().constData(), GLfloat(light->decay_const));
	prog->setUniformValue((ulightn + ".linearAttenuation").toLatin1().constData(), GLfloat(light->decay_linear));
	prog->setUniformValue((ulightn + ".quadraticAttenuation").toLatin1().constData(), GLfloat(light->decay_quadratic));
	prog->setUniformValue((ulightn + ".shadow").toLatin1().constData(), shadow);
	prog->setUniformValue((ulightn + ".shadowColor").toLatin1().constData(), shadow);
	prog->setUniformValue((ulightn + ".shadowMatrix").toLatin1().constData(), light->shadow_matrix);
	// qDebug() << light->shadow_matrix;
	// prog->setUniformValue((ulightn + ".shadowDir0").toLatin1().constData(), (mat * dir0));
	// prog->setUniformValue((ulightn + ".shadowDir1").toLatin1().constData(), (mat * dir1));
	// qDebug() << light->direction << light->dir0 << light->dir1;
}
