// NeL - MMORPG Framework <http://dev.ryzom.com/projects/nel/>
// Copyright (C) 2010  Winch Gate Property Limited
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero 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 Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

#include "lod_texture_builder.h"
#include "nel/misc/aabbox.h"


using namespace std;
using namespace NLMISC;
using namespace NL3D;


// ***************************************************************************
// Quality factor. Useful to get maximum of interseting values in the range 0..255.
#define	NL_LTB_MAX_DISTANCE_QUALITY_FACTOR	0.05f
// The bigger, the lower the influence of the normal is. must be >=0
#define	NL_LTB_NORMAL_BIAS					1.f


// ***************************************************************************
CLodTextureBuilder::CLodTextureBuilder()
{
	_OverSampleDistance= 0.05f;
}


// ***************************************************************************
float			CLodTextureBuilder::computeQualityPixel(const CPixelInfo &p0, const CPixelInfo &p1) const
{
	float	d= (p1.Pos-p0.Pos).norm();
	float	dotProd= p1.Normal*p0.Normal;
	// With NL_NORMAL_BIAS==1, it return froms d*1(normals aligned) to d*3 (opposite normals)
	return d*(NL_LTB_NORMAL_BIAS+1-dotProd);
}

// ***************************************************************************
void			CLodTextureBuilder::setLod(const NL3D::CLodCharacterShapeBuild &lod)
{
	uint	i;

	_CLod= lod;

	// **** compute the max quality distance. ie where CTUVQ.Q== 255
	// build a bbox around the lod.
	CAABBox	bbox;
	bbox.setCenter(lod.Vertices[0]);
	for(i=0;i<lod.Vertices.size();i++)
		bbox.extend(lod.Vertices[i]);
	/* get the opposite vertices/normals and so compute the max qualityPixel "possible" (it is still possible 
		for the CMesh to be completely out of the bbox of the CLod, but doesn't matter)
	*/
	CPixelInfo	p0, p1;
	p0.Pos= bbox.getMin();
	p0.Normal.set(1,0,0);
	p1.Pos= bbox.getMax();
	p1.Normal.set(-1,0,0);
	_MaxDistanceQuality= computeQualityPixel(p0, p1);
	// apply a user factor.
	_MaxDistanceQuality*= NL_LTB_MAX_DISTANCE_QUALITY_FACTOR;
}

// ***************************************************************************
bool			CLodTextureBuilder::computeTexture(const CMesh &mesh, NL3D::CLodCharacterTexture  &text)
{
	// a set to flag if an edge has already been overSampled
	TEdgeSet	edgeSet;

	// **** Over sample the mesh
	_Samples.clear();
	_Samples.reserve(1000);
	// Get vertex info
	const CVertexBuffer	&VB= mesh.getVertexBuffer();
	CVertexBufferRead vba;
	VB.lock (vba);
	const uint8			*srcPos= (const uint8*)vba.getVertexCoordPointer();
	const uint8			*srcNormal= (const uint8*)vba.getNormalCoordPointer();
	const uint8			*srcUV= (const uint8*)vba.getTexCoordPointer();
	uint				vertexSize= VB.getVertexSize();
	// For all matrix blocks..
	for(uint mb=0; mb<mesh.getNbMatrixBlock();mb++)
	{
		// for all rdrPass
		for(uint rp=0; rp<mesh.getNbRdrPass(mb);rp++)
		{
			const CIndexBuffer	&pb= mesh.getRdrPassPrimitiveBlock(mb, rp);
			CIndexBufferRead iba;
			pb.lock (iba);
			uint	matId= mesh.getRdrPassMaterial(mb, rp);
			// samples the tris of this pass			
			if (iba.getFormat() == CIndexBuffer::Indices16)
			{					
				addSampleTris(srcPos, srcNormal, srcUV, vertexSize, (uint16 *) iba.getPtr(), pb.getNumIndexes()/3, matId, edgeSet);
			}
			else
			{
				nlassert(iba.getFormat() == CIndexBuffer::Indices32);
				addSampleTris(srcPos, srcNormal, srcUV, vertexSize, (uint32 *) iba.getPtr(), pb.getNumIndexes()/3, matId, edgeSet);
			}
		}
	}


	// **** compute the texture, with the samples
	computeTextureFromSamples(text);

	return true;
}

// ***************************************************************************
bool			CLodTextureBuilder::computeTexture(const CMeshMRM &meshMRM, NL3D::CLodCharacterTexture  &text)
{
	// a set to flag if an edge has already been overSampled
	TEdgeSet	edgeSet;

	// **** Over sample the mesh
	_Samples.clear();
	_Samples.reserve(1000);
	// Get vertex info
	CVertexBuffer		&VB= const_cast<CVertexBuffer&>(meshMRM.getVertexBuffer());
	CVertexBufferRead vba;
	VB.lock (vba);
	const uint8			*srcPos= (const uint8*)vba.getVertexCoordPointer();
	const uint8			*srcNormal= (const uint8*)vba.getNormalCoordPointer();
	const uint8			*srcUV= (const uint8*)vba.getTexCoordPointer();
	uint				vertexSize = VB.getVertexSize();
	// For the more precise lod
	uint	lodId= meshMRM.getNbLod()-1;
	// Resolve Geomoprh problem: copy End to all geomorphs dest. Hence sure that all ids points to good vertex data
	const std::vector<CMRMWedgeGeom>	&geoms= meshMRM.getMeshGeom().getGeomorphs(lodId);
	for(uint gm=0;gm<geoms.size();gm++)
	{
		uint	srcId= geoms[gm].End;
		// copy the geom src to the dest VB place.
		*(CVector*)(srcPos+gm*vertexSize)= *(CVector*)(srcPos+srcId*vertexSize);
		*(CVector*)(srcNormal+gm*vertexSize)= *(CVector*)(srcNormal+srcId*vertexSize);
		*(CUV*)(srcUV+gm*vertexSize)= *(CUV*)(srcUV+srcId*vertexSize);
	}
	// for all rdrPass
	for(uint rp=0; rp<meshMRM.getNbRdrPass(lodId);rp++)
	{
		const CIndexBuffer	&pb= meshMRM.getRdrPassPrimitiveBlock(lodId, rp);
		CIndexBufferRead iba;
		pb.lock (iba);
		uint	matId= meshMRM.getRdrPassMaterial(lodId, rp);
		// samples the tris of this pass
		if (iba.getFormat() == CIndexBuffer::Indices16)
		{
			addSampleTris(srcPos, srcNormal, srcUV, vertexSize, (uint16 *) iba.getPtr(), pb.getNumIndexes()/3, matId, edgeSet);
		}
		else
		{
			nlassert(iba.getFormat() == CIndexBuffer::Indices32);
			addSampleTris(srcPos, srcNormal, srcUV, vertexSize, (uint32 *) iba.getPtr(), pb.getNumIndexes()/3, matId, edgeSet);
		}
	}


	// **** compute the texture, with the samples
	computeTextureFromSamples(text);

	return true;
}


// ***************************************************************************
bool			CLodTextureBuilder::computeTexture(const CMeshMRMSkinned &meshMRM, NL3D::CLodCharacterTexture  &text)
{
	// a set to flag if an edge has already been overSampled
	TEdgeSet	edgeSet;

	// **** Over sample the mesh
	_Samples.clear();
	_Samples.reserve(1000);
	// Get vertex info
	CVertexBuffer tmp;
	meshMRM.getVertexBuffer(tmp);
	CVertexBufferRead vba;
	tmp.lock (vba);
	const uint8			*srcPos= (const uint8*)vba.getVertexCoordPointer();
	const uint8			*srcNormal= (const uint8*)vba.getNormalCoordPointer();
	const uint8			*srcUV= (const uint8*)vba.getTexCoordPointer();
	uint				vertexSize= tmp.getVertexSize();
	// For the more precise lod
	uint	lodId= meshMRM.getNbLod()-1;
	// Resolve Geomoprh problem: copy End to all geomorphs dest. Hence sure that all ids points to good vertex data
	const std::vector<CMRMWedgeGeom>	&geoms= meshMRM.getMeshGeom().getGeomorphs(lodId);
	for(uint gm=0;gm<geoms.size();gm++)
	{
		uint	srcId= geoms[gm].End;
		// copy the geom src to the dest VB place.
		*(CVector*)(srcPos+gm*vertexSize)= *(CVector*)(srcPos+srcId*vertexSize);
		*(CVector*)(srcNormal+gm*vertexSize)= *(CVector*)(srcNormal+srcId*vertexSize);
		*(CUV*)(srcUV+gm*vertexSize)= *(CUV*)(srcUV+srcId*vertexSize);
	}
	// for all rdrPass
	for(uint rp=0; rp<meshMRM.getNbRdrPass(lodId);rp++)
	{
		CIndexBuffer	pb;
		meshMRM.getRdrPassPrimitiveBlock(lodId, rp, pb);
		uint	matId= meshMRM.getRdrPassMaterial(lodId, rp);
		CIndexBufferRead iba;
		pb.lock (iba);
		// samples the tris of this pass
		if (iba.getFormat() == CIndexBuffer::Indices16)
		{
			addSampleTris(srcPos, srcNormal, srcUV, vertexSize, (uint16 *) iba.getPtr(), pb.getNumIndexes()/3, matId, edgeSet);
		}
		else
		{
			addSampleTris(srcPos, srcNormal, srcUV, vertexSize, (uint32 *) iba.getPtr(), pb.getNumIndexes()/3, matId, edgeSet);
		}
	}


	// **** compute the texture, with the samples
	computeTextureFromSamples(text);

	return true;
}

// ***************************************************************************
void			CLodTextureBuilder::addSampleTris(const uint8 *srcPos, const uint8 *srcNormal, const uint8 *srcUV, uint vertexSize, 
	const uint16 *triPointer, uint numTris, uint materialId, TEdgeSet &edgeSet)
{
	std::vector<uint32> indices32(triPointer, triPointer + numTris * 3);
	addSampleTris(srcPos, srcNormal, srcUV, vertexSize, &indices32[0], numTris, materialId, edgeSet);
}

// ***************************************************************************
void			CLodTextureBuilder::addSampleTris(const uint8 *srcPos, const uint8 *srcNormal, const uint8 *srcUV, uint vertexSize, 
	const uint32 *triPointer, uint numTris, uint materialId, TEdgeSet &edgeSet)
{
	nlassert(srcPos);

	// ensure that the material is in 0..255
	clamp(materialId, 0U, 255U);

	for(uint i=0;i<numTris;i++)
	{
		uint c;

		// **** get tri indices and tri values.
		uint		idx[3];
		CVector		pos[3];
		CVector		normal[3];
		CUV			uv[3];
		for(c=0;c<3;c++)
		{
			idx[c]= *triPointer++;
			pos[c]= *(CVector*)(srcPos + idx[c]*vertexSize);
			if(srcNormal)
				normal[c]= *(CVector*)(srcNormal + idx[c]*vertexSize);
			else
				normal[c]= CVector::K;
			if(srcUV)
				uv[c]= *(CUV*)(srcUV + idx[c]*vertexSize);
			else
				uv[c].set(0,0);
		}

		// **** Append the 3 vertices in the samples. only if not already done
		for(c=0;c<3;c++)
		{
			// special edgeId: vertStart==vertEnd
			TEdge	edgeId;
			edgeId.first= idx[c];
			edgeId.second= idx[c];
			// if success to insert in the set, then must add it to samples
			if(edgeSet.insert(edgeId).second)
			{
				CSample		sample;
				sample.P.Pos= pos[c];
				sample.P.Normal= normal[c];
				sample.P.Normal.normalize();
				sample.UV= uv[c];
				sample.MaterialId= materialId;
				_Samples.push_back(sample);
			}
		}

		// **** Append the interior of each edges, according to _OverSampleDistance
		uint	numEdgeOverSamples[3];
		for(c=0;c<3;c++)
		{
			uint	cNext= (c+1)%3;
			// get the edgeId
			TEdge	edgeId;
			edgeId.first= idx[c];
			edgeId.second= idx[cNext];
			// compute the number of overSamples to apply to the edge
			numEdgeOverSamples[c]= (uint)floor( (pos[cNext]-pos[c]).norm()/_OverSampleDistance );

			// if success to insert in the set, then must add it to samples
			if(edgeSet.insert(edgeId).second)
			{
				// Do it only if numOverSamples>=2
				if(numEdgeOverSamples[c]>=2)
				{
					// Sample the edge, exclude endPoints.
					for(uint s=1;s<numEdgeOverSamples[c];s++)
					{
						float	f= s/(float)numEdgeOverSamples[c];
						// interpoalte the sample
						CSample		sample;
						sample.P.Pos= pos[c]*(1-f) + pos[cNext]*f;
						sample.P.Normal= normal[c]*(1-f) + normal[cNext]*f;
						sample.P.Normal.normalize();
						sample.UV= uv[c]*(1-f) + uv[cNext]*f;
						sample.MaterialId= materialId;
						// add it
						_Samples.push_back(sample);
					}
				}
			}
		}

		// **** Append the interior of the triangle, if too many overSample
		// compute min of over samples
		uint	triOverSample= min(numEdgeOverSamples[0], numEdgeOverSamples[1]);
		triOverSample= min(triOverSample, numEdgeOverSamples[2]);
		// If >=3 (at least one vertex in the middle)
		if(triOverSample>=2)
		{
			// Interpolate with barycentric coordinate rules
			uint	s,t,v;
			// Donc't compute on triangles edges.
			for(s=1;s<triOverSample;s++)
			{
				for(t=1;t<triOverSample-s;t++)
				{
					// barycentric sum==1 guaranteed.
					v= triOverSample-s-t;
					float	fs= s/(float)triOverSample;
					float	ft= t/(float)triOverSample;
					float	fv= v/(float)triOverSample;
					// compute the sample
					CSample		sample;
					sample.P.Pos= pos[0]*fs + pos[1]*ft + pos[2]*fv;
					sample.P.Normal= normal[0]*fs + normal[1]*ft + normal[2]*fv;
					sample.P.Normal.normalize();
					sample.UV= uv[0]*fs + uv[1]*ft + uv[2]*fv;
					sample.MaterialId= materialId;
					// add it
					_Samples.push_back(sample);
				}
			}
		}

	}
}


// ***************************************************************************
bool			CLodTextureBuilder::computeTextureFromSamples(NL3D::CLodCharacterTexture  &text)
{
	// The lod must have correct width/height
	if(NL3D_CLOD_TEXT_WIDTH!=_CLod.getTextureInfoWidth() || NL3D_CLOD_TEXT_HEIGHT!=_CLod.getTextureInfoHeight() )
	{
		nlwarning("ERROR: the _CLod must have a textureInfo size of %d/%d", NL3D_CLOD_TEXT_WIDTH, NL3D_CLOD_TEXT_HEIGHT);
		return false;
	}

	// prepare dest.
	text.Texture.resize(NL3D_CLOD_TEXT_SIZE);
	CLodCharacterTexture::CTUVQ		emptyTUVQ;
	emptyTUVQ.T= 0;
	emptyTUVQ.U= 0;
	emptyTUVQ.V= 0;
	emptyTUVQ.Q= 255;
	fill(text.Texture.begin(), text.Texture.end(), emptyTUVQ);

	// For all pixels.
	const	CPixelInfo		*srcPixel= _CLod.getTextureInfoPtr();
	for(uint i=0;i<NL3D_CLOD_TEXT_SIZE;i++)
	{
		// if the src pixel is not empty
		if( !(srcPixel[i]==CPixelInfo::EmptyPixel) )
		{
			CLodCharacterTexture::CTUVQ		&dstTUVQ= text.Texture[i];

			// For All samples, search the best.
			float	minQuality= FLT_MAX;
			uint	bestIdx= 0;
			for(uint j=0;j<_Samples.size();j++)
			{
				float	q= computeQualityPixel(srcPixel[i], _Samples[j].P);
				if(q<minQuality)
				{
					minQuality= q;
					bestIdx= j;
				}
			}

			// Then compress the sample info in the TUVQ.
			dstTUVQ.T= _Samples[bestIdx].MaterialId;
			dstTUVQ.U= (uint8)(sint)(_Samples[bestIdx].UV.U*256);
			dstTUVQ.V= (uint8)(sint)(_Samples[bestIdx].UV.V*256);
			minQuality= minQuality*255/_MaxDistanceQuality;
			clamp(minQuality, 0, 255);
			dstTUVQ.Q= (uint8)minQuality;
		}
	}

	return true;
}