/*
    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 "glrendererbase.h"

#include "globject.h"
#include "qglview.h"


GLRendererBase::GLRendererBase(QGLView * view_): view(*view_) {
	white_image = QImage(1, 1, QImage::Format_ARGB32);
	white_image.fill(0xFFFFFFFF);
	white_image_id = 0;
	violent_image  = QImage(1, 1, QImage::Format_ARGB32);
	violent_image.fill(QColor(127, 127, 255));
	violent_image_id = 0;
}


void GLRendererBase::setupLight(const Light & l, int inpass_index, int gl_index) {
	QVector3D lp = l.worldPos(), ld = (l.itransform_ * QVector4D(l.direction, 0.)).toVector3D().normalized();
	GLfloat pos[] = {0.f, 0.f, 0.f, 0.f};
	GLfloat dir[] = {0.f, 0.f, 0.f};
	GLfloat col[] = {0.f, 0.f, 0.f};
	pos[0]        = l.light_type == Light::Directional ? -l.direction.x() : lp.x();
	pos[1]        = l.light_type == Light::Directional ? -l.direction.y() : lp.y();
	pos[2]        = l.light_type == Light::Directional ? -l.direction.z() : lp.z();
	pos[3]        = l.light_type == Light::Directional ? 0. : 1.;
	dir[0]        = ld.x();
	dir[1]        = ld.y();
	dir[2]        = ld.z();
	col[0]        = l.visible_ ? l.color().redF() * l.intensity : 0.f;
	col[1]        = l.visible_ ? l.color().greenF() * l.intensity : 0.f;
	col[2]        = l.visible_ ? l.color().blueF() * l.intensity : 0.f;
	glEnable(gl_index);
	// glLightfv(gl_index, GL_AMBIENT, ambient);
	glLightfv(gl_index, GL_DIFFUSE, col);
	glLightfv(gl_index, GL_SPECULAR, col);
	glLightfv(gl_index, GL_POSITION, pos);
	glLightf(gl_index, GL_CONSTANT_ATTENUATION, l.decay_const);
	glLightf(gl_index, GL_LINEAR_ATTENUATION, l.decay_linear);
	glLightf(gl_index, GL_QUADRATIC_ATTENUATION, l.decay_quadratic);
	if (l.light_type == Light::Cone) {
		glLightfv(gl_index, GL_SPOT_DIRECTION, dir);
		glLightf(gl_index, GL_SPOT_CUTOFF, l.angle_end / 2.f);
		glLightf(gl_index,
		         GL_SPOT_EXPONENT,
		         (1.f - piClamp<float>((l.angle_end - l.angle_start) / (l.angle_end + 0.001f), 0., 1.f)) * 128.f);
	} else {
		glLightf(gl_index, GL_SPOT_CUTOFF, 180.);
	}
}


void GLRendererBase::setupAmbientLight(const QColor & a, bool first_pass) {
	GLfloat ambient[] = {0.0f, 0.0f, 0.0f, 1.f};
	if (first_pass) {
		ambient[0] = view.ambientColor_.redF();
		ambient[1] = view.ambientColor_.greenF();
		ambient[2] = view.ambientColor_.blueF();
	}
	glLightModelfv(GL_LIGHT_MODEL_AMBIENT, ambient);
}


void GLRendererBase::setupShadersLights(int lights_count) {
	/*foreach (QOpenGLShaderProgram * i, view.shaders_ppl) {
	    i->bind();
	    i->setUniformValue("lightsCount", lights_count);
	    i->setUniformValue("acc_light", lights_count > 0);
	    //i->setUniformValue("mat", mvm);
	}*/
}


#define BIND_TEXTURE(ch, map)                                                                                              \
	if (rp.prev_tex[ch] != mat.map.bitmap_id) {                                                                            \
		rp.prev_tex[ch] = mat.map.bitmap_id;                                                                               \
		glActiveTexture(GL_TEXTURE0 + ch);                                                                                 \
		glBindTexture(GL_TEXTURE_2D, mat.map.bitmap_id);                                                                   \
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, view.feature(QGLView::qglAnisotropicLevel).toInt()); \
	}

void GLRendererBase::setupTextures(GLObjectBase & o, GLRendererBase::RenderingParameters & rp, bool first_object) {
	if (first_object) {
		view.glReleaseTextures();
		return;
	}
	setupShadersTextures(o, rp);
	Material & mat(o.material_);
	if (rp.light) {
		if (o.accept_light) {
			if (!rp.prev_light) {
				glSetLightEnabled(true);
				rp.prev_light = true;
			}
		} else {
			if (rp.prev_light) {
				glSetLightEnabled(false);
				rp.prev_light = false;
			}
		}
	}
	if (rp.fog) {
		if (o.accept_fog) {
			if (!rp.prev_fog) {
				glSetFogEnabled(true);
				rp.prev_fog = true;
			}
		} else {
			if (rp.prev_fog) {
				glSetFogEnabled(false);
				rp.prev_fog = false;
			}
		}
	}
	if (rp.textures) {
		BIND_TEXTURE(0, map_diffuse)
		BIND_TEXTURE(1, map_normal)
		BIND_TEXTURE(2, map_relief)
		BIND_TEXTURE(3, map_self_illumination)
		BIND_TEXTURE(4, map_specularity)
		BIND_TEXTURE(5, map_specular)
		glActiveTexture(GL_TEXTURE0);
	}
}

#undef BIND_TEXTURE


void GLRendererBase::setupLights(int pass, int lights_per_pass) {
	int light_start, light_end, lmax;
	light_start = pass * lights_per_pass;
	light_end   = qMin<int>((pass + 1) * lights_per_pass, view.lights_.size());
	setupAmbientLight(view.ambientColor_, pass == 0);
	if (!view.lights_.isEmpty()) {
		setupShadersLights(light_end - light_start);
		for (int i = light_start; i < light_end; ++i)
			setupLight(*view.lights_[i], i - light_start, GL_LIGHT0 + i - light_start);
		lmax = light_start + 8;
		for (int i = light_end; i < lmax; ++i)
			glDisable(GL_LIGHT0 + i - light_start);
	} else {
		setupShadersLights(0);
		for (int i = 0; i < 8; ++i)
			glDisable(GL_LIGHT0 + i);
	}
}


void GLRendererBase::applyFilteringParameters() {
	if (view.isFeatureEnabled(QGLView::qglLinearFiltering)) {
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
	} else {
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_NEAREST);
	}
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, view.feature(QGLView::qglAnisotropicLevel).toInt());
}


void GLRendererBase::renderObjects(int pass, int light_pass, void * shaders, bool textures, bool light, bool fog) {
	RenderingParameters rpl;
	rpl.pass       = pass;
	rpl.light_pass = light_pass;
	rpl.shaders    = shaders;
	rpl.textures   = textures;
	rpl.light = rpl.prev_light = light;
	rpl.fog = rpl.prev_fog = fog;
	rpl.view_matrix        = rp.view_matrix;
	rpl.prev_view_matrix   = rp.prev_view_matrix;
	rpl.proj_matrix        = rp.proj_matrix;
	rpl.prev_proj_matrix   = rp.prev_proj_matrix;
	rpl.cam_offset_matrix  = view.camera()->offsetMatrix();
	// qDebug() << "view:" << rp.view_matrix;
	for (int i = 0; i < 32; ++i)
		rpl.prev_tex[i] = 0;
	setupTextures(view.objects_, rpl, true);
	glSetLightEnabled(rpl.prev_light);
	glSetFogEnabled(rpl.prev_fog);
	glSetCapEnabled(GL_TEXTURE_2D, rpl.textures);
	glSetCapEnabled(GL_BLEND, pass == GLObjectBase::Transparent);
	glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
	glDisable(GL_TEXTURE_CUBE_MAP);
	glPushMatrix();
	renderSingleObject(view.objects_, rpl);
	glPopMatrix();
}


void GLRendererBase::renderSingleObject(GLObjectBase & o, RenderingParameters & rpl) {
	if (!o.isInit()) o.init();
	if (!o.isTexturesLoaded()) o.loadTextures();
	if (!o.visible_) return;
	if (rpl.pass == o.pass_) {
		Material & mat(o.material_);
		QMatrix4x4 curview  = rpl.view_matrix * rpl.cam_offset_matrix * o.itransform_,
				   prevview = rpl.prev_view_matrix * rpl.cam_offset_matrix * o.itransform_;
		setupTextures(o, rpl, false);
		mat.apply((QOpenGLShaderProgram *)rpl.shaders);
		glSetPolygonMode(o.render_mode != GLObjectBase::View ? o.render_mode : (view.rmode != GLObjectBase::View ? view.rmode : GL_FILL));
		glLineWidth(o.line_width > 0.f ? o.line_width : view.lineWidth_);
		glPointSize(o.line_width > 0.f ? o.line_width : view.lineWidth_);
		o.update();
		if (o.pass_ == GLObjectBase::Transparent) {
			glActiveTexture(GL_TEXTURE0 + 3);
			if (mat.reflectivity > 0.f) {
				glEnable(GL_TEXTURE_CUBE_MAP);
				if (!mat.map_reflection.isEmpty())
					mat.map_reflection.bind();
				else
					glDisable(GL_TEXTURE_CUBE_MAP);
			} else
				glDisable(GL_TEXTURE_CUBE_MAP);
			if (rpl.light_pass > 0) glDisable(GL_TEXTURE_CUBE_MAP);
			GLfloat gm[16], bc[4] = {mat.reflectivity, mat.reflectivity, mat.reflectivity, mat.reflectivity};
			glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_COMBINE);
			glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_RGB, GL_INTERPOLATE);
			glTexEnvi(GL_TEXTURE_ENV, GL_SRC2_RGB, GL_CONSTANT);
			glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND2_RGB, GL_SRC_COLOR);
			glTexEnvfv(GL_TEXTURE_ENV, GL_TEXTURE_ENV_COLOR, bc);
			glGetFloatv(GL_MODELVIEW_MATRIX, gm);
			glMatrixMode(GL_TEXTURE);
			/// glLoadTransposeMatrixf(gm);
			glScalef(-1., -1., -1.);
			glMatrixMode(GL_MODELVIEW);
			glActiveTexture(GL_TEXTURE0);
		}
		if (rpl.shaders) {
			// qDebug() << o.name() << curview << curview.determinant();
			setUniformMatrices((QOpenGLShaderProgram *)rpl.shaders, rpl.proj_matrix, curview, rpl.prev_proj_matrix, prevview);
		} else {
			glMatrixMode(GL_MODELVIEW);
			setGLMatrix(curview);
		}
		o.draw((QOpenGLShaderProgram *)rpl.shaders);
	}
	foreach(GLObjectBase * i, o.children_)
		renderSingleObject(*i, rpl);
}


void GLRendererBase::renderShadow(Light * l, QOpenGLShaderProgram * prog, QMatrix4x4 mat) {
	Camera cam;
	QVector3D wp = l->worldPos();
	cam.setPos(wp);
	cam.setAim(wp + (l->worldTransform() * QVector4D(l->direction)).toVector3D());
	cam.setDepthStart(view.camera()->depthStart());
	cam.setDepthEnd(view.camera()->depthEnd());
	cam.setFOV(l->angle_end);
	cam.apply(1.);
	/*cam.rotateXY(l->angle_end);
	QVector3D rdir = l->direction * cos(l->angle_end / 2. * deg2rad);
	l->dir0 = cam.direction() - rdir;
	cam.rotateXY(-l->angle_end);
	cam.rotateZ(l->angle_end);
	l->dir1 = cam.direction() - rdir;*/
	// qDebug() << rdir << l->dir0 << l->dir1;
	RenderingParameters rpl;
	rpl.pass     = GLObjectBase::Solid;
	rpl.shaders  = prog;
	rpl.textures = rpl.light = rpl.fog = false;
	rpl.view_matrix                    = getGLMatrix(GL_MODELVIEW_MATRIX);
	rpl.proj_matrix                    = getGLMatrix(GL_PROJECTION_MATRIX);
	rpl.cam_offset_matrix              = cam.offsetMatrix();
	QMatrix4x4 mbias;
	mbias.scale(0.5, 0.5, 0.5);
	mbias.translate(1., 1., 1.);
	l->shadow_matrix = mbias * rpl.proj_matrix * rpl.view_matrix * rpl.cam_offset_matrix * mat; //;// * mbias;
	// qDebug() << mbias;
	// glPushMatrix();
	renderSingleShadow(view.objects_, rpl);
	// glPopMatrix();
}


void GLRendererBase::renderSingleShadow(GLObjectBase & o, RenderingParameters & rpl) {
	if (!o.isInit()) o.init();
	if (!o.visible_) return;
	if (rpl.shaders) {
		// qDebug() << o.name() << curview << curview.determinant();
		setUniformMatrices((QOpenGLShaderProgram *)rpl.shaders, rpl.proj_matrix, rpl.view_matrix * rpl.cam_offset_matrix * o.itransform_);
	} else {
		glMatrixMode(GL_MODELVIEW);
		setGLMatrix(rpl.view_matrix * rpl.cam_offset_matrix * o.itransform_);
	}
	glPolygonMode(GL_FRONT_AND_BACK,
	              o.render_mode != GLObjectBase::View ? o.render_mode : (view.rmode != GLObjectBase::View ? view.rmode : GL_FILL));
	glLineWidth(o.line_width > 0.f ? o.line_width : view.lineWidth_);
	glPointSize(o.line_width > 0.f ? o.line_width : view.lineWidth_);
	o.draw((QOpenGLShaderProgram *)rpl.shaders, true);
	foreach(GLObjectBase * i, o.children_)
		renderSingleShadow(*i, rpl);
}


GLRendererBase::RenderingParameters::RenderingParameters() {
	shaders    = nullptr;
	cur_shader = nullptr;
}


void GLRendererBase::RenderingParameters::prepare() {
	proj_matrix       = getGLMatrix(GL_PROJECTION_MATRIX);
	view_matrix       = getGLMatrix(GL_MODELVIEW_MATRIX);
	viewproj_matrix   = proj_matrix * view_matrix;
	normal_matrix     = view_matrix.normalMatrix();
	proj_matrix_i     = proj_matrix.inverted();
	view_matrix_i     = view_matrix.inverted();
	viewproj_matrix_i = viewproj_matrix.inverted();
}


void GLRendererBase::RenderingParameters::setUniform(QOpenGLShaderProgram * prog) {
	if (!prog) return;
	prog->setUniformValue("qgl_ModelViewMatrix", view_matrix);
	prog->setUniformValue("qgl_ProjectionMatrix", proj_matrix);
	prog->setUniformValue("qgl_ModelViewProjectionMatrix", viewproj_matrix);
	prog->setUniformValue("qgl_NormalMatrix", normal_matrix);
	prog->setUniformValue("qgl_ModelViewMatrixInverse", view_matrix_i);
	prog->setUniformValue("qgl_ProjectionMatrixInverse", proj_matrix_i);
	prog->setUniformValue("qgl_ModelViewProjectionMatrixInverse", viewproj_matrix_i);
	prog->setUniformValue("qgl_ModelViewMatrixTranspose", view_matrix.transposed());
	prog->setUniformValue("qgl_ProjectionMatrixTranspose", proj_matrix.transposed());
	prog->setUniformValue("qgl_ModelViewProjectionMatrixTranspose", viewproj_matrix.transposed());
	prog->setUniformValue("qgl_ModelViewMatrixInverseTranspose", view_matrix_i.transposed());
	prog->setUniformValue("qgl_ProjectionMatrixInverseTranspose", proj_matrix_i.transposed());
	prog->setUniformValue("qgl_ModelViewProjectionMatrixInverseTranspose", viewproj_matrix_i.transposed());
}
