/*
    QGLView Types
    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 "glcamera.h"
#include "gltexture_manager.h"
#include "qglview.h"

#include <QPainter>
#include <QRandomGenerator>


QString readCharsUntilNull(QDataStream & s) {
	QString str;
	char ch;
	s.readRawData(&ch, 1);
	while (ch != '\0') {
		str += ch;
		s.readRawData(&ch, 1);
	}
	return str;
}


QString findFile(const QString & file, const QStringList & pathes) {
	QFileInfo fi(QString(file).replace("\\", "/"));
	// qDebug() << "search" << file << "in" << pathes;
	if (fi.exists()) return fi.absoluteFilePath();
	QString fn = fi.fileName();
	if (fn.contains("/")) fn = fn.mid(fn.lastIndexOf("/"));
	foreach(QString p, pathes) {
		QFileInfoList fil = QDir(p).entryInfoList(QStringList(fn), QDir::Files | QDir::NoDotAndDotDot);
		// qDebug() << "findFile" << fn << "in" << p << "->" << fil.size();
		if (!fil.isEmpty()) return fil[0].absoluteFilePath();
	}
	return QString();
}


void glDrawQuad(QOpenGLShaderProgram * prog, QVector4D * corner_dirs, GLfloat x, GLfloat y, GLfloat w, GLfloat h) {
	glSetPolygonMode(GL_FILL);
	glDisable(GL_LIGHTING);
	glEnable(GL_TEXTURE_2D);
	// glColor4f(1, 1, 1, 1);
	int loc = prog ? prog->attributeLocation("qgl_Color") : -1, locv = prog ? prog->attributeLocation("qgl_Vertex") : -1,
		loct = prog ? prog->attributeLocation("qgl_Texture") : -1, locc = prog ? prog->attributeLocation("view_corner") : -1;
	QOpenGLFunctions * glFuncs = QOpenGLContext::currentContext()->functions();
	if (prog) {
		static const GLfloat cols[]  = {1.f, 1.f, 1.f, 1.f, 1.f, 1.f, 1.f, 1.f, 1.f, 1.f, 1.f, 1.f};
		static const GLfloat verts[] = {x, y, x + w, y, x, y + h, x + w, y + h};
		static const GLfloat texs[]  = {0.f, 0.f, 1.f, 0.f, 0.f, 1.f, 1.f, 1.f};
		GLfloat vcs[]                = {0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f};
		if (corner_dirs) {
			vcs[0]  = corner_dirs[0].x();
			vcs[1]  = corner_dirs[0].y();
			vcs[2]  = corner_dirs[0].z();
			vcs[3]  = corner_dirs[1].x();
			vcs[4]  = corner_dirs[1].y();
			vcs[5]  = corner_dirs[1].z();
			vcs[6]  = corner_dirs[2].x();
			vcs[7]  = corner_dirs[2].y();
			vcs[8]  = corner_dirs[2].z();
			vcs[9]  = corner_dirs[3].x();
			vcs[10] = corner_dirs[3].y();
			vcs[11] = corner_dirs[3].z();
		}
		glDisableClientState(GL_VERTEX_ARRAY);
		glDisableClientState(GL_NORMAL_ARRAY);
		glDisableClientState(GL_TEXTURE_COORD_ARRAY);
		glDisableClientState(GL_COLOR_ARRAY);
		glFuncs->glBindBuffer(GL_ARRAY_BUFFER, 0);
		glFuncs->glEnableVertexAttribArray(loc);
		glFuncs->glVertexAttribPointer(loc, 3, GL_FLOAT, 0, 0, cols);
		glFuncs->glEnableVertexAttribArray(locv);
		glFuncs->glVertexAttribPointer(locv, 2, GL_FLOAT, 0, 0, verts);
		glFuncs->glEnableVertexAttribArray(loct);
		glFuncs->glVertexAttribPointer(loct, 2, GL_FLOAT, 0, 0, texs);
		glFuncs->glEnableVertexAttribArray(locc);
		glFuncs->glVertexAttribPointer(locc, 3, GL_FLOAT, 0, 0, vcs);
		glFuncs->glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
		glFuncs->glDisableVertexAttribArray(loc);
		glFuncs->glDisableVertexAttribArray(locv);
		glFuncs->glDisableVertexAttribArray(loct);
		glFuncs->glDisableVertexAttribArray(locc);
	} else {
		glBegin(GL_TRIANGLE_STRIP);
		glColor4f(1.f, 1.f, 1.f, 1.f);
		glTexCoord2f(0.f, 0.f);
		glVertex2f(x, y);
		glTexCoord2f(1.f, 0.f);
		glVertex2f(x + w, y);
		glTexCoord2f(0.f, 1.f);
		glVertex2f(x, y + h);
		glTexCoord2f(1.f, 1.f);
		glVertex2f(x + w, y + h);
		glEnd();
	}
}


QMatrix4x4 getGLMatrix(GLenum matrix) {
	GLfloat gm[16];
	glGetFloatv(matrix, gm);
	float qm[16];
	for (int i = 0; i < 16; ++i)
		qm[i] = gm[i];
	return QMatrix4x4(qm).transposed();
}


void setGLMatrix(QMatrix4x4 matrix) {
	GLfloat gm[16];
	float qm[16];
	matrix.transposed().copyDataTo(qm);
	for (int i = 0; i < 16; ++i)
		gm[i] = qm[i];
	glLoadMatrixf(gm);
}


void qglMultMatrix(const QMatrix4x4 & m) {
	GLfloat gm[16];
	float qm[16];
	m.transposed().copyDataTo(qm);
	for (int i = 0; i < 16; ++i)
		gm[i] = qm[i];
	glMultMatrixf(gm);
}


void createGLTexture(QOpenGLExtraFunctions * f, GLuint & tex, int width, int height, const GLenum & format, const GLenum & target) {
	if (tex == 0) {
		f->glGenTextures(1, &tex);
		f->glBindTexture(target, tex);
	}
	if (format == GL_DEPTH_COMPONENT || format == GL_DEPTH_COMPONENT16 || format == GL_DEPTH_COMPONENT24 || format == GL_DEPTH_COMPONENT32)
		f->glTexImage2D(target, 0, format, width, height, 0, GL_DEPTH_COMPONENT, GL_UNSIGNED_BYTE, nullptr);
	else {
		int type = GL_UNSIGNED_BYTE;
		int fmt  = GL_RGBA;
		if (format == GL_RGB32F || format == GL_RGB16F || format == GL_RGBA32F || format == GL_RGBA16F) type = GL_FLOAT;
		if (format == GL_RGB32F || format == GL_RGB16F || format == GL_RGB8 || format == GL_RGB) fmt = GL_RGB;
		f->glTexImage2D(target, 0, format, width, height, 0, fmt, type, nullptr);
		// qDebug() << "glTexImage2D" << width << height << QString::number(t, 16);
	}
	// qDebug() << QString::number(glGetError(), 16);
}


void createGLTexture(QOpenGLExtraFunctions * f, GLuint & tex, const QImage & image, const GLenum & format, const GLenum & target) {
	if (tex == 0) {
		f->glGenTextures(1, &tex);
	}
	f->glBindTexture(target, tex);
	QImage im = image.mirrored(false, true).convertToFormat(QImage::Format_RGBA8888);
	f->glTexParameteri(target, GL_TEXTURE_WRAP_T, GL_REPEAT);
	f->glTexParameteri(target, GL_TEXTURE_WRAP_S, GL_REPEAT);
	f->glTexParameteri(target, GL_TEXTURE_WRAP_R, GL_REPEAT);
	if (target == GL_TEXTURE_1D || target == GL_TEXTURE_2D || target == GL_TEXTURE_3D) {
		f->glTexParameteri(target, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
		f->glTexParameteri(target, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
		f->glTexParameteri(target, GL_TEXTURE_MAX_ANISOTROPY_EXT, 8);
	} else {
		f->glTexParameteri(target, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
		f->glTexParameteri(target, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
	}
	glClearError();
	f->glTexImage2D(target, 0, format, im.width(), im.height(), 0, GL_RGBA, GL_UNSIGNED_BYTE, im.constBits());
	if (target == GL_TEXTURE_1D || target == GL_TEXTURE_2D || target == GL_TEXTURE_3D) {
		f->glGenerateMipmap(target);
	}
	// qDebug() << target << format << tex << im.width() << im.height() << im.bits() << QString::number(glGetError(), 16);
}


QMatrix4x4 glMatrixPerspective(float angle, float aspect, float near_) {
	QMatrix4x4 ret;
	float t = 1.f / (tanf(angle * deg2rad / 2.f)), e = 2.4e-7f;
	if (aspect >= 1.) {
		ret(0, 0) = t / aspect;
		ret(1, 1) = t;
	} else {
		ret(0, 0) = t;
		ret(1, 1) = t * aspect;
	}
	ret(2, 2) = e - 1.f;
	ret(2, 3) = (e - 2.f) * near_;
	ret(3, 2) = -1.f;
	ret(3, 3) = 0.f;
	return ret;
}


QImage rotateQImageLeft(const QImage & im) {
	QImage ri(im.height(), im.width(), im.format());
	QPainter p(&ri);
	p.rotate(90);
	p.drawImage(0, -im.height(), im);
	p.end();
	return ri;
}


QImage rotateQImageRight(const QImage & im) {
	QImage ri(im.height(), im.width(), im.format());
	QPainter p(&ri);
	p.rotate(-90);
	p.drawImage(-im.width(), 0, im);
	p.end();
	return ri;
}


QColor colorFromString(const QString & str) {
	QString s = str.trimmed();
	int i     = s.indexOf("\t");
	float r, g, b;
	r = s.left(i).toFloat();
	s = s.right(s.length() - i - 1);
	i = s.indexOf("\t");
	g = s.left(i).toFloat();
	s = s.right(s.length() - i - 1);
	b = s.toFloat();
	return QColor(r * 255.f, g * 255.f, b * 255.f);
}


QVector3D orthToVector(const QVector3D & v, const float & scale) {
	if (v.isNull()) return QVector3D();
	QVector3D rv, fn, sn;
	if (v.x() != 0.f)
		rv.setZ(1.);
	else if (v.y() != 0.f)
		rv.setX(1.);
	else
		rv.setY(1.);
	fn = QVector3D::crossProduct(v, rv).normalized();
	sn = QVector3D::crossProduct(v, fn).normalized();
	return fn * urand(scale) + sn * urand(scale);
}


QVector3D rotateVector(const QVector3D & v, const QVector3D & a) {
	QMatrix4x4 m;
	m.rotate(a.z(), 0., 0., 1.);
	m.rotate(a.y(), 0., 1., 0.);
	m.rotate(a.x(), 1., 0., 0.);
	return m * v;
}


void setVectorLength(QVector3D & v, const float & l) {
	float vl = v.length();
	if (vl == 0.f) return;
	float c = l / vl;
	v *= c;
}


void lengthenVector(QVector3D & v, const float & l) {
	float vl = v.length();
	if (l == 0.f || vl == 0.f) return;
	float c = 1.f + l / vl;
	v *= c;
}


Vector2i::Vector2i(const QString & str) {
	QString s = str.trimmed();
	int i     = s.indexOf("\t");
	p0        = s.left(i).toInt();
	s         = s.right(s.length() - i - 1);
	p1        = s.toInt();
}


Vector3i::Vector3i(const QString & str) {
	QString s = str.trimmed();
	int i     = s.indexOf("\t");
	p0        = s.left(i).toInt();
	s         = s.right(s.length() - i - 1);
	i         = s.indexOf("\t");
	p1        = s.left(i).toInt();
	s         = s.right(s.length() - i - 1);
	p2        = s.toInt();
}


void glEnableDepth() {
	glEnable(GL_DEPTH_TEST);
	glDepthFunc(GL_LESS);
	glDepthMask(GL_TRUE);
}


void glDisableDepth() {
	glDisable(GL_DEPTH_TEST);
	glDepthMask(GL_FALSE);
}


void glClearFramebuffer(const QColor & color, bool depth) {
	glClearColor(color.redF(), color.greenF(), color.blueF(), color.alphaF());
	if (depth)
		glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
	else
		glClear(GL_COLOR_BUFFER_BIT);
}


Box3D::Box3D(const QVector<QVector3D> & points) {
	x = y = z = width = length = height = angle_z = angle_xy = angle_roll = 0.f;
	if (points.isEmpty()) return;
	float ix, iy, iz, ax, ay, az;
	ix = ax = points[0].x();
	iy = ay = points[0].y();
	iz = az = points[0].z();
	for (int i = 1; i < points.size(); ++i) {
		ix = qMin<float>(ix, points[i].x());
		ax = qMax<float>(ax, points[i].x());
		iy = qMin<float>(iy, points[i].y());
		ay = qMax<float>(ay, points[i].y());
		iz = qMin<float>(iz, points[i].z());
		az = qMax<float>(az, points[i].z());
	}
	x      = ix;
	y      = iy;
	z      = iz;
	length = ax - ix;
	width  = ay - iy;
	height = az - iz;
}


QVector<QVector3D> Box3D::corners() const {
	QVector<QVector3D> ret;
	ret << QVector3D(x, y, z) << QVector3D(x, y + width, z) << QVector3D(x, y, z + height) << QVector3D(x, y + width, z + height)
		<< QVector3D(x + length, y, z) << QVector3D(x + length, y + width, z) << QVector3D(x + length, y, z + height)
		<< QVector3D(x + length, y + width, z + height);
	return ret;
}


Box3D & Box3D::operator|=(const Box3D & o) {
	if (o.isEmpty()) return *this;
	if (isEmpty())
		*this = o;
	else {
		GLfloat mx = x + length, my = y + width, mz = z + height;
		GLfloat omx = o.x + o.length, omy = o.y + o.width, omz = o.z + o.height;
		x      = qMin(x, o.x);
		y      = qMin(y, o.y);
		z      = qMin(z, o.z);
		mx     = qMax(mx, omx);
		my     = qMax(my, omy);
		mz     = qMax(mz, omz);
		length = mx - x;
		width  = my - y;
		height = mz - z;
	}
	return *this;
}


QVector3D vectorFromString(const QString & str) {
	QTextStream s(const_cast<QString *>(&str), QIODevice::ReadOnly);
	QVector3D ret;
	float f(0.f);
	s >> f;
	ret.setX(f);
	s >> f;
	ret.setY(f);
	s >> f;
	ret.setZ(f);
	return ret;
}


QImage generateNoise(QSize sz) {
	QImage im(sz, QImage::Format_ARGB32);
	QRandomGenerator rg[2] = {1, 2};
	uint pcnt              = im.sizeInBytes() / sizeof(QRgb);
	QRgb * data            = (QRgb *)im.bits();
	for (uint i = 0; i < pcnt; ++i) {
		double r = rg[0].generateDouble();
		double a = rg[0].generateDouble() * M_2PI;
		double x = r * cos(a) / 2. + 0.5, y = r * sin(a) / 2. + 0.5;
		data[i] = qRgba(qRound(x * 256),
						qRound(y * 256),
						(rg[1].generate() % sz.width()) % 256,
						(rg[1].generate() % sz.height()) % 256); // argb GLSL
	}
	return im;
}
