/*
    QGLView
    Copyright (C) 2020  Ivan Pelipenko peri4ko@yandex.ru

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU 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 General Public License for more details.

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

#define GL_GLEXT_PROTOTYPES
#include <QOpenGLExtraFunctions>
#include "renderer.h"
#include "qglview.h"
#include "glmesh.h"
#include "gltexture_manager.h"
#include <qad_types.h>

using namespace QGLEngineShaders;


Renderer::Renderer(QGLView * view_): RendererBase(view_),
		fbo_ds       (view_, QVector<GLenum>() << GL_RGBA16F << GL_RGBA32F << GL_RGBA16F << GL_RGBA16F << GL_RGBA16F),
		fbo_out      (view_, obrBuffersCount, false, GL_RGBA16F),
		rend_mat(this), rend_service(this), rend_selection(this), tone_proc(this), tex_env(view_, 512) {
	quad = Primitive::plane(2., 2.);
	cam_light = new Light();
	cam_light->intensity = 0.75;
	cam_light->setName("Camera_Light");

	shader_files[srSelectionFill ] = "selection.glsl";
	shader_files[srSelectionHalo ] = "selection_halo.glsl";
	shader_files[srSelectionApply] = "selection_apply.glsl";
	shader_files[srSelectionFrame] = "selection_frame.glsl";

	shader_files[srServiceFill ] = "service_fill.glsl";
	shader_files[srServiceFrame] = "service_frame.glsl";
	shader_files[srServiceLine ] = "service_line.glsl";

	shader_files[srGeometryPass ] = "ds_geom.glsl";
	shader_files[srLightOmniPass] = "ds_light.glsl";
	shader_files[srLightSpotPass] = "ds_light.glsl"; shader_defines[srLightSpotPass] << "SPOT";
	shader_files[srFinalPass    ] = "ds_final.glsl";
	shader_files[srTonemapPass  ] = "ds_tonemap.glsl";

	/*shaders << ShaderPair("FXAA", &shader_fxaa)
			<< ShaderPair("dsl_pass_0", &shader_ds_0)
			<< ShaderPair("dsl_pass_1", &shader_ds_1)
			<< ShaderPair("hdr", &shader_hdr)
			<< ShaderPair("downscale", &shader_small)
			<< ShaderPair("bloom_pass_0", &shader_bloom_0)
			<< ShaderPair("bloom_pass_1", &shader_bloom_1)
			<< ShaderPair("fbo_add", &shader_fbo_add)
			<< ShaderPair("motion_blur", &shader_motion_blur)
			<< ShaderPair("shadow", &shader_shadow)
			<< ShaderPair("ssr", &shader_ssr)
			<< ShaderPair("ssr_blur", &shader_ssr_blur)
			<< ShaderPair("ssr_merge", &shader_ssr_merge)
			<< ShaderPair("ssao_blur", &shader_ssao_blur)
			<< ShaderPair("ssao_merge", &shader_ssao_merge)
			<< ShaderPair("dof", &shader_dof);*/
	shader_fxaa = 0;
	gamma_ = 1.;
	edit_mode = need_init_shaders = true;
	camera_light_mode = QGLView::clmAuto;
}


Renderer::~Renderer() {
	delete quad;
	delete cam_light;
	qDeleteAll(shaders.values());
	if (shader_fxaa) delete shader_fxaa;
}


void Renderer::init(int width, int height) {
	resize(width, height);
	rend_mat.init(width, height);
	rend_service.init(width, height);
	rend_selection.init(width, height);
	tone_proc.init();
	initQuad(quad);
	initTextureArrays();
	initCoeffTextures();
	tex_env.init();
	need_init_shaders = true;
}


void Renderer::resize(int width, int height) {
	rend_mat.resize(width, height);
	rend_service.resize(width, height);
	rend_selection.resize(width, height);
	fbo_ds       .resize(width, height);
	fbo_out      .resize(width, height);
	tone_proc.resize();
}


void Renderer::reloadShaders() {
	QMapIterator<ShaderRole, QString> it(shader_files);
	QString dir = ":shaders/";
	while (it.hasNext()) {
		it.next();
		loadShadersMulti(shaders[it.key()], dir + it.value(), true, shader_defines.value(it.key()));
	}
	loadShadersMulti(tone_proc.shader_sum, dir + "sum.glsl", false);
	QStringList fxaa_defs;
	fxaa_defs << "FXAA_PC 1" << "FXAA_GLSL_130 1" << "FXAA_QUALITY__PRESET 15";
	loadShaders(shader_fxaa, QStringList() << (dir + "fxaa.vert") << (dir + "fxaa.frag"), true, fxaa_defs);
	need_init_shaders = true;
	view->scene()->setLightsChanged();
	view->scene()->setTreeStructChanged();
	view->scene()->setMaterialsChanged();
}


bool Renderer::bindShader(Renderer::ShaderRole role, QOpenGLShaderProgram ** ret) {
	QOpenGLShaderProgram * prog = shaders.value(role);
	if (ret) *ret = prog;
	if (!prog) return false;
	if (!prog->isLinked()) return false;
	prog->bind();
	return true;
}


bool Renderer::bindShader(QOpenGLShaderProgram * sp) {
	if (!sp) return true;
	if (!sp->isLinked()) return true;
	if (!sp->bind()) return false;
	return true;
}


void Renderer::initShaders() {
	if (!need_init_shaders) return;
	need_init_shaders = false;
	initUniformBuffer(shaders.value(srGeometryPass ), &buffer_materials , bpMaterials      , "QGLMaterialData"      );
	initUniformBuffer(shaders.value(srLightOmniPass), &buffer_materials , bpMaterials      , "QGLMaterialData"      );
	initUniformBuffer(shaders.value(srLightOmniPass), &buffer_lights    , bpLightParameters, "QGLLightParameterData");
	initUniformBuffer(shaders.value(srLightOmniPass), &buffer_lights_pos, bpLightPositions , "QGLLightPositionData" );
	initUniformBuffer(shaders.value(srLightSpotPass), &buffer_materials , bpMaterials      , "QGLMaterialData"      );
	initUniformBuffer(shaders.value(srLightSpotPass), &buffer_lights    , bpLightParameters, "QGLLightParameterData");
	initUniformBuffer(shaders.value(srLightSpotPass), &buffer_lights_pos, bpLightPositions , "QGLLightPositionData" );
	ShaderRole roles[] = {srLightOmniPass, srLightSpotPass};
	QOpenGLShaderProgram * prog = 0;
	for (int p = 0; p < 2; ++p) {
		if (!bindShader(roles[p], &prog)) continue;
		for (int i = 0; i < 5; ++i)
			prog->setUniformValue(QString("tex_%1").arg(i).toLatin1().constData(), i);
		prog->setUniformValue("tex_coeffs[0]", (int)Renderer::dbrBuffersCount);
		//prog->setUniformValue("tex_coeffs[1]", (int)Renderer::dbrBuffersCount+1);
		prog->setUniformValue("tex_env", (int)Renderer::dbrBuffersCount+1);
	}
	if (bindShader(srFinalPass, &prog)) {
		prog->setUniformValue("tex_g1" , 0);
		prog->setUniformValue("tex_s_0", 1);
		prog->setUniformValue("tex_s_1", 2);
		prog->setUniformValue("tex_t_0", 3);
		prog->setUniformValue("tex_t_1", 4);
	}
	if (bindShader(srGeometryPass, &prog)) {
		setUniformMaps(prog);
	}
	if (bindShader(srTonemapPass, &prog)) {
		prog->setUniformValue("tex_0", 0);
		prog->setUniformValue("tex_sum", 1);
	}
}


void Renderer::releaseShader() {
	view->glUseProgram(0);
}


void Renderer::fillObjectsBuffer(const ObjectBaseList & ol, RenderPass pass) {
	cur_objects_.resize(ol.size());
	for (int i = 0; i < ol.size(); ++i) {
		Object & so(cur_objects_[i]);
		ObjectBase * o = ol[i];
		if (o->material()) {
			so.material = o->material()->_index;
			so.color = QVector4D(1,1,1,1);
		} else {
			so.material = 0;
			so.color = QColor2QVector(o->color_);
		}
		so.object_id = o->id_;
		o->worldTransform().transposed().copyDataTo(so.modelmatrix);
		//qDebug() << "load obj" << o->name() << o->worldTransform();
	}
	//qDebug() << "fillObjectsBuffer" << ol.size();

}


void Renderer::renderObjects(Scene & scene, RenderPass pass) {
	QOpenGLExtraFunctions * f = view;
	QMapIterator<Mesh*, ObjectBaseList> it(scene.geometries_used[pass]);
	bool emit_pos_change = false;
	while (it.hasNext()) {
		it.next();
		Mesh * mesh = it.key();
		if (mesh->isObjectsChanged(pass)) {
			mesh->setObjectsChanged(pass, false);
			emit_pos_change = true;
			fillObjectsBuffer(it.value(), pass);
			//qDebug() << "loadObjects" << pass << cur_objects_.size();
			mesh->loadObjects(f, cur_objects_, pass);
		}
		if (mesh->isSelectionChanged(pass) && edit_mode) {
			mesh->setSelectionChanged(pass, false);
			fillSelectionsBuffer(rend_selection.cur_selections_, it.value());
			//qDebug() << "fillSelectionsBuffer" << pass << rend_selection.cur_selections_.size();
			mesh->loadSelections(f, rend_selection.cur_selections_, pass);
		}
		//qDebug() << "draw" << pass << it.value().size();
		mesh->draw(f, it.value().size(), pass);
	}
	if (emit_pos_change) emit view->objectsPositionChanged();
}


void Renderer::renderLight(int first_wr_buff, bool clear_only) {
	QOpenGLShaderProgram * prog = 0;
	Camera * cam = view->camera();
	for (int i = 0; i < 2; ++i) {
		view->glActiveTexture(GL_TEXTURE0 + Renderer::dbrBuffersCount + i);
		view->glBindTexture(GL_TEXTURE_2D, tex_coeff[i]);
	}
	fbo_ds.bindColorTextures();
	fbo_out.bind();
	tex_env.bind((int)Renderer::dbrBuffersCount+1);
	//int ri = 1, wi = 0;
	typedef QPair<Renderer::ShaderRole, Light::Type> PassPair;
	QVector<PassPair> passes;
	passes << PassPair(srLightOmniPass, Light::Omni) << PassPair(srLightSpotPass, Light::Cone);
	QColor back = view->fogColor_;
	back.setAlpha(0);
	foreach (PassPair pass, passes) {
		if (bindShader(pass.first, &prog)) {
			fbo_out.setWriteBuffer(first_wr_buff + pass.second);
			glClearFramebuffer(back, false);
			if (clear_only) continue;
			setUniformCamera(prog, cam);
			setUniformViewCorners(prog, cam);
			prog->setUniformValue("lights_start", lights_start[pass.second]);
			prog->setUniformValue("lights_count", cur_lights[pass.second].size());
			prog->setUniformValue("fog_color", view->fogColor_);
			prog->setUniformValue("fog_decay", qMax(view->fogDecay_, 0.001f));
			prog->setUniformValue("fog_density", view->fogDensity_);
			prog->setUniformValue("view_mat", cam->viewMatrix().inverted().toGenericMatrix<3,3>());
			renderQuad(prog, quad, cam);
		}
	}
}


void Renderer::renderScene() {
	initShaders();
	tex_env.load();
	QOpenGLExtraFunctions * f = view;
	Scene & scene(*(view->scene_));
	Camera * cam = view->camera();
	QOpenGLShaderProgram * prog = 0;
	bool scene_changed = scene.prepare();
	scene.destroyUnused(f);

	/// reload materials on change
	if (scene_changed || scene.need_reload_materials) {
		//if (scene.need_reload_materials)
		//	maps_hash = 0;
		rend_selection.generateObjectsID(scene);
		reloadMaterials(scene);
		if (edit_mode)
			recreateMaterialThumbnails();
		emit view->materialsChanged();
	}

	/// material thumbnails
	if (edit_mode && !scene_changed) {
		rend_mat.procQueue();
	}

	/// lights
	cur_lights = scene.lights_used;
	bool use_camlight = (camera_light_mode == QGLView::clmOn);
	if ((camera_light_mode == QGLView::clmAuto) && cur_lights.isEmpty())
		use_camlight = true;
	if (use_camlight) {
		cur_lights[Light::Omni] << cam_light;
		cam_light->setPos(cam->pos());
	}
	if (scene.lights_changed) {
		scene.lights_changed = false;
		reloadLightsParameters(cur_lights);
	}
	reloadLightsPositions(cam);

	/// selection
	if (edit_mode) {
		rend_selection.renderSelection(scene);
	}

	/// solid geometry pass
	fbo_ds.bind();
	glEnableDepth();
	glClearFramebuffer();
	if (bindShader(srGeometryPass, &prog)) {
		setUniformCamera(prog, cam);
		textures_empty.bind(f, tarEmpty);
		textures_maps .bind(f, tarMaps );
		renderObjects(scene, rpSolid);
	}
	fbo_ds.release();

	/// lighting passes
	renderLight(obrSolidOmni, scene.geometries_used[rpSolid].isEmpty());

	/// transparent geometry pass
	fbo_ds.bind();
	glEnableDepth();
	glClearFramebuffer(Qt::black, false);
	if (bindShader(srGeometryPass, &prog)) {
		renderObjects(scene, rpTransparent);
	}
	fbo_ds.release();

	/// lighting passes
	renderLight(obrTransparentOmni, scene.geometries_used[rpTransparent].isEmpty());

	/// blending layers
	if (bindShader(srFinalPass, &prog)) {
		//fbo_ds .bindColorTexture(dbrNormalZ        , 0);
		fbo_out.bindColorTexture(obrSolidOmni      , 1);
		fbo_out.bindColorTexture(obrSolidSpot      , 2);
		fbo_out.bindColorTexture(obrTransparentOmni, 3);
		fbo_out.bindColorTexture(obrTransparentSpot, 4);
		fbo_out.setWriteBuffer(obrSum);
		renderQuad(prog, quad);
	}

	/// tonemapping
	if (tone_proc.process())
		fbo_out.bind();
	if (bindShader(srTonemapPass, &prog)) {
		prog->setUniformValue("gamma", gamma_);
		prog->setUniformValue("frame_max", tone_proc.frameMax());
		fbo_out.bindColorTexture(obrSum, 0);
		fbo_out.setWriteBuffer(obrTonemap);
		renderQuad(prog, quad);
	} else
		fbo_out.blit(obrSum, fbo_out.id(), obrTonemap, fbo_out.rect(), fbo_out.rect());
	fbo_out.release();

	/// apply hovers and selection frame
	if (edit_mode) {
		rend_selection.drawSelection(fbo_out, obrTonemap);
		rend_service.renderService();
	} else {
		fbo_out.blit(obrTonemap, 0, 0, fbo_out.rect(), QRect(QPoint(), view->size()));
	}
	//fbo_small_16.blit(0, 0, 0, fbo_small_16.rect(), fbo_small_16.rect(), GL_COLOR_BUFFER_BIT, GL_LINEAR);

}


void Renderer::setCameraLightMode(int m) {
	camera_light_mode = m;
	view->scene()->setLightsChanged();
}
