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


void Loader3DS::init3DSMesh(GLObjectBase * o, const QVector<uint> & smooth) {
	QVector<GLfloat> &vertices(o->VBO().vertices()), &normals(o->VBO().normals()), &uvs(o->VBO().texcoords());
	QVector<Vector3d> &points(o->points), &puvws(o->puvws), fnormals;
	QVector<Vector3i> & faces(o->faces);
	Vector3d pos = Vector3d(o->pos());
	bool has_uv  = !puvws.isEmpty();
	Vector3i cf;
	Vector3d v0, v1, v2, cn0, cn1, cn2;
	fnormals.resize(faces.size());
	for (int i = 0; i < points.size(); ++i)
		points[i] -= pos;
	for (int i = 0; i < fnormals.size(); ++i) {
		cf          = faces[i];
		v0          = points[cf.p0];
		v1          = points[cf.p1];
		v2          = points[cf.p2];
		fnormals[i] = ((v1 - v0) * (v2 - v0)).normalized();
	}
	int fcnt = faces.size() * 3;
	vertices.resize(fcnt * 3);
	normals.resize(vertices.size());
	if (has_uv) uvs.resize(fcnt * 2);
	int ind = 0, induv = 0, ncnt0, ncnt1, ncnt2;
	uint csg;
	// qDebug() << faces.size();
	if (smooth.isEmpty()) {
		for (int i = 0; i < faces.size(); ++i) {
			cf            = faces[i];
			cn0           = fnormals[i];
			v0            = points[cf.p0];
			v1            = points[cf.p1];
			v2            = points[cf.p2];
			vertices[ind] = v0.x;
			normals[ind]  = cn0.x;
			++ind;
			vertices[ind] = v0.y;
			normals[ind]  = cn0.y;
			++ind;
			vertices[ind] = v0.z;
			normals[ind]  = cn0.z;
			++ind;
			vertices[ind] = v1.x;
			normals[ind]  = cn0.x;
			++ind;
			vertices[ind] = v1.y;
			normals[ind]  = cn0.y;
			++ind;
			vertices[ind] = v1.z;
			normals[ind]  = cn0.z;
			++ind;
			vertices[ind] = v2.x;
			normals[ind]  = cn0.x;
			++ind;
			vertices[ind] = v2.y;
			normals[ind]  = cn0.y;
			++ind;
			vertices[ind] = v2.z;
			normals[ind]  = cn0.z;
			++ind;
			if (has_uv) {
				uvs[induv] = puvws[cf.p0].x;
				++induv;
				uvs[induv] = puvws[cf.p0].y;
				++induv;
				uvs[induv] = puvws[cf.p1].x;
				++induv;
				uvs[induv] = puvws[cf.p1].y;
				++induv;
				uvs[induv] = puvws[cf.p2].x;
				++induv;
				uvs[induv] = puvws[cf.p2].y;
				++induv;
			}
		}
	} else {
		for (int i = 0; i < faces.size(); ++i) {
			cf  = faces[i];
			csg = smooth[i];
			v0  = points[cf.p0];
			v1  = points[cf.p1];
			v2  = points[cf.p2];

			cn0 = cn1 = cn2 = fnormals[i];
			ncnt0 = ncnt1 = ncnt2 = 1;
			for (int j = 0; j < faces.size(); ++j) {
				if (csg != smooth[j] || j == i) continue;
				if (faces[j].p0 == cf.p0 || faces[j].p1 == cf.p0 || faces[j].p2 == cf.p0 || points[faces[j].p0] == v0 ||
				    points[faces[j].p1] == v0 || points[faces[j].p2] == v0) {
					cn0 += fnormals[j];
					++ncnt0;
				}
				if (faces[j].p0 == cf.p1 || faces[j].p1 == cf.p1 || faces[j].p2 == cf.p1 || points[faces[j].p0] == v1 ||
				    points[faces[j].p1] == v1 || points[faces[j].p2] == v1) {
					cn1 += fnormals[j];
					++ncnt1;
				}
				if (faces[j].p0 == cf.p2 || faces[j].p1 == cf.p2 || faces[j].p2 == cf.p2 || points[faces[j].p0] == v2 ||
				    points[faces[j].p1] == v2 || points[faces[j].p2] == v2) {
					cn2 += fnormals[j];
					++ncnt2;
				}
			}
			cn0 /= ncnt0;
			cn1 /= ncnt1;
			cn2 /= ncnt2;
			vertices[ind] = v0.x;
			normals[ind]  = cn0.x;
			++ind;
			vertices[ind] = v0.y;
			normals[ind]  = cn0.y;
			++ind;
			vertices[ind] = v0.z;
			normals[ind]  = cn0.z;
			++ind;
			vertices[ind] = v1.x;
			normals[ind]  = cn1.x;
			++ind;
			vertices[ind] = v1.y;
			normals[ind]  = cn1.y;
			++ind;
			vertices[ind] = v1.z;
			normals[ind]  = cn1.z;
			++ind;
			vertices[ind] = v2.x;
			normals[ind]  = cn2.x;
			++ind;
			vertices[ind] = v2.y;
			normals[ind]  = cn2.y;
			++ind;
			vertices[ind] = v2.z;
			normals[ind]  = cn2.z;
			++ind;
			if (has_uv) {
				uvs[induv] = puvws[cf.p0].x;
				++induv;
				uvs[induv] = puvws[cf.p0].y;
				++induv;
				uvs[induv] = puvws[cf.p1].x;
				++induv;
				uvs[induv] = puvws[cf.p1].y;
				++induv;
				uvs[induv] = puvws[cf.p2].x;
				++induv;
				uvs[induv] = puvws[cf.p2].y;
				++induv;
			}
		}
	}
}


Material Loader3DS::materialByName(const QVector<Material> & materials, const QString & name) {
	foreach(const Material & m, materials)
		if (m.name == name) return m;
	return Material();
}


GLObjectBase * loadFrom3DSFile(const QString & filepath, float scale) {
	QFile f(filepath);
	if (!f.exists()) {
		qDebug() << "[Loader 3DS] Error: can`t open \"" + filepath + "\"";
		return nullptr;
	}
	f.open(QIODevice::ReadOnly);
	QDataStream stream(&f);
	QVector<Material> materials;
	QVector<uint> smooth;
	QVector<ushort> face_mats;
	GLObjectBase *root = new GLObjectBase(), *co = nullptr;
	Material mat;
	Loader3DS::Chunk cc;
	Loader3DS::Face face;
	Vector3d pos;
	QString str;
	ushort cnt;
	QString name;
	QByteArray ba;
	int cur_map = 0;
	float fl, fl1, matrix[3][3];
	uint col;
	root->setName(QFileInfo(f).baseName());
	while (!stream.atEnd()) {
		stream.readRawData((char *)&cc, sizeof(cc));
		switch (cc.id) {
		case LOADER_3DS_CHUNK_MAIN: /*qDebug() << "main" << cc.size;*/ break;
		case LOADER_3DS_CHUNK_OBJECTS: /*qDebug() << " objects" << cc.size;*/ break;
		case LOADER_3DS_CHUNK_OBJECT:
			if (co != nullptr) {
				Loader3DS::init3DSMesh(co, smooth);
				root->addChild(co);
			}
			co = new GLObjectBase();
			co->setName(readCharsUntilNull(stream));
			smooth.clear();
			// qDebug() << "  object" << co->name();
			break;
		case LOADER_3DS_CHUNK_MESH: /*qDebug() << "   mesh" << cc.size;*/ break;
		case LOADER_3DS_CHUNK_VERTLIST:
			stream.readRawData((char *)&cnt, sizeof(ushort));
			co->points.resize(cnt);
			// qDebug() << "    vertices" << cnt;
			for (int i = 0; i < cnt; ++i) {
				stream.readRawData((char *)&co->points[i].x, sizeof(float));
				stream.readRawData((char *)&co->points[i].y, sizeof(float));
				stream.readRawData((char *)&co->points[i].z, sizeof(float));
				co->points[i] *= scale;
			}
			break;
		case LOADER_3DS_CHUNK_FACELIST:
			stream.readRawData((char *)&cnt, sizeof(ushort));
			co->faces.resize(cnt);
			// qDebug() << "    faces" << cnt;
			for (int i = 0; i < cnt; ++i) {
				stream.readRawData((char *)&face, sizeof(Loader3DS::Face));
				co->faces[i].p0 = face.v0;
				co->faces[i].p1 = face.v1;
				co->faces[i].p2 = face.v2;
			}
			break;
		case LOADER_3DS_CHUNK_FACEMAT:
			name = readCharsUntilNull(stream);
			stream.readRawData((char *)&cnt, sizeof(ushort));
			face_mats.resize(cnt);
			for (int i = 0; i < cnt; ++i)
				stream.readRawData((char *)&(face_mats[i]), sizeof(ushort));
			// qDebug() << "   facemat name" << name << cnt;
			co->material().name = name;
			break;
		case LOADER_3DS_CHUNK_MAPLIST:
			stream.readRawData((char *)&cnt, sizeof(ushort));
			co->puvws.resize(cnt);
			// qDebug() << "    texcoords" << cnt;
			for (int i = 0; i < cnt; ++i) {
				stream.readRawData((char *)&co->puvws[i].x, sizeof(float));
				stream.readRawData((char *)&co->puvws[i].y, sizeof(float));
			}
			break;
		case LOADER_3DS_CHUNK_SMOOTH:
			cnt = co->faces.size();
			smooth.resize(cnt);
			// qDebug() << "    smooth" << cnt;
			for (int i = 0; i < cnt; ++i)
				stream.readRawData((char *)&smooth[i], sizeof(uint));
			break;
		case LOADER_3DS_CHUNK_TRMATRIX:
			// qDebug() << co->name();
			for (int i = 0; i < 3; ++i) {
				for (int j = 0; j < 3; ++j)
					stream.readRawData((char *)&(matrix[i][j]), sizeof(float));
				// qDebug() << matrix[i][0] << matrix[i][1] << matrix[i][2];
			}
			stream.readRawData((char *)&pos, sizeof(Vector3d));
			pos *= scale;
			// qDebug() << "pos =" << pos;
			co->setPos(pos.toQVector3D());
			break;
		case LOADER_3DS_CHUNK_LIGHT:
			// qDebug() << "   light" << cc.size;
			str = co->name();
			delete co;
			co = new Light();
			co->setName(str);
			stream.readRawData((char *)&pos, sizeof(Vector3d));
			pos *= scale;
			co->setPos(pos.toQVector3D());
			break;
		case LOADER_3DS_CHUNK_SPOTLIGHT:
			stream.readRawData((char *)&pos, sizeof(Vector3d));
			pos *= scale;
			globject_cast<Light *>(co)->light_type = Light::Cone;
			globject_cast<Light *>(co)->direction  = (pos.toQVector3D() - co->pos()).normalized();
			stream.readRawData((char *)&fl1, sizeof(float));
			stream.readRawData((char *)&fl, sizeof(float));
			globject_cast<Light *>(co)->angle_start = fl1;
			globject_cast<Light *>(co)->angle_end   = fl;
			// qDebug() << "spotlight" << globject_cast<Light * >(co)->direction << globject_cast<Light * >(co)->angle_spread;
			break;
		case LOADER_3DS_CHUNK_LIGHT_OFF:
			stream.skipRawData(cc.size - 6);
			co->hide();
			break;
		case LOADER_3DS_CHUNK_ATTENUATION_ON:
			stream.skipRawData(cc.size - 6);
			fl                                          = globject_cast<Light *>(co)->decay_end;
			// fl1 = globject_cast<Light * >(co)->decay_start;
			globject_cast<Light *>(co)->decay_quadratic = 4.f / fl;
			// qDebug() << "decay" << globject_cast<Light * >(co)->decay_quadratic;
			break;
		case LOADER_3DS_CHUNK_COLOR_F:
			stream.readRawData((char *)&pos, sizeof(Vector3d));
			co->setColor(QColor::fromRgbF(pos.x, pos.y, pos.z));
			// qDebug() << "    color_f" << co->color();
			break;
		case LOADER_3DS_CHUNK_COLOR_B:
			stream.readRawData((char *)&col, 3);
			co->setColor(QColor::fromRgb(((uchar *)&col)[0], ((uchar *)&col)[1], ((uchar *)&col)[2]));
			// qDebug() << "    color_b" << co->color();
			break;
		case LOADER_3DS_CHUNK_MULTIPLIER:
			stream.readRawData((char *)&fl, sizeof(float));
			globject_cast<Light *>(co)->intensity = fl;
			// qDebug() << "    multiplier" << fl;
			break;
		case LOADER_3DS_CHUNK_RANGE_START:
			stream.readRawData((char *)&fl, sizeof(float));
			globject_cast<Light *>(co)->decay_start = fl;
			// qDebug() << "    range start" << fl;
			break;
		case LOADER_3DS_CHUNK_RANGE_END:
			stream.readRawData((char *)&fl, sizeof(float));
			globject_cast<Light *>(co)->decay_end = fl;
			// qDebug() << "    range end" << fl;
			break;
		case LOADER_3DS_CHUNK_MATERIAL:
			// stream.skipRawData(cc.size - 6);
			if (!mat.name.isEmpty()) materials << mat;
			mat = Material();
			break;
		case LOADER_3DS_CHUNK_MATERIAL_NAME:
			mat.name = readCharsUntilNull(stream);
			// qDebug() << "matname" << mat.name;
			break;
		case LOADER_3DS_CHUNK_AMBIENT_COLOR:
			stream.skipRawData(cc.size - 9);
			stream.readRawData((char *)&col, 3);
			mat.color_self_illumination = QColor::fromRgb(((uchar *)&col)[0], ((uchar *)&col)[1], ((uchar *)&col)[2]);
			// qDebug() << "mat diffuse" << mat.color_diffuse;
			break;
		case LOADER_3DS_CHUNK_DIFFUSE_COLOR:
			stream.skipRawData(cc.size - 9);
			stream.readRawData((char *)&col, 3);
			mat.color_diffuse = QColor::fromRgb(((uchar *)&col)[0], ((uchar *)&col)[1], ((uchar *)&col)[2]);
			// qDebug() << "mat diffuse" << mat.color_diffuse;
			break;
		case LOADER_3DS_CHUNK_SPECULAR_COLOR:
			stream.skipRawData(cc.size - 9);
			stream.readRawData((char *)&col, 3);
			mat.color_specular = QColor::fromRgb(((uchar *)&col)[0], ((uchar *)&col)[1], ((uchar *)&col)[2]);
			// qDebug() << "mat diffuse" << mat.color_diffuse;
			break;
		case LOADER_3DS_CHUNK_TEXTURE_MAP: cur_map = LOADER_3DS_CHUNK_TEXTURE_MAP; break;
		case LOADER_3DS_CHUNK_BUMP_MAP: cur_map = LOADER_3DS_CHUNK_BUMP_MAP; break;
		case LOADER_3DS_CHUNK_REFLECTION_MAP: cur_map = LOADER_3DS_CHUNK_REFLECTION_MAP; break;
		case LOADER_3DS_CHUNK_MAP_FILENAME:
			name = readCharsUntilNull(stream);
			// qDebug() << "  mat map" << QString::number(cur_map, 16) << name;
			switch (cur_map) {
			case LOADER_3DS_CHUNK_TEXTURE_MAP: mat.map_diffuse.bitmap_path = name; break;
			case LOADER_3DS_CHUNK_BUMP_MAP: mat.map_normal.bitmap_path = name; break;
			}
			break;
		default: /*qDebug() << "???" << QString::number(cc.id, 16).rightJustified(4, '0') << cc.size;*/ stream.skipRawData(cc.size - 6);
		}
	}
	if (!mat.name.isEmpty()) materials << mat;
	foreach(const Material & m, materials)
		qDebug() << m.name;
	if (co != nullptr) {
		Loader3DS::init3DSMesh(co, smooth);
		root->addChild(co);
	}
	for (int i = 0; i < root->childCount(); ++i)
		root->child(i)->material() = Loader3DS::materialByName(materials, root->child(i)->material().name);
	qDebug() << "[Loader 3DS] Loaded" << root->childCount() << "objects from" << filepath;
	return root;
}
