/*
    QGL Mesh
    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 "glmesh.h"

#include "globject.h"

#include <QOpenGLExtraFunctions>
#include <QTime>

using namespace QGLEngineShaders;

// static int _count = 0;

Mesh::Mesh(GLenum geom_type_)
	: geom_type(geom_type_)
	, buffer_geom(GL_ARRAY_BUFFER, GL_STATIC_DRAW)
	, buffer_ind(GL_ELEMENT_ARRAY_BUFFER, GL_STATIC_DRAW) {
	hash_   = 0;
	changed = hash_changed = true;
	// qDebug() << "Mesh, now" << ++_count;
}


Mesh::~Mesh() {
	// qDebug() << "~Mesh, now" << --_count;
}


Mesh * Mesh::clone() {
	Mesh * c        = new Mesh();
	c->vertices_    = vertices_;
	c->normals_     = normals_;
	c->texcoords_   = texcoords_;
	c->triangles_   = triangles_;
	c->lines_       = lines_;
	c->geom_type    = geom_type;
	c->hash_        = hash_;
	c->hash_changed = hash_changed;
	// qDebug() << "clone VBO";
	return c;
}


void Mesh::init(QOpenGLExtraFunctions * f) {
	if (!isInit()) {
		buffer_geom.init(f);
		buffer_ind.init(f);
		changed = true;
	}
}


void Mesh::reinit() {
	buffer_geom.reinit();
	buffer_ind.reinit();
	QMapIterator<int, VertexObject *> it(vao_map);
	while (it.hasNext())
		it.next().value()->reinit();
	changed = true;
}


void Mesh::destroy(QOpenGLExtraFunctions * f) {
	buffer_geom.destroy(f);
	buffer_ind.destroy(f);
	QList<VertexObject *> vaol = vao_map.values();
	foreach(VertexObject * vao, vaol)
		vao->destroy(f);
	qDeleteAll(vao_map);
	vao_map.clear();
}


void Mesh::calculateNormals() {
	normals_.resize(vertices_.size());
	QVector3D dv1, dv2, n;
	foreach(const Vector3i & t, triangles_) {
		QVector3D & v0(vertices_[t.p0]);
		QVector3D & v1(vertices_[t.p1]);
		QVector3D & v2(vertices_[t.p2]);
		dv1 = v1 - v0, dv2 = v2 - v0;
		n              = QVector3D::crossProduct(dv1, dv2).normalized();
		normals_[t.p0] = n;
		normals_[t.p1] = n;
		normals_[t.p2] = n;
	}
}


void Mesh::calculateTangents() {
	if (vertices_.isEmpty() || texcoords_.isEmpty()) return;
	if (texcoords_.size() != vertices_.size()) return;
	tangents_.resize(vertices_.size());
	bitangents_.resize(vertices_.size());
	// qDebug() << "calculateBinormals" << vcnt << tcnt << vertices_.size() << texcoords_.size() << "...";
	QVector3D dv1, dv2;
	QVector2D dt1, dt2;
	QVector3D tan, bitan;
	foreach(const Vector3i & t, triangles_) {
		QVector3D & v0(vertices_[t.p0]);
		QVector3D & v1(vertices_[t.p1]);
		QVector3D & v2(vertices_[t.p2]);
		QVector2D & t0(texcoords_[t.p0]);
		QVector2D & t1(texcoords_[t.p1]);
		QVector2D & t2(texcoords_[t.p2]);
		dv1 = v1 - v0, dv2 = v2 - v0;
		dt1 = t1 - t0, dt2 = t2 - t0;
		tan               = (dv1 * dt2.y() - dv2 * dt1.y()).normalized();
		bitan             = (dv2 * dt1.x() - dv1 * dt2.x()).normalized();
		tangents_[t.p0]   = tan;
		tangents_[t.p1]   = tan;
		tangents_[t.p2]   = tan;
		bitangents_[t.p0] = bitan;
		bitangents_[t.p1] = bitan;
		bitangents_[t.p2] = bitan;
		// qDebug() << "  t" << t << vi << ti << dv1.toQVector3D() << "...";
	}
	// qDebug() << "calculateBinormals" << vcnt << tcnt << tangents_.size();
}


VertexObject * Mesh::vaoByType(int type) {
	VertexObject *& vao(vao_map[type]);
	if (!vao) {
		vao = new VertexObject();
	}
	return vao;
}


bool Mesh::rebuffer(QOpenGLExtraFunctions * f) {
	changed = false;
	if (vertices_.isEmpty()) return true;
	if (normals_.isEmpty()) calculateNormals();
	calculateTangents();
	vert_count = qMin(vertices_.size(), normals_.size());
	vert_count = qMin(vert_count, tangents_.size());
	vert_count = qMin(vert_count, bitangents_.size());
	vert_count = qMin(vert_count, texcoords_.size());
	data_.resize(vert_count);
	for (int i = 0; i < vert_count; ++i) {
		Vertex & v(data_[i]);
		v.pos       = vertices_[i];
		v.normal    = normals_[i];
		v.tangent   = tangents_[i];
		v.bitangent = bitangents_[i];
		v.tex       = texcoords_[i];
	}
	int gsize = data_.size() * sizeof(Vertex);
	int tsize = triangles_.size() * sizeof(Vector3i);
	int lsize = lines_.size() * sizeof(Vector2i);

	buffer_geom.bind(f);
	buffer_geom.resize(f, gsize);
	buffer_geom.load(f, data_.constData(), gsize);

	buffer_ind.bind(f);
	if (geom_type == GL_TRIANGLES) {
		buffer_ind.resize(f, tsize);
		buffer_ind.load(f, triangles_.constData(), tsize);
	} else {
		buffer_ind.resize(f, lsize);
		buffer_ind.load(f, lines_.constData(), lsize);
	}

	return !isEmpty();
}


void Mesh::draw(QOpenGLExtraFunctions * f, int count, int type) {
	if (isEmpty()) return;
	if (!isInit()) init(f);
	if (changed) rebuffer(f);
	// qDebug() << "draw" << geom_type << vert_count << count;

	VertexObject * vao = vaoByType(type);
	vao->bindBuffers(f, buffer_geom, buffer_ind);
	if (geom_type == GL_TRIANGLES)
		vao->draw(f, geom_type, triangles_.size() * 3, count);
	else
		vao->draw(f, geom_type, lines_.size() * 2, count);
}


void Mesh::clear() {
	vertices_.clear();
	normals_.clear();
	tangents_.clear();
	bitangents_.clear();
	texcoords_.clear();
	triangles_.clear();
	lines_.clear();
	data_.clear();
	changed = hash_changed = true;
}


void Mesh::loadObject(QOpenGLExtraFunctions * f, const Object & object, int type) {
	VertexObject * vao = vaoByType(type);
	vao->loadObject(f, object);
}


void Mesh::loadObjects(QOpenGLExtraFunctions * f, const QVector<Object> & objects, int type) {
	VertexObject * vao = vaoByType(type);
	vao->loadObjects(f, objects);
}


void Mesh::loadSelections(QOpenGLExtraFunctions * f, const QVector<uchar> & sels, int type) {
	VertexObject * vao = vaoByType(type);
	vao->loadSelections(f, sels);
}


uint Mesh::hash() const {
	if (hash_changed) {
		hash_changed = false;
		hash_        = qHashBits(vertices_.constData(), vertices_.size() * sizeof(QVector3D));
		hash_ ^= qHashBits(normals_.constData(), normals_.size() * sizeof(QVector3D));
		hash_ ^= qHashBits(texcoords_.constData(), texcoords_.size() * sizeof(QVector2D));
		hash_ ^= qHashBits(triangles_.constData(), triangles_.size() * sizeof(Vector3i));
		hash_ ^= qHashBits(lines_.constData(), lines_.size() * sizeof(Vector2i));
	}
	return hash_;
}


bool Mesh::isObjectsChanged(int type) const {
	return (const_cast<Mesh *>(this))->vaoByType(type)->isObjectsChanged();
}


bool Mesh::isSelectionChanged(int type) const {
	return (const_cast<Mesh *>(this))->vaoByType(type)->isSelectionChanged();
}


void Mesh::setObjectsChanged(int type, bool yes) {
	vaoByType(type)->setObjectsChanged(yes);
}


void Mesh::setSelectionChanged(int type, bool yes) {
	vaoByType(type)->setSelectionChanged(yes);
}


void Mesh::setAllObjectsChanged(bool yes) {
	QMapIterator<int, VertexObject *> it(vao_map);
	while (it.hasNext())
		it.next().value()->setObjectsChanged(yes);
}


void Mesh::setAllSelectionChanged(bool yes) {
	QMapIterator<int, VertexObject *> it(vao_map);
	while (it.hasNext())
		it.next().value()->setSelectionChanged(yes);
}


void Mesh::translatePoints(const QVector3D & dp) {
	QMatrix4x4 m;
	m.translate(dp);
	transformPoints(m);
}


void Mesh::scalePoints(const QVector3D & dp) {
	QMatrix4x4 m;
	m.scale(dp);
	transformPoints(m);
}


void Mesh::rotatePoints(const double & angle, const QVector3D & a) {
	QMatrix4x4 m;
	m.rotate(angle, a);
	transformPoints(m);
}


void Mesh::transformPoints(const QMatrix4x4 & mat) {
	if (vertices_.isEmpty()) return;
	int vcnt = vertices_.size(), ncnt = normals_.size();
	for (int i = 0; i < vcnt; ++i) {
		vertices_[i] = (mat * QVector4D(vertices_[i], 1)).toVector3D();
		if (i < ncnt) normals_[i] = (mat * QVector4D(normals_[i], 0)).toVector3D();
	}
	changed = hash_changed = true;
}


void Mesh::flipNormals() {
	if (vertices_.isEmpty()) return;
	for (int i = 0; i < triangles_.size(); ++i)
		piSwap(triangles_[i].p0, triangles_[i].p1);
	for (int i = 0; i < lines_.size(); ++i)
		piSwap(lines_[i].p0, lines_[i].p1);
	int ncnt = normals_.size();
	for (int i = 0; i < ncnt; ++i)
		normals_[i] = -normals_[i];
	changed = hash_changed = true;
}


void Mesh::append(const Mesh * m) {
	if (!m) return;
	if (m->isEmpty()) return;
	if (normals_.isEmpty()) calculateNormals();
	int vcnt = vertices_.size();
	vertices_.append(m->vertices_);
	normals_.append(m->normals_);
	texcoords_.append(m->texcoords_);
	QVector<Vector3i> tri = m->triangles_;
	for (int i = 0; i < tri.size(); ++i)
		tri[i] += vcnt;
	triangles_.append(tri);
	QVector<Vector2i> lin = m->lines_;
	for (int i = 0; i < lin.size(); ++i)
		lin[i] += vcnt;
	lines_.append(lin);
}


bool Mesh::saveToFile(const QString & filename) {
	if (filename.isEmpty()) return false;
	QFile f(filename);
	QByteArray ba;
	if (f.open(QFile::WriteOnly)) {
		QDataStream out(&ba, QFile::WriteOnly);
		out << vertices_ << normals_ << texcoords_ << triangles_ << lines_;
		ba = qCompress(ba);
		f.resize(0);
		f.write(ba);
		f.close();
		return true;
	}
	return false;
}


bool Mesh::loadFromFile(const QString & filename) {
	if (filename.isEmpty()) return false;
	QFile f(filename);
	QByteArray ba;
	if (f.open(QFile::ReadOnly)) {
		ba = f.readAll();
		if (ba.isEmpty()) return false;
		ba = qUncompress(ba);
		QDataStream in(ba);
		in >> vertices_ >> normals_ >> texcoords_ >> triangles_ >> lines_;
		changed = hash_changed = true;
		f.close();
		return !isEmpty();
	}
	return false;
}


Box3D Mesh::boundingBox() const {
	if (vertices_.isEmpty()) return Box3D();
	int vcnt = vertices_.size();
	// qDebug() << "calculateBinormals" << vcnt << tcnt << vertices_.size() << texcoords_.size() << "...";
	GLfloat mix, miy, miz, max, may, maz;
	QVector3D v0(vertices_[0]);
	mix = max = v0.x();
	miy = may = v0.y();
	miz = maz = v0.z();
	Box3D bound;
	for (int i = 1; i < vcnt; ++i) {
		const QVector3D & v(vertices_[i]);
		if (mix > v.x()) mix = v.x();
		if (max < v.x()) max = v.x();
		if (miy > v.y()) miy = v.y();
		if (may < v.y()) may = v.y();
		if (miz > v.z()) miz = v.z();
		if (maz < v.z()) maz = v.z();
	}
	bound.x      = mix;
	bound.y      = miy;
	bound.z      = miz;
	bound.length = max - mix;
	bound.width  = may - miy;
	bound.height = maz - miz;
	return bound;
}


QDataStream & operator<<(QDataStream & s, const Mesh * m) {
	ChunkStream cs;
	// qDebug() << "place VBO" << m.vertices_.size() << m.normals_.size() << m.texcoords_.size() << m.colors_.size() << "...";
	cs.add(1, m->vertices_).add(2, m->normals_).add(3, m->texcoords_).add(6, m->triangles_).add(7, m->lines_).add(10, int(m->geom_type));
	// qDebug() << "place VBO done" << cs.data().size() << "...";
	s << cs.data();
	return s;
}


QDataStream & operator>>(QDataStream & s, Mesh *& m) {
	m = new Mesh();
	ChunkStream cs(s);
	while (!cs.atEnd()) {
		switch (cs.read()) {
		case 1: cs.get(m->vertices_); break;
		case 2: cs.get(m->normals_); break;
		case 3: cs.get(m->texcoords_); break;
		case 6: cs.get(m->triangles_); break;
		case 7: cs.get(m->lines_); break;
		case 10: m->geom_type = cs.getData<int>(); break;
		}
	}
	m->changed = true;
	return s;
}
