/*
    QGL RendererBase
    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/>.
*/

#define GL_GLEXT_PROTOTYPES
#include "renderer_base.h"

#include "glmesh.h"
#include "glshaders_headers.h"
#include "gltexture_manager.h"
#include "qglview.h"
#include "renderer.h"

#include <QOpenGLExtraFunctions>

using namespace QGLEngineShaders;


RendererBase::RendererBase(QGLView * view_)
	: view(view_)
	, buffer_materials(GL_UNIFORM_BUFFER, GL_STREAM_DRAW)
	, buffer_lights(GL_UNIFORM_BUFFER, GL_STREAM_DRAW)
	, buffer_lights_pos(GL_UNIFORM_BUFFER, GL_STREAM_DRAW)
	, textures_empty(GL_RGBA, false)
	, textures_maps(GL_RGBA, true)
	, shadow_maps_cone(GL_DEPTH_COMPONENT, false)
	, depth_maps_cone(GL_R32F, false)
	, shadow_maps_omni(GL_DEPTH_COMPONENT, false)
	, depth_maps_omni(GL_R32F, false) {
	textures_manager = new TextureManager(view);
	maps_size        = QSize(1024, 1024);
	maps_hash        = 0;
	tex_coeff[0] = tex_coeff[1] = tex_coeff[2] = 0;
}


RendererBase::~RendererBase() {
	delete textures_manager;
}


void RendererBase::initTextureArrays() {
	QOpenGLExtraFunctions * f = view;
	textures_maps.init(f);
	textures_empty.init(f);
	textures_empty.resize(f, QSize(1, 1), emrCount);
	textures_empty.bind(f, tarEmpty);
	QImage im(1, 1, QImage::Format_RGBA8888);
	im.fill(0xFFFFFFFF);
	textures_empty.load(f, im, emrWhite);
	im.fill(0xFF8080);
	textures_empty.load(f, im, emrBlue);
	im.fill(0x000000);
	textures_empty.load(f, im, emrBlack);
	shadow_maps_cone.init(f);
	shadow_maps_omni.init(f);
	depth_maps_cone.init(f);
	depth_maps_omni.init(f);
	shadow_maps_cone.bind(f, tarShadowsCone);
	f->glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
	f->glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
	f->glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_R_TO_TEXTURE);
	f->glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_COMPARE_FUNC, GL_LESS);
	shadow_maps_omni.bind(f, tarShadowsOmni);
	f->glTexParameteri(GL_TEXTURE_CUBE_MAP_ARRAY, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_REF_TO_TEXTURE);
	f->glTexParameteri(GL_TEXTURE_CUBE_MAP_ARRAY, GL_TEXTURE_COMPARE_FUNC, GL_LESS);
}


void RendererBase::initUniformBuffer(QOpenGLShaderProgram * prog, Buffer * buffer, int bind_point, const char * blockName) {
	if (!prog || !buffer) return;
	if (!prog->isLinked()) return;
	QOpenGLExtraFunctions * f = view;
	buffer->init(f);
	// glClearError();
	GLint ubo_ind = f->glGetUniformBlockIndex(prog->programId(), blockName);
	f->glUniformBlockBinding(prog->programId(), ubo_ind, bind_point);
	f->glBindBufferBase(GL_UNIFORM_BUFFER, bind_point, buffer->ID());
	// qDebug() << "initUBO" << QString::number(f->glGetError(), 16);
}


void RendererBase::setUniformHalo(QOpenGLShaderProgram * prog, const char * type, QColor color, float fill) {
	prog->setUniformValue((QString(type) + "_color").toLatin1().constData(), color);
	prog->setUniformValue((QString(type) + "_fill").toLatin1().constData(), fill);
}


void RendererBase::setUniformMaps(QOpenGLShaderProgram * prog) {
	prog->setUniformValue("qgl_texture_array[0]", (int)tarEmpty);
	prog->setUniformValue("qgl_texture_array[1]", (int)tarMaps);
}


void RendererBase::setUniformCamera(QOpenGLShaderProgram * prog, Camera * cam, bool matrices, QSize viewport) {
	double w = view->pixelWidth(), h = view->pixelHeight();
	if (viewport.isValid()) {
		w = viewport.width();
		h = viewport.height();
	}
	QMatrix4x4 mat_view, mat_proj;
	if (cam) {
		if (matrices) {
			mat_view = cam->fullViewMatrix();
			mat_proj = cam->projectionMatrix(w / h);
		}
		prog->setUniformValue("z_near", cam->depthStart());
	}
	prog->setUniformValue("dt", QVector2D(1. / w, 1. / h));
	prog->setUniformValue("qgl_ViewSize", QVector2D(w, h));
	prog->setUniformValue("qgl_ViewMatrix", mat_view);
	prog->setUniformValue("qgl_ViewProjMatrix", mat_proj * mat_view);
}


void RendererBase::setUniformViewCorners(QOpenGLShaderProgram * prog, Camera * cam, QSize viewport) {
	double w = view->pixelWidth(), h = view->pixelHeight();
	if (viewport.isValid()) {
		w = viewport.width();
		h = viewport.height();
	}
	QMatrix4x4 mproji = cam->projectionMatrix(w / h).inverted();
	QMatrix4x4 mviewi = cam->viewMatrix().inverted();
	QVector4D corner_dirs[4], world_dirs[4];
	corner_dirs[0] = (mproji * QVector4D(-1, -1, 0, 1));
	corner_dirs[1] = (mproji * QVector4D(-1, 1, 0, 1));
	corner_dirs[2] = (mproji * QVector4D(1, 1, 0, 1));
	corner_dirs[3] = (mproji * QVector4D(1, -1, 0, 1));
	for (int i = 0; i < 4; ++i) {
		world_dirs[i] = QVector4D(mviewi.mapVector(corner_dirs[i].toVector3D()));
		prog->setUniformValue(QString("view_corners[%1]").arg(i).toLatin1().constData(), corner_dirs[i]);
		prog->setUniformValue(QString("world_corners[%1]").arg(i).toLatin1().constData(), world_dirs[i]);
	}
}


void RendererBase::fillSelectionsBuffer(QVector<uchar> & buffer, const ObjectBaseList & ol) {
	buffer.resize(ol.size());
	for (int i = 0; i < ol.size(); ++i) {
		buffer[i] = (ol[i]->isSelected(true) ? 1 : 0);
	}
}


void RendererBase::fillSelectionsBuffer(QVector<uchar> & buffer, bool yes, int size) {
	buffer.resize(size);
	for (int i = 0; i < size; ++i)
		buffer[i] = (yes ? 1 : 0);
}


void RendererBase::reloadMaterials(Scene & scene) {
	// qDebug() << "reloadMaterias";
	textures_manager->clearMissed();
	for (Material * m: scene.materials) {
		m->load(textures_manager);
	}
	auto cone_ll = scene.lights_used.value(Light::Cone);
	for (auto * l: cone_ll)
		l->light_map.load(textures_manager);
	uint cur_maps_hash = textures_manager->texturesHash() ^ ((maps_size.width() << 16) | maps_size.height());
	if (maps_hash != cur_maps_hash) {
		maps_hash = cur_maps_hash;
		// textures_maps.resize(view, maps_size, textures_manager->texturesCount());
		textures_maps.bind(view, tarMaps);
		textures_manager->loadToTexture2DArray(&textures_maps, maps_size);
		qDebug() << "loaded" << textures_maps.layersCount() << "bitmaps";
	}
	for (Material * m: scene.materials) {
		m->setMapsLayers(textures_manager);
	}
	for (auto * l: cone_ll)
		l->light_map.setMapLayer(textures_manager);
	textures_manager->loadingDone();

	QGLMaterial glm;
	cur_materials_.clear();
	cur_materials_ << glm;
	foreach(Material * m, scene.materials) {
		if (cur_materials_.size() >= max_materials) {
			qDebug() << "[QGLEngine] Warning: Too many materials! Maximum" << max_materials;
			break;
		}
		m->_index          = cur_materials_.size();
		m->_changed        = false;
		glm.color_diffuse  = QColor2QVector(m->color_diffuse);
		glm.color_emission = QColor2QVector(m->color_emission);
		glm.transparency   = m->transparency;
		glm.reflectivity   = m->reflectivity;
		glm.iof            = m->iof;
		glm.dispersion     = m->dispersion;
		m->map_diffuse.copyToQGLMap(glm.map[mtDiffuse]);
		m->map_normal.copyToQGLMap(glm.map[mtNormal]);
		m->map_metalness.copyToQGLMap(glm.map[mtMetalness]);
		m->map_roughness.copyToQGLMap(glm.map[mtRoughness]);
		m->map_emission.copyToQGLMap(glm.map[mtEmission]);
		m->map_relief.copyToQGLMap(glm.map[mtRelief]);
		cur_materials_ << glm;
	}
	// qDebug() << "load" << cur_materials_.size() << "materials";
	buffer_materials.bind(view);
	buffer_materials.resize(view, cur_materials_.size() * sizeof(QGLMaterial));
	buffer_materials.load(view, cur_materials_.constData(), cur_materials_.size() * sizeof(QGLMaterial));
	scene.need_reload_materials = false;
}


void RendererBase::reloadLightsParameters(const QMap<int, QList<Light *>> & lights) {
	lights_start.clear();
	lights_start[Light::Omni] = 0;
	QMapIterator<int, QList<Light *>> it(lights);
	current_lights.clear();
	while (it.hasNext()) {
		it.next();
		lights_start[it.key()] = current_lights.size();
		current_lights.append(it.value());
	}
	cur_lights_params_.resize(qMin(current_lights.size(), max_lights));
	// qDebug() << "reloadLightsParameters" << cur_lights_params_.size();
	for (int i = 0; i < cur_lights_params_.size(); ++i) {
		QGLLightParameter & so(cur_lights_params_[i]);
		Light * l        = current_lights[i];
		double ang_start = l->angle_start / 2.f, ang_end = l->angle_end / 2.f;
		if (l->light_type == Light::Omni) ang_start = ang_end = 180.;
		// qDebug() << "light" << light->name() << ulightn << pos;
		so.color              = QColor2QVector(l->color_);
		so.angles[0]          = ang_start;
		so.angles[1]          = cos(ang_start * deg2rad);
		so.angles[2]          = ang_end;
		so.angles[3]          = cos(ang_end * deg2rad);
		so.decay_intensity[0] = l->decay_const;
		so.decay_intensity[1] = l->decay_linear;
		so.decay_intensity[2] = l->decay_quadratic;
		so.decay_intensity[3] = l->intensity;
		so.size               = l->size;
		so.flags              = l->cast_shadow ? 1 : 0;
		if (l->light_type == Light::Cone) l->light_map.copyToQGLMap(so.map);
	}
	buffer_lights.bind(view);
	buffer_lights.resize(view, cur_lights_params_.size() * sizeof(QGLLightParameter));
	buffer_lights.load(view, cur_lights_params_.constData(), cur_lights_params_.size() * sizeof(QGLLightParameter));
}


void RendererBase::reloadLightsPositions(Camera * cam) {
	cur_lights_pos_.resize(qMin(current_lights.size(), max_lights));
	QMatrix4x4 mat = cam->viewMatrix() * cam->offsetMatrix();
	for (int i = 0; i < cur_lights_pos_.size(); ++i) {
		QGLLightPosition & so(cur_lights_pos_[i]);
		Light * l    = current_lights[i];
		QMatrix4x4 m = mat * l->worldTransform();
		QVector4D pos(0, 0, 0, 1.), dir(0, 0, -1, 1);
		pos          = m * pos;
		dir          = (m * QVector4D(0, 0, -1, 0)).normalized();
		so.position  = pos;
		so.direction = dir;
		l->shadow_matrix.transposed().copyDataTo(so.shadowmatrix);
	}
	buffer_lights_pos.bind(view);
	buffer_lights_pos.resize(view, cur_lights_pos_.size() * sizeof(QGLLightPosition));
	buffer_lights_pos.load(view, cur_lights_pos_.constData(), cur_lights_pos_.size() * sizeof(QGLLightPosition));
}


void RendererBase::markReloadTextures() {
	maps_hash = 0;
	textures_manager->clearImageCache();
	view->scene()->need_reload_materials = true;
}


void RendererBase::markReloadMaterials() {
	view->scene()->need_reload_materials = true;
}


void RendererBase::setMapsSize(QSize sz) {
	maps_size = sz;
	markReloadTextures();
}


void RendererBase::initQuad(Mesh * mesh, QMatrix4x4 mat) {
	QGLEngineShaders::Object quab_object;
	mat.transposed().copyDataTo(quab_object.modelmatrix);
	mesh->init(view);
	mesh->loadObject(view, quab_object);
}


void RendererBase::renderQuad(QOpenGLShaderProgram * prog, Mesh * mesh, Camera * cam, bool uniforms) {
	glDisableDepth();
	if (uniforms) setUniformCamera(prog, cam, false);
	mesh->draw(view, 1);
}


// ----------------------------------------------------------------------------

float RadicalInverse_VdC(uint bits) {
	bits = (bits << 16u) | (bits >> 16u);
	bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xAAAAAAAAu) >> 1u);
	bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xCCCCCCCCu) >> 2u);
	bits = ((bits & 0x0F0F0F0Fu) << 4u) | ((bits & 0xF0F0F0F0u) >> 4u);
	bits = ((bits & 0x00FF00FFu) << 8u) | ((bits & 0xFF00FF00u) >> 8u);
	return float(bits) * 2.3283064365386963e-10; // / 0x100000000
}


QVector2D Hammersley(uint i, uint N) {
	return QVector2D(float(i) / float(N), RadicalInverse_VdC(i));
}


QVector3D ImportanceSampleGGX(QVector2D Xi, QVector3D N, float roughness) {
	float a        = roughness * roughness;
	float phi      = 2.0 * M_PI * Xi[0];
	float cosTheta = sqrt((1.0 - Xi[1]) / (1.0 + (a * a - 1.0) * Xi[1]));
	float sinTheta = sqrt(1.0 - cosTheta * cosTheta);
	// преобразование из сферических в декартовы координаты
	QVector3D H;
	H[0]                = cos(phi) * sinTheta;
	H[1]                = sin(phi) * sinTheta;
	H[2]                = cosTheta;
	// преобразование из касательного пространства в мировые координаты
	QVector3D up        = qAbs(N[2]) < 0.999 ? QVector3D(0.0, 0.0, 1.0) : QVector3D(1.0, 0.0, 0.0);
	QVector3D tangent   = QVector3D::crossProduct(up, N).normalized();
	QVector3D bitangent = QVector3D::crossProduct(N, tangent);
	QVector3D sampleVec = tangent * H[0] + bitangent * H[1] + N * H[2];
	return sampleVec.normalized();
}


float GeometrySchlickGGX(float NdotV, float roughness) {
	float k     = (roughness * roughness) / 2.0;
	float nom   = NdotV;
	float denom = NdotV * (1.0 - k) + k;
	return nom / denom;
}


float GeometrySmith(QVector3D N, QVector3D V, QVector3D L, float roughness) {
	float NdotV = piMax(QVector3D::dotProduct(N, V), 0.f);
	float NdotL = piMax(QVector3D::dotProduct(N, L), 0.f);
	float ggx2  = GeometrySchlickGGX(NdotV, roughness);
	float ggx1  = GeometrySchlickGGX(NdotL, roughness);
	return ggx1 * ggx2;
}


QVector2D IntegrateBRDF(float NdotV, float roughness) {
	QVector3D V;
	V[0]                    = sqrt(1.f - NdotV * NdotV);
	V[1]                    = 0.f;
	V[2]                    = NdotV;
	float A                 = 0.f;
	float B                 = 0.f;
	QVector3D N             = QVector3D(0.f, 0.f, 1.f);
	const uint SAMPLE_COUNT = 256u;
	for (uint i = 0u; i < SAMPLE_COUNT; ++i) {
		QVector2D Xi = Hammersley(i, SAMPLE_COUNT);
		QVector3D H  = ImportanceSampleGGX(Xi, N, roughness);
		QVector3D L  = (2.f * QVector3D::dotProduct(V, H) * H - V).normalized();
		float NdotL  = piMax(L[2], 0.f);
		float NdotH  = piMax(H[2], 0.f);
		float VdotH  = piMax(QVector3D::dotProduct(V, H), 0.f);
		if (NdotL > 0.f) {
			float G     = GeometrySmith(N, V, L, roughness);
			float G_Vis = (G * VdotH) / (NdotH * NdotV);
			float Fc    = pow(1.f - VdotH, 5.f);
			A += (1.f - Fc) * G_Vis;
			B += Fc * G_Vis;
		}
	}
	A /= float(SAMPLE_COUNT);
	B /= float(SAMPLE_COUNT);
	return QVector2D(A, B);
}


void RendererBase::initCoeffTextures() {
	QImage im = QImage(":/coeffs_brdf.png").mirrored();
	int size  = im.width();
	QVector<QVector2D> data(size * size);
	int ind = -1;
	for (int x = 0; x < size; ++x) {
		for (int y = 0; y < size; ++y) {
			QColor p    = im.pixelColor(x, y);
			data[++ind] = QVector2D(p.redF(), p.greenF());
		}
	}
	createCoeffTexture(tex_coeff[0], data.constData(), size, 2);
	// createNoiseTexture(tex_coeff[2]);
}


void RendererBase::createCoeffTexture(GLuint & id, const void * data, int size, int channels) {
	QOpenGLExtraFunctions * f = view;
	deleteGLTexture(f, id);
	f->glGenTextures(1, &id);
	f->glBindTexture(GL_TEXTURE_2D, id);
	f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
	f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
	f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
	f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
	GLenum iformat = GL_R16F, format = GL_RED;
	if (channels == 2) {
		iformat = GL_RG16F;
		format  = GL_RG;
	}
	if (channels == 3) {
		iformat = GL_RGB16F;
		format  = GL_RGB;
	}
	if (channels == 4) {
		iformat = GL_RGBA16F;
		format  = GL_RGBA;
	}
	f->glTexImage2D(GL_TEXTURE_2D, 0, iformat, size, size, 0, format, GL_FLOAT, data);
}


void RendererBase::createNoiseTexture(GLuint & id) {
	QOpenGLExtraFunctions * f = view;
	deleteGLTexture(f, id);
	f->glGenTextures(1, &id);
	f->glBindTexture(GL_TEXTURE_2D, id);
	f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
	f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
	f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
	f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
	QImage im = generateNoise(QSize(noise_size, noise_size));
	f->glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, im.width(), im.height(), 0, GL_RGBA, GL_UNSIGNED_BYTE, im.bits());
}
