//
//---------------------------------------------------------------------------
//
// Copyright(C) 2018 Kevin Caccamo
// All rights reserved.
//
// 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 "filesystem.h"
#include "model_obj.h"
#include "texturemanager.h"
#include "modelrenderer.h"
#include "printf.h"
#include "textureid.h"

/**
 * Load an OBJ model
 *
 * @param fn The path to the model file
 * @param lumpnum The lump index in the wad collection
 * @param buffer The contents of the model file
 * @param length The size of the model file
 * @return Whether or not the model was parsed successfully
 */
bool FOBJModel::Load(const char* fn, int lumpnum, const char* buffer, int length)
{
	FString objName = fileSystem.GetFileFullPath(lumpnum);
	FString objBuf(buffer, length);

	// Do some replacements before we parse the OBJ string
	{
		// Ensure usemtl statements remain intact
		TArray<FString> mtlUsages;
		TArray<long> mtlUsageIdxs;
		long bpos = 0, nlpos = 0, slashpos = 0;
		while (1)
		{
			bpos = objBuf.IndexOf("\nusemtl", bpos);
			if (bpos == -1) break;
			slashpos = objBuf.IndexOf('/', bpos);
			nlpos = objBuf.IndexOf('\n', ++bpos);
			if (slashpos > nlpos || slashpos == -1)
			{
				continue;
			}
			if (nlpos == -1)
			{
				nlpos = (long)objBuf.Len();
			}
			FString lineStr(objBuf.GetChars() + bpos, nlpos - bpos);
			mtlUsages.Push(lineStr);
			mtlUsageIdxs.Push(bpos);
		}

		// Replace forward slashes with percent signs so they aren't parsed as line comments
		objBuf.ReplaceChars('/', *newSideSep);
		char* wObjBuf = objBuf.LockBuffer();

		// Substitute broken usemtl statements with old ones
		for (size_t i = 0; i < mtlUsages.Size(); i++)
		{
			bpos = mtlUsageIdxs[i];
			nlpos = objBuf.IndexOf('\n', bpos);
			if (nlpos == -1)
			{
				nlpos = (long)objBuf.Len();
			}
			memcpy(wObjBuf + bpos, mtlUsages[i].GetChars(), nlpos - bpos);
		}

		bpos = 0;
		// Find each OBJ line comment, and convert each to a C-style line comment
		while (1)
		{
			bpos = objBuf.IndexOf('#', bpos);
			if (bpos == -1) break;
			if (objBuf[(unsigned int)bpos + 1] == '\n')
			{
				wObjBuf[bpos] = ' ';
			}
			else
			{
				wObjBuf[bpos] = '/';
				wObjBuf[bpos+1] = '/';
			}
			bpos += 1;
		}
		wObjBuf = nullptr;
		objBuf.UnlockBuffer();
	}
	sc.OpenString(objName, objBuf);

	FTextureID curMtl = FNullTextureID();
	OBJSurface *curSurface = nullptr;
	unsigned int aggSurfFaceCount = 0;
	unsigned int curSurfFaceCount = 0;
	unsigned int curSmoothGroup = 0;

	while(sc.GetString())
	{
		if (sc.Compare("v")) // Vertex
		{
			ParseVector<FVector3, 3>(this->verts);
		}
		else if (sc.Compare("vn")) // Vertex normal
		{
			ParseVector<FVector3, 3>(this->norms);
		}
		else if (sc.Compare("vt")) // UV Coordinates
		{
			ParseVector<FVector2, 2>(this->uvs);
		}
		else if (sc.Compare("usemtl"))
		{
			// Get material name and try to load it
			sc.MustGetString();

			curMtl = LoadSkin("", sc.String);
			if (!curMtl.isValid())
			{
				// Relative to model file path?
				curMtl = LoadSkin(fn, sc.String);
			}

			if (!curMtl.isValid())
			{
				sc.ScriptMessage("Material %s (#%u) not found.", sc.String, surfaces.Size());
			}

			// Build surface...
			if (curSurface == nullptr)
			{
				// First surface
				curSurface = new OBJSurface(curMtl);
			}
			else
			{
				if (curSurfFaceCount > 0)
				{
					// Add previous surface
					curSurface->numFaces = curSurfFaceCount;
					curSurface->faceStart = aggSurfFaceCount;
					surfaces.Push(*curSurface);
					delete curSurface;
					// Go to next surface
					curSurface = new OBJSurface(curMtl);
					aggSurfFaceCount += curSurfFaceCount;
				}
				else
				{
					curSurface->skin = curMtl;
				}
			}
			curSurfFaceCount = 0;
		}
		else if (sc.Compare("f"))
		{
			FString sides[4];
			OBJFace face;
			for (int i = 0; i < 3; i++)
			{
				// A face must have at least 3 sides
				sc.MustGetString();
				sides[i] = sc.String;
				if (!ParseFaceSide(sides[i], face, i)) return false;
			}
			face.sideCount = 3;
			if (sc.GetString())
			{
				if (!sc.Compare("f") && FString(sc.String).IndexOfAny("-0123456789") == 0)
				{
					sides[3] = sc.String;
					face.sideCount += 1;
					if (!ParseFaceSide(sides[3], face, 3)) return false;
				}
				else
				{
					sc.UnGet(); // No 4th side, move back
				}
			}
			face.smoothGroup = curSmoothGroup;
			faces.Push(face);
			curSurfFaceCount += 1;
		}
		else if (sc.Compare("s"))
		{
			sc.MustGetString();
			if (sc.Compare("off"))
			{
				curSmoothGroup = 0;
			}
			else
			{
				sc.UnGet();
				sc.MustGetNumber();
				curSmoothGroup = sc.Number;
				hasSmoothGroups = hasSmoothGroups || curSmoothGroup > 0;
			}
		}
	}
	sc.Close();

	if (curSurface == nullptr)
	{ // No valid materials detected
		FTextureID dummyMtl = LoadSkin("", "-NOFLAT-"); // Built-in to GZDoom
		curSurface = new OBJSurface(dummyMtl);
	}
	curSurface->numFaces = curSurfFaceCount;
	curSurface->faceStart = aggSurfFaceCount;
	surfaces.Push(*curSurface);
	delete curSurface;

	if (uvs.Size() == 0)
	{ // Needed so that OBJs without UVs can work
		uvs.Push(FVector2(0.0, 0.0));
	}

	return true;
}

/**
 * Parse an x-Dimensional vector
 *
 * @tparam T A subclass of TVector2 to be used
 * @tparam L The length of the vector to parse
 * @param[out] array The array to append the parsed vector to
 */
template<typename T, size_t L> void FOBJModel::ParseVector(TArray<T> &array)
{
	T vec;
	for (unsigned axis = 0; axis < L; axis++)
	{
		sc.MustGetFloat();
		vec[axis] = (float)sc.Float;
	}
	array.Push(vec);
}

/**
 * Parse a side of a face
 *
 * @param[in] sideStr The side definition string
 * @param[out] face The face to assign the parsed side data to
 * @param sidx The 0-based index of the side
 * @return Whether or not the face side was parsed successfully
 */
bool FOBJModel::ParseFaceSide(const FString &sideStr, OBJFace &face, int sidx)
{
	OBJFaceSide side;
	int origIdx;
	if (sideStr.IndexOf(newSideSep) >= 0)
	{
		TArray<FString> sides = sideStr.Split(newSideSep, FString::TOK_KEEPEMPTY);

		if (sides[0].Len() > 0)
		{
			origIdx = atoi(sides[0].GetChars());
			side.vertref = ResolveIndex(origIdx, FaceElement::VertexIndex);
		}
		else
		{
			sc.ScriptError("Vertex reference is not optional!");
			return false;
		}

		if (sides[1].Len() > 0)
		{
			origIdx = atoi(sides[1].GetChars());
			side.uvref = ResolveIndex(origIdx, FaceElement::UVIndex);
		}
		else
		{
			side.uvref = -1;
		}

		if (sides.Size() > 2)
		{
			if (sides[2].Len() > 0)
			{
				origIdx = atoi(sides[2].GetChars());
				side.normref = ResolveIndex(origIdx, FaceElement::VNormalIndex);
			}
			else
			{
				side.normref = -1;
				hasMissingNormals = true;
			}
		}
		else
		{
			side.normref = -1;
			hasMissingNormals = true;
		}
	}
	else
	{
		origIdx = atoi(sideStr.GetChars());
		side.vertref = ResolveIndex(origIdx, FaceElement::VertexIndex);
		side.normref = -1;
		hasMissingNormals = true;
		side.uvref = -1;
	}
	face.sides[sidx] = side;
	return true;
}

/**
 * Resolve an OBJ index to an absolute index
 *
 * OBJ indices are 1-based, and can also be negative
 *
 * @param origIndex The original OBJ index to resolve
 * @param el What type of element the index references
 * @return The absolute index of the element
 */
int FOBJModel::ResolveIndex(int origIndex, FaceElement el)
{
	if (origIndex > 0)
	{
		return origIndex - 1; // OBJ indices start at 1
	}
	else if (origIndex < 0)
	{
		if (el == FaceElement::VertexIndex)
		{
			return verts.Size() + origIndex; // origIndex is negative
		}
		else if (el == FaceElement::UVIndex)
		{
			return uvs.Size() + origIndex;
		}
		else if (el == FaceElement::VNormalIndex)
		{
			return norms.Size() + origIndex;
		}
	}
	return -1;
}

/**
 * Construct the vertex buffer for this model
 *
 * @param renderer A pointer to the model renderer. Used to allocate the vertex buffer.
 */
void FOBJModel::BuildVertexBuffer(FModelRenderer *renderer)
{
	if (GetVertexBuffer(renderer->GetType()))
	{
		return;
	}

	unsigned int vbufsize = 0;

	for (size_t i = 0; i < surfaces.Size(); i++)
	{
		ConstructSurfaceTris(surfaces[i]);
		surfaces[i].vbStart = vbufsize;
		vbufsize += surfaces[i].numTris * 3;
	}
	// Initialize/populate vertFaces
	if (hasMissingNormals && hasSmoothGroups)
	{
		AddVertFaces();
	}

	auto vbuf = renderer->CreateVertexBuffer(false,true);
	SetVertexBuffer(renderer->GetType(), vbuf);

	FModelVertex *vertptr = vbuf->LockVertexBuffer(vbufsize);

	for (unsigned int i = 0; i < surfaces.Size(); i++)
	{
		for (unsigned int j = 0; j < surfaces[i].numTris; j++)
		{
			for (size_t side = 0; side < 3; side++)
			{
				FModelVertex *mdv = vertptr +
					side + j * 3 + // Current surface and previous triangles
					surfaces[i].vbStart; // Previous surfaces

				OBJFaceSide &curSide = surfaces[i].tris[j].sides[2 - side];

				int vidx = curSide.vertref;
				int uvidx = (curSide.uvref >= 0 && (unsigned int)curSide.uvref < uvs.Size()) ? curSide.uvref : 0;
				int nidx = curSide.normref;

				FVector3 curVvec = RealignVector(verts[vidx]);
				FVector2 curUvec = FixUV(uvs[uvidx]);
				FVector3 nvec;

				mdv->Set(curVvec.X, curVvec.Y, curVvec.Z, curUvec.X, curUvec.Y);

				if (nidx >= 0 && (unsigned int)nidx < norms.Size())
				{
					nvec = RealignVector(norms[nidx]);
				}
				else
				{
					if (surfaces[i].tris[j].smoothGroup == 0)
					{
						nvec = CalculateNormalFlat(i, j);
					}
					else
					{
						nvec = CalculateNormalSmooth(vidx, surfaces[i].tris[j].smoothGroup);
					}
				}
				mdv->SetNormal(nvec.X, nvec.Y, nvec.Z);
			}
		}
		delete[] surfaces[i].tris;
	}

	// Destroy vertFaces
	if (hasMissingNormals && hasSmoothGroups)
	{
		for (size_t i = 0; i < verts.Size(); i++)
		{
			vertFaces[i].Clear();
		}
		delete[] vertFaces;
	}
	vbuf->UnlockVertexBuffer();
}

/**
 * Fill in the triangle data for a surface
 *
 * @param[in,out] surf The surface to fill in the triangle data for
 */
void FOBJModel::ConstructSurfaceTris(OBJSurface &surf)
{
	unsigned int triCount = 0;

	size_t start = surf.faceStart;
	size_t end = start + surf.numFaces;
	for (size_t i = start; i < end; i++)
	{
		triCount += faces[i].sideCount - 2;
	}

	surf.numTris = triCount;
	surf.tris = new OBJFace[triCount];

	for (size_t i = start, triIdx = 0; i < end; i++, triIdx++)
	{
		surf.tris[triIdx].sideCount = 3;
		if (faces[i].sideCount == 3)
		{
			surf.tris[triIdx].smoothGroup = faces[i].smoothGroup;
			memcpy(surf.tris[triIdx].sides, faces[i].sides, sizeof(OBJFaceSide) * 3);
		}
		else if (faces[i].sideCount == 4) // Triangulate face
		{
			OBJFace *triangulated = new OBJFace[2];
			TriangulateQuad(faces[i], triangulated);
			memcpy(surf.tris[triIdx].sides, triangulated[0].sides, sizeof(OBJFaceSide) * 3);
			memcpy(surf.tris[triIdx+1].sides, triangulated[1].sides, sizeof(OBJFaceSide) * 3);
			delete[] triangulated;
			triIdx += 1; // Filling out two faces
		}
		DPrintf(DMSG_SPAMMY, "Smooth group: %d\n", surf.tris[triIdx].smoothGroup);
	}
}

/**
 * Triangulate a 4-sided face
 *
 * @param[in] quad The 4-sided face to triangulate
 * @param[out] tris The resultant triangle data
 */
void FOBJModel::TriangulateQuad(const OBJFace &quad, OBJFace *tris)
{
	tris[0].sideCount = 3;
	tris[0].smoothGroup = quad.smoothGroup;
	tris[1].sideCount = 3;
	tris[1].smoothGroup = quad.smoothGroup;

	int tsidx[2][3] = {{0, 1, 3}, {1, 2, 3}};

	for (int i = 0; i < 3; i++)
	{
		for (int j = 0; j < 2; j++)
		{
			tris[j].sides[i].vertref = quad.sides[tsidx[j][i]].vertref;
			tris[j].sides[i].uvref = quad.sides[tsidx[j][i]].uvref;
			tris[j].sides[i].normref = quad.sides[tsidx[j][i]].normref;
		}
	}
}

/**
 * Add the vertices of all surfaces' triangles to the array of vertex->triangle references
 */
void FOBJModel::AddVertFaces() {
	// Initialize and populate vertFaces - this array stores references to triangles per vertex
	vertFaces = new TArray<OBJTriRef>[verts.Size()];
	for (unsigned int i = 0; i < surfaces.Size(); i++)
	{
		for (unsigned int j = 0; j < surfaces[i].numTris; j++)
		{
			OBJTriRef otr = OBJTriRef(i, j);
			for (size_t k = 0; k < surfaces[i].tris[j].sideCount; k++)
			{
				int vidx = surfaces[i].tris[j].sides[k].vertref;
				vertFaces[vidx].Push(otr);
			}
		}
	}
}

/**
 * Re-align a vector to match MD3 alignment
 *
 * @param vecToRealign The vector to re-align
 * @return The re-aligned vector
 */
inline FVector3 FOBJModel::RealignVector(FVector3 vecToRealign)
{
	vecToRealign.Z *= -1;
	return vecToRealign;
}

/**
 * Fix UV coordinates of a UV vector
 *
 * @param vecToRealign The vector to fix
 * @return The fixed UV coordinate vector
 */
inline FVector2 FOBJModel::FixUV(FVector2 vecToRealign)
{
	vecToRealign.Y *= -1;
	return vecToRealign;
}

/**
 * Calculate the surface normal for a triangle
 *
 * @param surfIdx The surface index
 * @param triIdx The triangle Index
 * @return The surface normal vector
 */
FVector3 FOBJModel::CalculateNormalFlat(unsigned int surfIdx, unsigned int triIdx)
{
	// https://www.khronos.org/opengl/wiki/Calculating_a_Surface_Normal
	int curVert = surfaces[surfIdx].tris[triIdx].sides[0].vertref;
	int nextVert = surfaces[surfIdx].tris[triIdx].sides[2].vertref;
	int lastVert = surfaces[surfIdx].tris[triIdx].sides[1].vertref;

	// Cross-multiply the U-vector and V-vector
	FVector3 curVvec = RealignVector(verts[curVert]);
	FVector3 uvec = RealignVector(verts[nextVert]) - curVvec;
	FVector3 vvec = RealignVector(verts[lastVert]) - curVvec;

	return uvec ^ vvec;
}

/**
 * Calculate the surface normal for a triangle
 *
 * @param otr A reference to the surface, and a triangle within that surface, as an OBJTriRef
 * @return The surface normal vector
 */
FVector3 FOBJModel::CalculateNormalFlat(OBJTriRef otr)
{
	return CalculateNormalFlat(otr.surf, otr.tri);
}

/**
 * Calculate the normal of a vertex in a specific smooth group
 *
 * @param vidx The index of the vertex in the array of vertices
 * @param smoothGroup The smooth group number
 */
FVector3 FOBJModel::CalculateNormalSmooth(unsigned int vidx, unsigned int smoothGroup)
{
	unsigned int connectedFaces = 0;
	TArray<OBJTriRef>& vTris = vertFaces[vidx];

	FVector3 vNormal(0,0,0);
	for (size_t face = 0; face < vTris.Size(); face++)
	{
		OBJFace& tri = surfaces[vTris[face].surf].tris[vTris[face].tri];
		if (tri.smoothGroup == smoothGroup)
		{
			FVector3 fNormal = CalculateNormalFlat(vTris[face]);
			connectedFaces += 1;
			vNormal += fNormal;
		}
	}
	vNormal /= (float)connectedFaces;
	return vNormal;
}

/**
 * Find the index of the frame with the given name
 *
 * OBJ models are not animated, so this always returns 0
 *
 * @param name The name of the frame
 * @return The index of the frame
 */
int FOBJModel::FindFrame(const char* name)
{
	return 0; // OBJs are not animated.
}

/**
 * Render the model
 *
 * @param renderer The model renderer
 * @param skin The loaded skin for the surface
 * @param frameno Unused
 * @param frameno2 Unused
 * @param inter Unused
 * @param translation The translation for the skin
 */
void FOBJModel::RenderFrame(FModelRenderer *renderer, FGameTexture * skin, int frameno, int frameno2, double inter, int translation)
{
	for (unsigned int i = 0; i < surfaces.Size(); i++)
	{
		OBJSurface *surf = &surfaces[i];

		FGameTexture *userSkin = skin;
		if (!userSkin && curSpriteMDLFrame)
		{
			int ssIndex = i + curMDLIndex * MD3_MAX_SURFACES;
			if (i < MD3_MAX_SURFACES && curSpriteMDLFrame->surfaceskinIDs[ssIndex].isValid())
			{
				userSkin = TexMan.GetGameTexture(curSpriteMDLFrame->surfaceskinIDs[ssIndex], true);
			}
			else if (surf->skin.isValid())
			{
				userSkin = TexMan.GetGameTexture(surf->skin, true);
			}
		}

		// Still no skin after checking for one?
		if (!userSkin)
		{
			continue;
		}

		renderer->SetMaterial(userSkin, false, translation);
		renderer->SetupFrame(this, surf->vbStart, surf->vbStart, surf->numTris * 3);
		renderer->DrawArrays(0, surf->numTris * 3);
	}
}

/**
 * Pre-cache skins for the model
 *
 * @param hitlist The list of textures
 */
void FOBJModel::AddSkins(uint8_t* hitlist)
{
	for (size_t i = 0; i < surfaces.Size(); i++)
	{
		size_t ssIndex = i + curMDLIndex * MD3_MAX_SURFACES;
		if (curSpriteMDLFrame && i < MD3_MAX_SURFACES && curSpriteMDLFrame->surfaceskinIDs[ssIndex].isValid())
		{
			// Precache skins manually reassigned by the user.
			// On OBJs with lots of skins, such as Doom map OBJs exported from GZDB,
			// there may be too many skins for the user to manually change, unless
			// the limit is bumped or surfaceskinIDs is changed to a TArray<FTextureID>.
			hitlist[curSpriteMDLFrame->surfaceskinIDs[ssIndex].GetIndex()] |= FTextureManager::HIT_Flat;
			return; // No need to precache skin that was replaced
		}

		OBJSurface * surf = &surfaces[i];
		if (surf->skin.isValid())
		{
			hitlist[surf->skin.GetIndex()] |= FTextureManager::HIT_Flat;
		}
	}
}

/**
 * Remove the data that was loaded
 */
FOBJModel::~FOBJModel()
{
	verts.Clear();
	norms.Clear();
	uvs.Clear();
	faces.Clear();
	surfaces.Clear();
}