//========= Copyright © 1996-2005, Valve Corporation, All rights reserved. ============//
//
// Purpose: 
//
// $NoKeywords: $
//
//=============================================================================//

#include "MAX.H"
#include "DECOMP.H"
#include "STDMAT.H"
#include "ANIMTBL.H"
#include "istdplug.h"
#include "phyexp.h"
#include "vtaexprc.h"
#include "vtadefs.h"



//===================================================================
// Prototype declarations
//
int GetIndexOfINode(INode *pnode,BOOL fAssertPropExists = TRUE);
void SetIndexOfINode(INode *pnode, int inode);
BOOL FUndesirableNode(INode *pnode);
BOOL FNodeMarkedToSkip(INode *pnode);
float FlReduceRotation(float fl);


//===================================================================
// Global variable definitions
//

// Save for use with dialogs
static HINSTANCE hInstance;

// We just need one of these to hand off to 3DSMAX.
static VtaExportClassDesc VtaExportCD;

// For OutputDebugString and misc sprintf's
static char st_szDBG[300];

// INode mapping table
static int g_inmMac = 0;

//===================================================================
// Utility functions
//

static int AssertFailedFunc(char *sz)
{
	MessageBox(GetActiveWindow(), sz, "Assert failure", MB_OK);
	int Set_Your_Breakpoint_Here = 1;
	return 1;
}
#define ASSERT_MBOX(f, sz) ((f) ? 1 : AssertFailedFunc(sz))


//===================================================================
// Required plug-in export functions
//
BOOL WINAPI DllMain( HINSTANCE hinstDLL, ULONG fdwReason, LPVOID lpvReserved) 
{	
	static int fFirstTimeHere = TRUE;
	if (fFirstTimeHere)
	{
		fFirstTimeHere = FALSE;
		hInstance = hinstDLL;
	}
	return TRUE;
}

	
EXPORT_THIS int LibNumberClasses(void)
{
	return 1;
}

	
EXPORT_THIS ClassDesc *LibClassDesc(int iWhichClass)
{
	switch(iWhichClass)
	{
		case 0: return &VtaExportCD;
		default: return 0;
	}
}

	
EXPORT_THIS const TCHAR *LibDescription()
{
	return _T("Valve VTA Plug-in.");
}

	
EXPORT_THIS ULONG LibVersion()
{
	return VERSION_3DSMAX;
}


//=====================================================================
// Methods for VtaExportClass
//

CONSTRUCTOR VtaExportClass::VtaExportClass(void)
{
	m_rgmaxnode		= NULL;
}


DESTRUCTOR VtaExportClass::~VtaExportClass(void)
{
	if (m_rgmaxnode)
		delete[] m_rgmaxnode;
}


int VtaExportClass::DoExport(const TCHAR *name,ExpInterface *ei,Interface *i, BOOL suppressPrompts, DWORD options) 
{
	ExpInterface	*pexpiface = ei;	// Hungarian
	Interface		*piface = i;		// Hungarian
	
	// Reset the name-map property manager
	g_inmMac = 0;

	// Break up filename, re-assemble longer versions
	TSTR strPath, strFile, strExt;
	TCHAR szFile[MAX_PATH];
	SplitFilename(TSTR(name), &strPath, &strFile, &strExt);
		sprintf(szFile,  "%s\\%s.%s",  (char*)strPath, (char*)strFile, DEFAULT_EXT);

	FILE *pFile;
	if ((pFile = fopen(szFile, "w")) == NULL)
		return FALSE/*failure*/;

	fprintf( pFile, "version %d\n", 1 );

	// Get animation metrics
	m_intervalOfAnimation = piface->GetAnimRange();
	m_tvStart = m_intervalOfAnimation.Start();
	m_tvEnd = m_intervalOfAnimation.End();
	m_tpf = ::GetTicksPerFrame();

	// Count nodes, label them, collect into array
	if (!CollectNodes(pexpiface))
		return 0;	/*fail*/
	
	// Output nodes
	if (!DumpBones(pFile, pexpiface))
	{
		fclose( pFile );
		return 0;	/*fail*/
	}

	// Output bone rotations, for each frame. Do only first frame if this is the reference frame MAX file
	DumpRotations(pFile, pexpiface);

	// Output triangle meshes (first frame/all frames), if this is the reference frame MAX file
	DumpModel(pFile, pexpiface);

	// Tell user that exporting is finished (it can take a while with no feedback)
	char szExportComplete[300];
	sprintf(szExportComplete, "Exported %s.", szFile);
	MessageBox(GetActiveWindow(), szExportComplete, "Status", MB_OK);

	fclose( pFile );

	return 1/*success*/;
}
	
	
BOOL VtaExportClass::CollectNodes( ExpInterface *pexpiface)
{
	// Count total nodes in the model, so I can alloc array
	// Also "brands" each node with node index, or with "skip me" marker.
	CountNodesTEP procCountNodes;
	procCountNodes.m_cNodes = 0;
	(void) pexpiface->theScene->EnumTree(&procCountNodes);
	ASSERT_MBOX(procCountNodes.m_cNodes > 0, "No nodes!");

	// Alloc and fill array
	m_imaxnodeMac = procCountNodes.m_cNodes;
	m_rgmaxnode = new MaxNode[m_imaxnodeMac];
	ASSERT_MBOX(m_rgmaxnode != NULL, "new failed");


	CollectNodesTEP procCollectNodes;
	procCollectNodes.m_phec = this;
	(void) pexpiface->theScene->EnumTree(&procCollectNodes);
	
	return TRUE;
}


BOOL VtaExportClass::DumpBones(FILE *pFile, ExpInterface *pexpiface)
{
	// Dump bone names
	DumpNodesTEP procDumpNodes;
	procDumpNodes.m_pfile = pFile;
	procDumpNodes.m_phec = this;
	fprintf(pFile, "nodes\n" );
	(void) pexpiface->theScene->EnumTree(&procDumpNodes);
	fprintf(pFile, "end\n" );

	return TRUE;
}

	
BOOL VtaExportClass::DumpRotations(FILE *pFile, ExpInterface *pexpiface)
{
	// Dump bone-rotation info, for each frame
	// Also dumps root-node translation info (the model's world-position at each frame)
	DumpFrameRotationsTEP procDumpFrameRotations;
	procDumpFrameRotations.m_pfile	= pFile;
	procDumpFrameRotations.m_phec	= this;

	fprintf(pFile, "skeleton\n" );
	for (TimeValue tv = m_tvStart; tv <= m_tvEnd; tv += m_tpf)
	{
		fprintf(pFile, "time %d\n", tv / GetTicksPerFrame() );
		procDumpFrameRotations.m_tvToDump = tv;
		(void) pexpiface->theScene->EnumTree(&procDumpFrameRotations);
	}
	fprintf(pFile, "end\n" );

	return TRUE;
}
	
	
BOOL VtaExportClass::DumpModel( FILE *pFile, ExpInterface *pexpiface)
{
	// Dump mesh info: vertices, normals, UV texture map coords, bone assignments
	DumpModelTEP procDumpModel;
	procDumpModel.m_pfile	= pFile;
	procDumpModel.m_phec	= this;

	procDumpModel.m_tvBase = m_tvStart;

	fprintf(pFile, "vertexanimation\n" );
	procDumpModel.m_baseVert = NULL;
	for (TimeValue tv = m_tvStart; tv <= m_tvEnd; tv += m_tpf)
	{
		fprintf(pFile, "time %d\n", tv / GetTicksPerFrame() );
		procDumpModel.m_tvToDump = tv;
		procDumpModel.m_baseVertCount = 0;
		(void) pexpiface->theScene->EnumTree(&procDumpModel);
	}
	fprintf(pFile, "end\n" );

	return TRUE;
}	




//=============================================================================
//							TREE-ENUMERATION PROCEDURES
//=============================================================================

#define ASSERT_AND_ABORT(f, sz)							\
	if (!(f))											\
	{													\
		ASSERT_MBOX(FALSE, sz);							\
		/* cleanup( ); */								\
		return TREE_ABORT;								\
	}


//=================================================================
// Methods for CountNodesTEP
//
int CountNodesTEP::callback( INode *node)
{
	INode *pnode = node; // Hungarian

	ASSERT_MBOX(!(pnode)->IsRootNode(), "Encountered a root node!");

	if (::FUndesirableNode(pnode))
	{
		// Mark as skippable
		::SetIndexOfINode(pnode, VtaExportClass::UNDESIRABLE_NODE_MARKER);
		return TREE_CONTINUE;
	}
	
	// Establish "node index"--just ascending ints
	::SetIndexOfINode(pnode, m_cNodes);

	m_cNodes++;
	
	return TREE_CONTINUE;
}


//=================================================================
// Methods for CollectNodesTEP
//
int CollectNodesTEP::callback(INode *node)
{
	INode *pnode = node; // Hungarian

	ASSERT_MBOX(!(pnode)->IsRootNode(), "Encountered a root node!");

	if (::FNodeMarkedToSkip(pnode))
		return TREE_CONTINUE;

	// Get pre-stored "index"
	int iNode = ::GetIndexOfINode(pnode);
	ASSERT_MBOX(iNode >= 0 && iNode <= m_phec->m_imaxnodeMac-1, "Bogus iNode");
	
	// Get name, store name in array
	TSTR strNodeName(pnode->GetName());
	strcpy(m_phec->m_rgmaxnode[iNode].szNodeName, (char*)strNodeName);

	// Get Node's time-zero Transformation Matrices
	m_phec->m_rgmaxnode[iNode].mat3NodeTM		= pnode->GetNodeTM(0/*TimeValue*/);
	m_phec->m_rgmaxnode[iNode].mat3ObjectTM		= pnode->GetObjectTM(0/*TimeValue*/);

	// I'll calculate this later
	m_phec->m_rgmaxnode[iNode].imaxnodeParent	= VtaExportClass::UNDESIRABLE_NODE_MARKER;

	return TREE_CONTINUE;
}






//=================================================================
// Methods for DumpNodesTEP
//
int DumpNodesTEP::callback(INode *pnode)
{
	ASSERT_MBOX(!(pnode)->IsRootNode(), "Encountered a root node!");

	if (::FNodeMarkedToSkip(pnode))
		return TREE_CONTINUE;

	// Get node's parent
	INode *pnodeParent;
	pnodeParent = pnode->GetParentNode();
	
	// The model's root is a child of the real "scene root"
	TSTR strNodeName(pnode->GetName());
	BOOL fNodeIsRoot = pnodeParent->IsRootNode( );
	
	int iNode = ::GetIndexOfINode(pnode);
	int iNodeParent = ::GetIndexOfINode(pnodeParent, !fNodeIsRoot/*fAssertPropExists*/);

	// Convenient time to cache this
	m_phec->m_rgmaxnode[iNode].imaxnodeParent = fNodeIsRoot ? VtaExportClass::UNDESIRABLE_NODE_MARKER : iNodeParent;

	// Root node has no parent, thus no translation
	if (fNodeIsRoot)
		iNodeParent = -1;
		
	// Dump node description
	fprintf(m_pfile, "%3d \"%s\" %3d\n", 
		iNode, 
		strNodeName, 
		iNodeParent );

	return TREE_CONTINUE;
}



//=================================================================
// Methods for DumpFrameRotationsTEP
//
int DumpFrameRotationsTEP::callback(INode *pnode)
{
	ASSERT_MBOX(!(pnode)->IsRootNode(), "Encountered a root node!");

	if (::FNodeMarkedToSkip(pnode))
		return TREE_CONTINUE;

	int iNode = ::GetIndexOfINode(pnode);

	TSTR strNodeName(pnode->GetName());

	// The model's root is a child of the real "scene root"
	INode *pnodeParent = pnode->GetParentNode();
	BOOL fNodeIsRoot = pnodeParent->IsRootNode( );

	// Get Node's "Local" Transformation Matrix
	Matrix3 mat3NodeTM		= pnode->GetNodeTM(m_tvToDump);
	Matrix3 mat3ParentTM	= pnodeParent->GetNodeTM(m_tvToDump);
	mat3NodeTM.NoScale();		// Clear these out because they apparently
	mat3ParentTM.NoScale();		// screw up the following calculation.
	Matrix3 mat3NodeLocalTM	= mat3NodeTM * Inverse(mat3ParentTM);
	Point3 rowTrans = mat3NodeLocalTM.GetTrans();

	// Get the rotation (via decomposition into "affine parts", then quaternion-to-Euler)
	// Apparently the order of rotations returned by QuatToEuler() is X, then Y, then Z.
	AffineParts affparts;
	float rgflXYZRotations[3];

	decomp_affine(mat3NodeLocalTM, &affparts);
	QuatToEuler(affparts.q, rgflXYZRotations);

	float xRot = rgflXYZRotations[0];		// in radians
	float yRot = rgflXYZRotations[1];		// in radians
	float zRot = rgflXYZRotations[2];		// in radians

	// Get rotations in the -2pi...2pi range
	xRot = ::FlReduceRotation(xRot);
	yRot = ::FlReduceRotation(yRot);
	zRot = ::FlReduceRotation(zRot);
	
	// Print rotations
	//fprintf(m_pfile, "%3d %8.4f %8.4f %8.4f %8.4f %8.4f %8.4f\n", 
	fprintf(m_pfile, "%3d %f %f %f %f %f %f\n", 
			// Node:%-15s Rotation (x,y,z)\n",
			iNode, rowTrans.x, rowTrans.y, rowTrans.z, xRot, yRot, zRot);

	return TREE_CONTINUE;
}



//=================================================================
// Methods for DumpModelTEP
//
Modifier *FindPhysiqueModifier (INode *nodePtr)
{
	// Get object from node. Abort if no object.
	Object *ObjectPtr = nodePtr->GetObjectRef();
	if (!ObjectPtr) return NULL;

	// Is derived object ?
	if (ObjectPtr->SuperClassID() == GEN_DERIVOB_CLASS_ID)
	{
		// Yes -> Cast.
		IDerivedObject *DerivedObjectPtr = static_cast<IDerivedObject*>(ObjectPtr);

		// Iterate over all entries of the modifier stack.
		int ModStackIndex = 0;
		while (ModStackIndex < DerivedObjectPtr->NumModifiers())
		{
			// Get current modifier.
			Modifier *ModifierPtr = DerivedObjectPtr->GetModifier(ModStackIndex);

			// Is this Physique ?
			if (ModifierPtr->ClassID() == Class_ID(	PHYSIQUE_CLASS_ID_A, PHYSIQUE_CLASS_ID_B) )
			{
				// Yes -> Exit.
				return ModifierPtr;
			}
			// Next modifier stack entry.
			ModStackIndex++;
		}
	}
	// Not found.
	return NULL;
}


// #define DEBUG_MESH_DUMP

//=================================================================
// Methods for DumpModelTEP
//
int DumpModelTEP::callback(INode *pnode)
{
	ASSERT_MBOX(!(pnode)->IsRootNode(), "Encountered a root node!");

	if (::FNodeMarkedToSkip(pnode))
		return TREE_CONTINUE;
	
	if ( !pnode->Selected())
		return TREE_CONTINUE;

	int iNode = ::GetIndexOfINode(pnode);
	TSTR strNodeName(pnode->GetName());
	
	// The Footsteps node apparently MUST have a dummy mesh attached!  Ignore it explicitly.
	if (FStrEq((char*)strNodeName, "Bip01 Footsteps"))
		return TREE_CONTINUE;

	// Helper nodes don't have meshes
	Object *pobj = pnode->GetObjectRef();
	if (pobj->SuperClassID() == HELPER_CLASS_ID)
		return TREE_CONTINUE;


   // Get the object's parameter block if it has one 

    IParamBlock *pb = NULL;
    IParamArray *pa = pobj->GetParamBlock(); 

	if (!pa) 
	{
		int i = pobj->NumRefs();
		// Search the references looking for a parameter block 
		for (i = 0; i < pobj->NumRefs(); i++) 
		{
			RefTargetHandle r = pobj->GetReference(i); 
			if (r)
			{
				Class_ID x = r->ClassID();

				if (r && r->ClassID() == Class_ID(PARAMETER_BLOCK_CLASS_ID,0)) 
				{
					pb = (IParamBlock *) r;
				}
			}
		}
	}
	else 
	{
		// pb = pa->GetParamBlock2();
	}

	if (pb)
	{
	    // Find out how many _animatable_ parameters there are 
        int count = pb->NumSubs();

        // Display data about each one
        for (int i = 0; i < count; i++)
		{
			TSTR name = pb->SubAnimName(i);

			int pbIndex = pb->AnimNumToParamNum(i);

			TSTR cname;

			SClass_ID sc = pb->GetAnimParamControlType(i); 

			if (sc == CTRL_FLOAT_CLASS_ID) 

			cname = TSTR(_T("Float"));

			else if (sc == CTRL_POINT3_CLASS_ID) 

			cname = TSTR(_T("Point3"));
		}
	}



	// Get Node's object, convert to a triangle-mesh object, so I can access the Faces
	ObjectState os = pnode->EvalWorldState(m_tvToDump);
	pobj = os.obj;

	// Shouldn't have gotten this far if it's a helper object
	if (pobj->SuperClassID() == HELPER_CLASS_ID)
	{
		sprintf(st_szDBG, "ERROR--Helper node %s has an attached mesh, and it shouldn't.", (char*)strNodeName);
		ASSERT_AND_ABORT(FALSE, st_szDBG);
	}

	// convert mesh to triobject
	if (!pobj->CanConvertToType(triObjectClassID))
		return TREE_CONTINUE;
	TriObject *ptriobj = (TriObject*)pobj->ConvertToType(m_tvToDump, triObjectClassID);

	if (ptriobj == NULL)
		return TREE_CONTINUE;

	Mesh *pmesh = &ptriobj->mesh;
	BOOL deleteMesh = (ptriobj != pobj);

	// Ensure that the vertex normals are up-to-date
	pmesh->buildNormals();

	// We want the vertex coordinates in World-space, not object-space
	Matrix3 mat3ObjectTM = pnode->GetObjectTM(m_tvToDump);
	Matrix3 mat3ObjectNTM = mat3ObjectTM;
	mat3ObjectNTM.NoScale( );

	int cVerts = pmesh->getNumVerts();

	// it would be nice to just evaluate the object for both time periods, but MAX
	// may do EvalWorldState in place, so the vertex animations get stomped
	if (m_tvToDump == m_tvBase)
	{
		if (m_baseVert)
			m_baseVert = (Point3 *)realloc( m_baseVert, (m_baseVertCount + cVerts) * sizeof( Point3 ) );
		else
			m_baseVert = (Point3 *)malloc( (m_baseVertCount + cVerts) * sizeof( Point3 ) );

		for (int iVert = 0; iVert < cVerts; iVert++)
		{
			Point3 pt3Vertex1 = pmesh->getVert(iVert);
			int iAdjVert = m_baseVertCount + iVert;

			m_baseVert[iAdjVert] = pt3Vertex1 * mat3ObjectTM;

			Point3 pt3Normal1 = pmesh->getNormal(iVert);
			pt3Normal1 = VectorTransform( mat3ObjectNTM, pt3Normal1 );

			fprintf(m_pfile, "%5d %8.4f %8.4f %8.4f  %9.6f %9.6f %9.6f\n",
					iAdjVert, m_baseVert[iAdjVert].x, m_baseVert[iAdjVert].y, m_baseVert[iAdjVert].z,
					pt3Normal1.x, pt3Normal1.y, pt3Normal1.z );

		}
	}
	else
	{
		for (int iVert = 0; iVert < cVerts; iVert++)
		{
			Point3 pt3Vertex1 = pmesh->getVert(iVert);
			Point3 v1 = pt3Vertex1 * mat3ObjectTM;

			int iAdjVert = m_baseVertCount + iVert;

			if (Length( m_baseVert[iAdjVert] - v1) > 0.01)
			{
				Point3 pt3Vertex2 = pt3Vertex1;
				pt3Vertex2.z = pt3Vertex2.z + 10.0;
				pmesh->setVert( iVert, pt3Vertex2 );
				
				Point3 pt3Normal1 = pmesh->getNormal(iVert);
				pt3Normal1 = VectorTransform( mat3ObjectNTM, pt3Normal1 );

				fprintf(m_pfile, "%5d %8.4f %8.4f %8.4f  %9.6f %9.6f %9.6f\n",
						iAdjVert, v1.x, v1.y, v1.z, 
						pt3Normal1.x, pt3Normal1.y, pt3Normal1.z );
			}
		}
	}
	m_baseVertCount += cVerts;

	fflush( m_pfile );

	/*
	if (deleteMesh)
		delete pmesh;
	*/

	return TREE_CONTINUE;
}



Point3 DumpModelTEP::Pt3GetRVertexNormal(RVertex *prvertex, DWORD smGroupFace)
{
	// Lookup the appropriate vertex normal, based on smoothing group.
	int cNormals = prvertex->rFlags & NORCT_MASK;

	ASSERT_MBOX((cNormals == 1 && prvertex->ern == NULL) ||
				(cNormals > 1 && prvertex->ern != NULL), "BOGUS RVERTEX");
	
	if (cNormals == 1)
		return prvertex->rn.getNormal();
	else
	{
		for (int irn = 0; irn < cNormals; irn++)
			if (prvertex->ern[irn].getSmGroup() & smGroupFace)
				break;

		if (irn >= cNormals) 
		{
			irn = 0;
			// ASSERT_MBOX(irn < cNormals, "unknown smoothing group\n");
		}
		return prvertex->ern[irn].getNormal();
	}
}


DumpModelTEP::~DumpModelTEP()
{
	if (m_baseVert)
	{
		delete m_baseVert;
	}
}


//========================================================================
// Utility functions for getting/setting the personal "node index" property.
// NOTE: I'm storing a string-property because I hit a 3DSMax bug in v1.2 when I
// NOTE: tried using an integer property.
// FURTHER NOTE: those properties seem to change randomly sometimes, so I'm
// implementing my own.

typedef struct
{
	char	szNodeName[VtaExportClass::MAX_NAME_CHARS];
	int		iNode;
} NAMEMAP;
const int MAX_NAMEMAP = 512;
static NAMEMAP g_rgnm[MAX_NAMEMAP];

int GetIndexOfINode(INode *pnode, BOOL fAssertPropExists)
{
	TSTR strNodeName(pnode->GetName());
	for (int inm = 0; inm < g_inmMac; inm++)
		if (FStrEq(g_rgnm[inm].szNodeName, (char*)strNodeName))
			return g_rgnm[inm].iNode;
	if (fAssertPropExists)
		ASSERT_MBOX(FALSE, "No NODEINDEXSTR property");
	return -7777;
}

	
void SetIndexOfINode(INode *pnode, int inode)
{
	TSTR strNodeName(pnode->GetName());
	NAMEMAP *pnm;
	for (int inm = 0; inm < g_inmMac; inm++)
		if (FStrEq(g_rgnm[inm].szNodeName, (char*)strNodeName))
			break;
	if (inm < g_inmMac)
		pnm = &g_rgnm[inm];
	else
	{
		ASSERT_MBOX(g_inmMac < MAX_NAMEMAP, "NAMEMAP is full");
		pnm = &g_rgnm[g_inmMac++];
		strcpy(pnm->szNodeName, (char*)strNodeName);
	}
	pnm->iNode = inode;
}


//=============================================================
// Returns TRUE if a node should be ignored during tree traversal.
//
BOOL FUndesirableNode(INode *pnode)
{
	// Get Node's underlying object, and object class name
	Object *pobj = pnode->GetObjectRef();

	// Don't care about lights, dummies, and cameras
	if (pobj->SuperClassID() == CAMERA_CLASS_ID)
		return TRUE;
	if (pobj->SuperClassID() == LIGHT_CLASS_ID)
		return TRUE;

	return FALSE;

	// Actually, if it's not selected, pretend it doesn't exist!
	//if (!pnode->Selected())
	//	return TRUE;
	//return FALSE;
}


//=============================================================
// Returns TRUE if a node has been marked as skippable
//
BOOL FNodeMarkedToSkip(INode *pnode)
{
	return (::GetIndexOfINode(pnode) == VtaExportClass::UNDESIRABLE_NODE_MARKER);
}

	
//=============================================================
// Reduces a rotation to within the -2PI..2PI range.
//
static float FlReduceRotation(float fl)
{
	while (fl >= TWOPI)
		fl -= TWOPI;
	while (fl <= -TWOPI)
		fl += TWOPI;
	return fl;
}
