/* ScummVM - Graphic Adventure Engine
 *
 * ScummVM is the legal property of its developers, whose names
 * are too numerous to list here. Please refer to the COPYRIGHT
 * file distributed with this source distribution.
 *
 * 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/>.
 *
 */

#include "watchmaker/3d/loader.h"
#include "common/stream.h"
#include "watchmaker/3d/geometry.h"
#include "watchmaker/3d/light.h"
#include "watchmaker/3d/math/llmath.h"
#include "watchmaker/3d/t3d_body.h"
#include "watchmaker/3d/t3d_mesh.h"
#include "watchmaker/game.h"
#include "watchmaker/ll/ll_mesh.h"
#include "watchmaker/ll/ll_system.h"
#include "watchmaker/renderer.h"
#include "watchmaker/t3d.h"
#include "watchmaker/types.h"
#include "watchmaker/utils.h"
#include "watchmaker/windows_hacks.h"
#include "watchmaker/work_dirs.h"

namespace Watchmaker {

#define T3DFILEVERSION  11

t3dV3F  CharCorrection;
t3dF32  CurFloorY;
int32  t3dCurTime = 900, t3dCurOliSet = 0;

t3dPathCamera::t3dPathCamera(Common::SeekableReadStream &stream) {
	NumCamera = stream.readByte();
	PathIndex = stream.readByte();
	Direction = stream.readByte();
}

t3dCAMERA::t3dCAMERA(Common::SeekableReadStream &stream) {
	Index = stream.readByte();

	Source = t3dV3F(stream) * SCALEFACTOR;
	Target = t3dV3F(stream) * SCALEFACTOR;

	t3dVectCopy(&MaxTarget, &Target);

//		Camera[camera].Fov=t3dReadReal();
	Fov = RADIANS_TO_DEGREE(stream.readFloatLE());  //FOV
	NearClipPlane = stream.readFloatLE() * SCALEFACTOR;
	FarClipPlane = stream.readFloatLE() * SCALEFACTOR;

	int numPaths = stream.readByte();
	CameraPaths.reserve(numPaths);
	for (int i = 0; i < numPaths; i++) {
		CameraPaths.push_back(t3dPathCamera(stream));
	}
}

t3dCAMERAPATH::t3dCAMERAPATH(Common::SeekableReadStream &stream) {
	int numPoints = stream.readSint16LE();
	CarrelloDist = stream.readSint32LE();

	PList.resize(numPoints);
	for (int j = 0; j < numPoints; j++) {
		PList[j].x = stream.readFloatLE() * SCALEFACTOR;
		PList[j].y = stream.readFloatLE() * SCALEFACTOR;
		PList[j].z = stream.readFloatLE() * SCALEFACTOR;
	}
}
void decodeLoaderFlags(uint32 flags) {
	warning("%d: T3D_GENERATESHADOWMAPS", flags & T3D_GENERATESHADOWMAPS);
	warning("%d: T3D_NOLIGHTMAPS", flags & T3D_NOLIGHTMAPS);
	warning("%d: T3D_NORECURSION", flags & T3D_NORECURSION);
	warning("%d: T3D_HALFTEXTURESIZE", flags & T3D_HALFTEXTURESIZE);
	warning("%d: T3D_FULLSCREEN", flags & T3D_FULLSCREEN);
	warning("%d: T3D_FASTRENDERING", flags & T3D_FASTRENDERING);
	warning("%d: T3D_OUTDOORLIGHTS", flags & T3D_OUTDOORLIGHTS);
	warning("%d: T3D_NOVOLUMETRICLIGHTS", flags & T3D_NOVOLUMETRICLIGHTS);
	warning("%d: T3D_NOBOUNDS", flags & T3D_NOBOUNDS);
	warning("%d: T3D_NOCAMERAS", flags & T3D_NOCAMERAS);
	warning("%d: T3D_NONEXCLUSIVEMOUSE", flags & T3D_NONEXCLUSIVEMOUSE);
	warning("%d: T3D_RECURSIONLEVEL1", flags & T3D_RECURSIONLEVEL1);
	warning("%d: T3D_SKY", flags & T3D_SKY);
	warning("%d: T3D_PRELOAD_RXT", flags & T3D_PRELOAD_RXT);
	warning("%d: T3D_STATIC_SET0", flags & T3D_STATIC_SET0);
	warning("%d: T3D_STATIC_SET1", flags & T3D_STATIC_SET1);
	warning("%d: T3D_NOSHADOWS", flags & T3D_NOSHADOWS);
	warning("%d: T3D_NOICONS", flags & T3D_NOICONS);
	warning("%d: T3D_NOSOUND", flags & T3D_NOSOUND);
	warning("%d: T3D_PRELOADBASE", flags & T3D_PRELOADBASE);
	warning("%d: T3D_NOMUSIC", flags & T3D_NOMUSIC);
	warning("%d: T3D_DEBUGMODE", flags & T3D_DEBUGMODE);
	warning("%d: T3D_FASTFILE", flags & T3D_FASTFILE);
	warning("%d: T3D_HIPOLYPLAYERS", flags & T3D_HIPOLYPLAYERS);
	warning("%d: T3D_HIPOLYCHARACTERS", flags & T3D_HIPOLYCHARACTERS);
}

Common::String constructPath(const Common::String &prefix, const Common::String &filename, const char *suffix) {
	Common::String name = prefix + filename;
	uint16 len = name.size();
	if (suffix != nullptr) {
		uint16 suffixLen = strlen(suffix);
		name = name.substr(0, len - suffixLen) + suffix;
		assert(suffixLen == 3);
	}
	return Common::String(Common::move(name));
}

class RoomManagerImplementation : public RoomManager {
	WGame *_game;
	#define MAX_LOADED_FILES    100
	RecStruct LoadedFiles[MAX_LOADED_FILES];
	uint16  NumLoadedFiles = 0;
public:
	RoomManagerImplementation(WGame *game) : _game(game) {}
	#define MAX_T3D_LOADLIST_ITEMS  50
	struct _t3dLOADLIST {
		Common::String pname = {};
		uint32 LoaderFlags = 0;
		t3dMESH *m = nullptr;
	};
	_t3dLOADLIST t3dLoadList[MAX_T3D_LOADLIST_ITEMS] = {};

	void addToLoadList(t3dMESH *m, const Common::String &pname, uint32 _LoaderFlags) override {
		if (!pname.empty()) {
			int32 a;
			for (a = 0; a < MAX_T3D_LOADLIST_ITEMS; a++) {
				if (t3dLoadList[a].pname.empty()) {
					t3dLoadList[a].LoaderFlags = _LoaderFlags;
					t3dLoadList[a].m = m;
					t3dLoadList[a].pname = pname;
					break;
				}
			}

			if (a >= MAX_T3D_LOADLIST_ITEMS)
				warning("Cannot add %s to LoadList", pname.c_str());
		} else {
			warning("Invalid parameters invoking AddToLoadList()");
			warning("Mesh (%s), pname %s", m->name.c_str(), pname.c_str());
		}
	}

	_t3dLOADLIST* getFromLoadList() {
		for (int a = 0; a < MAX_T3D_LOADLIST_ITEMS; a++) {
			if (!t3dLoadList[a].pname.empty())
				return &t3dLoadList[a];
		}

		return nullptr;
	}

	t3dBODY *getRoomIfLoaded(const Common::String &roomname) override {
		t3dBODY *t = nullptr;
		for (int i = 0; i < NumLoadedFiles; i++)
			if ((LoadedFiles[i].b != nullptr) && LoadedFiles[i].b->name.equalsIgnoreCase(roomname))
				t = LoadedFiles[i].b;
		return t;
	}



	t3dBODY* loadRoom(const Common::String &pname, t3dBODY *b, uint16 *NumBody, uint32 LoaderFlags) override;
	t3dBODY* loadSingleRoom(const Common::String &pname, uint16 *NumBody, uint32 LoaderFlags);
	void hideRoomMeshesMatching(const Common::String &pname) override {
		for (int i = 0; i < NumLoadedFiles; i++)
			if (LoadedFiles[i].b)
				if (LoadedFiles[i].b->name.equalsIgnoreCase(pname)) {
					HideRoomMeshes(_game->init, LoadedFiles[i].b);
				}
	}
	Common::Array<t3dBODY*> getLoadedFiles() override {
		// TODO: This won't need to be a copy if we maintain a Common::Array only containing the valid ones.
		Common::Array<t3dBODY*> files;
		for (int i = 0; i < NumLoadedFiles; i++)
			if (LoadedFiles[i].b)
				files.push_back(LoadedFiles[i].b);
		return files;
	}
	t3dBODY *checkIfAlreadyLoaded(const Common::String &Name) override {
		if (Name.empty()) return nullptr;

		for (uint16 i = 0; i < NumLoadedFiles; i++) {
			if ((LoadedFiles[i].b != nullptr) && /*(LoadedFiles[i].Name != nullptr) &&*/ (!LoadedFiles[i].name.empty()))
				if (LoadedFiles[i].name.equalsIgnoreCase(Name))
					return LoadedFiles[i].b;
		}

		return nullptr;
	}

	t3dMESH *linkMeshToStr(Init &init, const Common::String &str) override {
		if (str.empty()) return nullptr;

		//	Cerca tra le camere
		if (str.equalsIgnoreCase("camera"))
			return &init._globals._invVars.CameraDummy;
		//	Cerca tra i personaggi
		for (uint16 i = 0; i < T3D_MAX_CHARACTERS; i++)
			if ((Character[i]) && (str.equalsIgnoreCase(init.Obj[i].getMeshLink(0))))
				return Character[i]->Mesh;
		//	Cerca nelle stanze caricate
		for (uint16 i = 0; i < NumLoadedFiles; i++) {
			if (LoadedFiles[i].b)
				for (uint16 j = 0; j < LoadedFiles[i].b->NumMeshes(); j++) {
					if (str.equalsIgnoreCase(LoadedFiles[i].b->MeshTable[j].name))
						return &LoadedFiles[i].b->MeshTable[j];
				}
		}

		return nullptr;
	}
	void releaseBody(const Common::String &name, const Common::String &altName) override {
		for (int j = 0; j < NumLoadedFiles; j++) {
			if (LoadedFiles[j].name.equalsIgnoreCase(name) || LoadedFiles[j].name.equalsIgnoreCase(altName)) {
				t3dReleaseBody(LoadedFiles[j].b);
				LoadedFiles[j].b = nullptr;
				break;
			}
		}
	}

	void releaseLoadedFiles(uint32 exceptFlag) override {
		for (int i = 0; i < NumLoadedFiles; i++) {
			if (LoadedFiles[i].b && !(LoadedFiles[i].Flags & exceptFlag)) {
				t3dReleaseBody(LoadedFiles[i].b);
				LoadedFiles[i] = RecStruct(); // TODO: Deduplicate.
			}
		}
	}
private:
	void loadFromList();
};

RoomManager *RoomManager::create(WGame *game) {
	return new RoomManagerImplementation(game);
}

/* -----------------10/06/99 16.04-------------------
 *                  t3dLoadRoom
 * --------------------------------------------------*/
t3dBODY* RoomManagerImplementation::loadRoom(const Common::String &pname, t3dBODY *b, uint16 *NumBody, uint32 _LoaderFlags) {
	warning("t3dLoadRoom(%s, b, %d, %d)", pname.c_str(), *NumBody, _LoaderFlags);
	struct _t3dLOADLIST *l;
	t3dBODY *r = nullptr;
	t3dBODY *rez = nullptr;

	// reset everything that was previously in the load list
	for (int i = 0; i < MAX_T3D_LOADLIST_ITEMS; i++) {
		t3dLoadList[i] = _t3dLOADLIST();
	}

	// Add the base stanza to the upload list
	addToLoadList(nullptr, pname, _LoaderFlags);

	while ((l = getFromLoadList())) {
		uint16 num = 0;
		if (l->m) {
			if ((rez = _vm->_roomManager->checkIfAlreadyLoaded(l->pname)))
				l->m->PortalList = rez;
			else {
//				if (l->m->Flags&T3D_MESH_PREPROCESSPORTAL)
//					body=l->m->PortalList = t3dLoadSingleRoom( l->pname, l->m->PortalList, &num, (l->LoaderFlags|T3D_HALFTEXTURESIZE) );
//				else
				// TODO: This should increase some refcount on the PortalList
				warning("TODO: Handle refcounts on PortalList");
				l->m->PortalList = loadSingleRoom(l->pname, &num, l->LoaderFlags);
			}
		} else
			r = loadSingleRoom(l->pname, NumBody, l->LoaderFlags);

		*l = _t3dLOADLIST();
	}

	if (!(_LoaderFlags & T3D_NORECURSION)) {
		for (uint16 i = 0; i < NumLoadedFiles; i++)
			if (LoadedFiles[i].b)
				t3dCalcRejectedMeshFromPortal(LoadedFiles[i].b);
	}

	warning("Room loaded");
	return r;
}

t3dBODY* RoomManagerImplementation::loadSingleRoom(const Common::String &_pname, uint16 *NumBody, uint32 _LoaderFlags) {
	//warning("t3dLoadSingleRoom(workDirs, %s, b, %d, %d)", _pname, *NumBody, _LoaderFlags);
	//decodeLoaderFlags(_LoaderFlags);
	Common::String pname(_pname);

	WorkDirs &workdirs = _game->workDirs;

	if (pname.equalsIgnoreCase("r1c.t3d"))
		if (((t3dCurTime >= 1300) && (t3dCurTime <= 1310)) || (t3dCurTime >= 1800))          //se viene cambiato l'orario cambiarlo anche in UpdateRoomVis...
			pname = "r1c-notte.t3d";
	if (pname.equalsIgnoreCase("r15.t3d"))
		if (((t3dCurTime >= 1300) && (t3dCurTime <= 1310)) || (t3dCurTime >= 1800))
			pname = "r15-notte.t3d";

	auto name = constructPath(workdirs._t3dDir, pname);

	//warning("t3dLoadSingleRoom opening(%s)", name.c_str());
	auto stream = openFile(name);
	if (!(stream)) { // Apre file
		warning("t3dLoadSingleRoom: Failed to open(%s)", name.c_str());
		return nullptr;
	}

	if (*NumBody != 0) {
		// TODO: This currently means that we never free the dependant bodies.
		warning("Loading a dependant body, should also be deleted alongside the base body");
	}
	t3dBODY *b = new t3dBODY();

	//warning("Loading %s ...", name.c_str());
	*b = t3dBODY(); // Azzera Body

	uint16 fileVersion = stream->readByte();
	if (fileVersion != T3DFILEVERSION) {                                                   // Controlla la versione del file
		warning("%s file incompatible: current version: %d.\tFile version: %d", name.c_str(), T3DFILEVERSION, fileVersion);
		delete b;
		return nullptr;
	}

	{
		uint16 j = 1;
		while (j < MAX_LOADED_FILES && LoadedFiles[j].b != nullptr) {
			j++;
		}
		if (j >= MAX_LOADED_FILES) {
			warning("Too many t3d files loaded!");
			delete b;
			return nullptr;
		}
		if ((j + 1) > NumLoadedFiles)
			NumLoadedFiles = j + 1;

		LoadedFiles[j].name = _pname;                              // Aggiunge il file alla lista
		LoadedFiles[j].Flags = LoaderFlags;                        // Aggiunge Flags alla lista
		LoadedFiles[j].b = b;                                      // Aggiunge Body alla lista
		j = 0;
	}
	b->loadFromStream(*_game, pname, *stream, _LoaderFlags);

	return b;
}

t3dParticle::t3dParticle(Common::SeekableReadStream &stream) {
	t3dF32  difR1, difG1, difB1;
	t3dF32  difR2, difG2, difB2;
	Num = (uint32)stream.readFloatLE();
	Lung = stream.readFloatLE();
	Size = stream.readFloatLE();
	Seg1 = stream.readFloatLE();
	Seg2 = stream.readFloatLE();
	Dim1 = stream.readFloatLE() / 1000.0f;
	Dim2 = stream.readFloatLE() / 1000.0f;
	Speed = stream.readFloatLE() / 10.0f;
	Speed1 = stream.readFloatLE() / 10.0f;
	Speed2 = stream.readFloatLE() / 10.0f;
	Caos = stream.readFloatLE() / 10;
	Caos1 = stream.readFloatLE() / 10;
	Caos2 = stream.readFloatLE() / 10;
	Delay = (uint32)stream.readFloatLE();
	OR1 = (uint8)stream.readSint32LE();
	R1 = (uint8)stream.readSint32LE();
	G1 = (uint8)stream.readSint32LE();
	B1 = (uint8)stream.readSint32LE();
	R2 = (uint8)stream.readSint32LE();
	G2 = (uint8)stream.readSint32LE();
	B2 = (uint8)stream.readSint32LE();
	R3 = (uint8)stream.readSint32LE();
	G3 = (uint8)stream.readSint32LE();
	B3 = (uint8)stream.readSint32LE();
	Type = (uint8)stream.readSint32LE();

#ifndef WMGEN
	ParticleIndex = t3dCreateSmokeParticle(Num,
	                                       Type,
	                                       OR1);
#endif
	difR1 = (R2 - R1) / (Seg1 / Speed1);
	difG1 = (G2 - G1) / (Seg1 / Speed1);
	difB1 = (B2 - B1) / (Seg1 / Speed1);
	difR2 = (R3 - R2) / (Seg2 / Speed2);
	difG2 = (G3 - G2) / (Seg2 / Speed2);
	difB2 = (B3 - B2) / (Seg2 / Speed2);
	R2 = difR1;
	G2 = difG1;
	B2 = difB1;
	R3 = difR2;
	G3 = difG2;
	B3 = difB2;
}

/* -----------------24/05/00 10.24-------------------
 *                  t3dPrecalcLight
 * --------------------------------------------------*/
void t3dPrecalcLight(t3dBODY *b, uint8 *sun) {
	t3dV3F  tmp, l, *normal;
	t3dVERTEX vv;
	t3dF32  nlight;
	int32  k, aa, rr, gg, bb;
	uint32 i, j, cv;

	for (i = 0, cv = 0; i < b->NumMeshes(); i++) {                                            // Si ripassa tutte le mesh
		t3dMESH &Mesh = b->MeshTable[i];
#ifndef WMGEN
		Mesh.VBptr = Mesh.VertexBuffer;
#endif
		for (j = 0; j < Mesh.NumVerts; j++, cv++) {                                                // Si passa tutti i vertici
			rr = RGBA_GETRED(Mesh.VBptr[j].diffuse);
			gg = RGBA_GETGREEN(Mesh.VBptr[j].diffuse);
			bb = RGBA_GETBLUE(Mesh.VBptr[j].diffuse);
			aa = RGBA_GETALPHA(Mesh.VBptr[j].diffuse);
			tmp.x = Mesh.VBptr[j].x;
			tmp.y = Mesh.VBptr[j].y;
			tmp.z = Mesh.VBptr[j].z;
			for (k = 0; k < (int)b->NumLights(); k++) {                                           // Si passa tutte le luci
				if ((b->LightTable[k].Type & T3D_LIGHT_REALTIME) || (b->LightTable[k].Type & T3D_LIGHT_FLARE) || !(b->LightTable[k].Type & T3D_LIGHT_LIGHTON)) continue;

				t3dVectSub(&l, &b->LightTable[k].Source, &tmp);                                  // Calcola vettore luce->vertice
				t3dVectNormalize(&l);                                                            // lo normalizza

				normal = &Mesh.NList[j]->n;                                                     // Calcola normale
				if ((nlight = t3dVectDot(normal, &l)) >= 0) {
					if (LightVertex(&vv, &tmp, &b->LightTable[k])) {
						if ((sun) && (!sun[cv]) && (b->LightTable[k].Type & T3D_LIGHT_SUN)) {
							rr += t3dFloatToInt(vv.r * nlight * 0.50f);
							gg += t3dFloatToInt(vv.g * nlight * 0.35f);
							bb += t3dFloatToInt(vv.b * nlight * 0.27f);
						} else {
							rr += t3dFloatToInt(vv.r * nlight);
							gg += t3dFloatToInt(vv.g * nlight);
							bb += t3dFloatToInt(vv.b * nlight);
						}
					}
				}
			}
			if (rr < 0) rr = 0;
			if (gg < 0) gg = 0;
			if (bb < 0) bb = 0;
			if (rr > 255) rr = 255;
			if (gg > 255) gg = 255;
			if (bb > 255) bb = 255;

			Mesh.VBptr[j].diffuse = RGBA_MAKE(rr, gg, bb, aa);
		}
#ifndef WMGEN
		Mesh.VBptr = nullptr;
#endif
	}
}


/* -----------------10/06/99 16.06-------------------
 *                      t3dLoadSky
 * --------------------------------------------------*/
void t3dLoadSky(WGame &game, t3dBODY * /*body*/) {
	t3dF32  Skyminx, Skyminy, Skyminz, Skymaxx, Skymaxy, Skymaxz;
	uint16  n = 0, i;
	gVertex *gv;
//	t3dF32   Tile=1.5f;
	t3dF32  div;

	if (!(t3dSky = _vm->_roomManager->loadRoom("sky.t3d", t3dSky, &n, T3D_NORECURSION | T3D_NOLIGHTMAPS | T3D_NOVOLUMETRICLIGHTS | T3D_NOCAMERAS | T3D_NOBOUNDS | T3D_STATIC_SET0))) {
		warning("Error during t3dLoadRoom: Sky not loaded");
	}

	GetBoundaries(t3dSky, &Skyminx, &Skyminy, &Skyminz, &Skymaxx, &Skymaxy, &Skymaxz);

	for (i = 0; i < t3dSky->NumMeshes(); i++) {
		gv = t3dSky->MeshTable[i].VertexBuffer;
		for (n = 0; n < t3dSky->MeshTable[i].NumVerts; n++, gv++) {
			gv->x -= Skyminx + ((Skymaxx - Skyminx) / 2.0f);
			gv->y -= Skyminy + ((Skymaxy - Skyminy) / 2.0f);
			gv->z -= Skyminz + ((Skymaxz - Skyminz) / 2.0f);
			div = (t3dF32)sqrt(gv->x * gv->x + gv->y * gv->y + gv->z * gv->z);
			gv->x /= div;
			gv->y /= div;
			gv->z /= div;

			gv->x *= 15000;
			gv->y *= 500;
			gv->z *= 15000;

			gv->u1 *= 1.0f;
			gv->v1 *= 1.0f;
		}

		t3dSky->MeshTable[0].Radius = 15000.0f * 2;
	}

	for (n = 0; n < t3dSky->NumNormals; n++) {
		t3dSky->NList[n]->dist *= 15000.0f;
	}

	for (n = 0; n < t3dSky->NumMaterials(); n++) {
		t3dSky->MatTable[n]->addProperty(T3D_MATERIAL_SKY);
		t3dSky->MatTable[n]->addProperty(T3D_MATERIAL_NOLIGHTMAP);
	}
}

/* -----------------02/08/99 15.40-------------------
 *                  t3dAddVertexBuffer
 * --------------------------------------------------*/
Common::SharedPtr<VertexBuffer> t3dAddVertexBuffer(t3dBODY *b, uint32 numv) {
	return b->addVertexBuffer();
}

/* -----------------10/06/99 16.03-------------------
 *              t3dOptimizeMaterialList
 * --------------------------------------------------*/
void t3dOptimizeMaterialList(t3dBODY *b) {
	for (uint32 i = 0; i < b->NumMaterials(); i++) {                                              // Scorre tutti materilai di un body
		MaterialPtr Mat = b->MatTable[i];
		if ((Mat == nullptr) || /*(!Mat->Texture->Name) ||*/ (Mat->Movie) || (Mat->hasFlag(T3D_MATERIAL_MOVIE))) // Se non esiste o non ha texture
			continue;                                                                           // esce

		for (uint32 j = 0; j < b->NumMaterials(); j++) {                                       // Cerca materiali uguali
			MaterialPtr CurMat = b->MatTable[j];
			if (Mat == CurMat)
				continue;

			if (CurMat == nullptr /*|| (!CurMat->Texture->Name)*/)
				continue;

			if (Mat->Texture->name.equalsIgnoreCase(CurMat->Texture->name)) {                      // Se ha lo setsso nome di texture
				//warning("TODO: Implement Material-merging");
				// This is currently broken.

				Mat = rMergeMaterial(Mat, CurMat);                                          // Unisce i due materiali
				for (uint32 k = 0; k < b->NumMeshes(); k++) {                                       // Aggiorna in tutte le mesh id materiale
					auto &m = b->MeshTable[k];
					for (int q = 0; q < m.NumFaces(); q++) {
						auto &f = m.FList[q];
						if (f.getMaterialIndex() == j)
							f.setMaterialIndex(i);
					}
				}
				warning("Deduplicating: %s (%d v %d", Mat->Texture->name.c_str(), i, j);
				b->MatTable[j] = nullptr; // TODO: This should probably happen in rMergeMaterial
			}
		}
	}

	// TODO: The optimization leaves a bunch of materials as nullptr, we need to update all the
	// references to them. Currently we do this by subtracting 1 from all references that were above
	// a removed material. This works, but isn't really optimal.
	//int subtract = 0;
	for (uint32 i = 0; i < b->NumMaterials(); i++) {
		if (!b->MatTable[i]) {
			b->MatTable.remove_at(i);
			//subtract++;
			for (uint32 k = 0; k < b->NumMeshes(); k++) {
				auto &m = b->MeshTable[k];
				for (int q = 0; q < m.NumFaces(); q++) {
					auto &f = m.FList[q];
					if (f.getMaterialIndex() >= i) {
						f.setMaterialIndex(f.getMaterialIndex() - 1);
					}
				}
			}
			i--;
		}
	}
}

/* -----------------30/07/99 10.55-------------------
 *              t3dFinalizeMaterialList
 * --------------------------------------------------*/
void t3dFinalizeMaterialList(t3dBODY *b) {
	for (uint32 i = 0; i < b->NumMeshes(); i++) {
		t3dMESH &Mesh = b->MeshTable[i];
#ifndef WMGEN
		Mesh.VBptr = Mesh.VertexBuffer;
#endif
		for (uint32 j = 0; j < Mesh.NumFaces(); j++) {
			t3dFACE &Face = Mesh.FList[j];
			MaterialPtr Mat = Face.getMaterial();
			if (Face.lightmap) {
				Mat = nullptr;
				for (const auto &material : Face.getMaterial()->AddictionalMaterial) {
					if (material->Texture->ID == Face.lightmap->Texture->ID) {
						Mat = material;
						break;
					}
				}
				if (Mat == nullptr) {
					warning("%s: Can't find Lightmap Sub-Material!", Mesh.name.c_str());
					warning("%d %d", Face.getMaterial()->NumAddictionalMaterial, Face.lightmap->Texture->ID);
					for (const auto &material : Face.getMaterial()->AddictionalMaterial) {
						warning("%d", material->Texture->ID);
					}
					continue;
				}
			}

			uint32 k = 0;
			for (k = 0; k < (uint32)Mat->NumAllocatedMesh; k++)
				if (Mat->FlagsList[k] == &Mesh.Flags)
					break;

			if (k >= (uint32)Mat->NumAllocatedMesh) {
				Mat->FlagsList.push_back(&Mesh.Flags);
				Mat->NumAllocatedMesh++;
				Mesh.Flags |= T3D_MESH_UPDATEVB;
			}
			Mesh.Flags |= T3D_MESH_UPDATEVB;

			for (uint32 h = 0; h < 3; h++) {
				for (k = 0; k < (uint32)Mat->NumAllocatedVerts(); k++)
					if (Mat->VertsList[k] == &Mesh.VBptr[Face.VertexIndex[h]])
						break;

				if (k >= (uint32)Mat->NumAllocatedVerts()) {
					Mat->VertsList.push_back(&Mesh.VBptr[Face.VertexIndex[h]]);
				}
				assert(k < Mat->VertsList.size());
				Face.setMatVertexIndex(h, k);
			}

		}
#ifndef WMGEN
		Mesh.VBptr = nullptr;
#endif
	}

	for (uint32 i = 0; i < b->NumMaterials(); i++) {
		auto &Mat = b->MatTable[i];
		if (!Mat) {
			warning("nullptr");
		}
		Mat->VBO = b->addVertexBuffer(); // t3dAddVertexBuffer(b, Mat->NumAllocatedVerts);
		for (int j = 0; j < Mat->NumAddictionalMaterial; j++)
			Mat->AddictionalMaterial[j]->VBO = t3dAddVertexBuffer(b, Mat->AddictionalMaterial[j]->NumAllocatedVerts());
	}
}

} // End of namespace Watchmaker
