/*
===========================================================================

Doom 3 GPL Source Code
Copyright (C) 1999-2011 id Software LLC, a ZeniMax Media company.

This file is part of the Doom 3 GPL Source Code ("Doom 3 Source Code").

Doom 3 Source Code 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.

Doom 3 Source Code 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 Doom 3 Source Code.  If not, see <http://www.gnu.org/licenses/>.

In addition, the Doom 3 Source Code is also subject to certain additional terms. You should have received a copy of these additional terms immediately following the terms and conditions of the GNU General Public License which accompanied the Doom 3 Source Code.  If not, please request a copy in writing from id Software at the address below.

If you have questions concerning this license or the applicable additional terms, you may contact in writing id Software LLC, c/o ZeniMax Media Inc., Suite 120, Rockville, Maryland 20850 USA.

===========================================================================
*/

#include "sys/platform.h"

#include "Maya5.0/maya.h"
//#include "Maya6.0/maya.h"			// must also change include directory in project from "MayaImport\Maya4.5\include" to "MayaImport\Maya6.0\include" (requires MSDev 7.1)
#include "MayaImport/exporter.h"
#include "MayaImport/maya_main.h"

idStr	errorMessage;
bool	initialized = false;

#define DEFAULT_ANIM_EPSILON	0.125f
#define DEFAULT_QUAT_EPSILON	( 1.0f / 8192.0f )

#define SLOP_VERTEX				0.01f			// merge xyz coordinates this far apart
#define	SLOP_TEXCOORD			0.001f			// merge texture coordinates this far apart

const char *componentNames[ 6 ] = { "Tx", "Ty", "Tz", "Qx", "Qy", "Qz" };

idSys *			sys = NULL;
idCommon *		common = NULL;
idCVarSystem *	cvarSystem = NULL;

idCVar *		idCVar::staticVars = NULL;

/*
=================
MayaError
=================
*/
void MayaError( const char *fmt, ... ) {
	va_list	argptr;
	char	text[ 8192 ];

	va_start( argptr, fmt );
	idStr::vsnPrintf( text, sizeof( text ), fmt, argptr );
	va_end( argptr );

	throw idException( text );
}

/*
=================
FS_WriteFloatString
=================
*/
#define	MAX_PRINT_MSG	4096
static int WriteFloatString( FILE *file, const char *fmt, ... ) {
	long i;
	unsigned long u;
	double f;
	char *str;
	int index;
	idStr tmp, format;
	va_list argPtr;

	va_start( argPtr, fmt );

	index = 0;

	while( *fmt ) {
		switch( *fmt ) {
			case '%':
				format = "";
				format += *fmt++;
				while ( (*fmt >= '0' && *fmt <= '9') ||
						*fmt == '.' || *fmt == '-' || *fmt == '+' || *fmt == '#') {
					format += *fmt++;
				}
				format += *fmt;
				switch( *fmt ) {
					case 'f':
					case 'e':
					case 'E':
					case 'g':
					case 'G':
						f = va_arg( argPtr, double );
						if ( format.Length() <= 2 ) {
							// high precision floating point number without trailing zeros
							sprintf( tmp, "%1.10f", f );
							tmp.StripTrailing( '0' );
							tmp.StripTrailing( '.' );
							index += fprintf( file, "%s", tmp.c_str() );
						}
						else {
							index += fprintf( file, format.c_str(), f );
						}
						break;
					case 'd':
					case 'i':
						i = va_arg( argPtr, long );
						index += fprintf( file, format.c_str(), i );
						break;
					case 'u':
						u = va_arg( argPtr, unsigned long );
						index += fprintf( file, format.c_str(), u );
						break;
					case 'o':
						u = va_arg( argPtr, unsigned long );
						index += fprintf( file, format.c_str(), u );
						break;
					case 'x':
						u = va_arg( argPtr, unsigned long );
						index += fprintf( file, format.c_str(), u );
						break;
					case 'X':
						u = va_arg( argPtr, unsigned long );
						index += fprintf( file, format.c_str(), u );
						break;
					case 'c':
						i = va_arg( argPtr, long );
						index += fprintf( file, format.c_str(), (char) i );
						break;
					case 's':
						str = va_arg( argPtr, char * );
						index += fprintf( file, format.c_str(), str );
						break;
					case '%':
						index += fprintf( file, format.c_str() );
						break;
					default:
						MayaError( "WriteFloatString: invalid format %s", format.c_str() );
						break;
				}
				fmt++;
				break;
			case '\\':
				fmt++;
				switch( *fmt ) {
					case 't':
						index += fprintf( file, "\t" );
						break;
					case 'n':
						index += fprintf( file, "\n" );
					default:
						MayaError( "WriteFloatString: unknown escape character \'%c\'", *fmt );
						break;
				}
				fmt++;
				break;
			default:
				index += fprintf( file, "%c", *fmt );
				fmt++;
				break;
		}
	}

	va_end( argPtr );

	return index;
}

/*
================
OSPathToRelativePath

takes a full OS path, as might be found in data from a media creation
program, and converts it to a qpath by stripping off directories

Returns false if the osPath tree doesn't match any of the existing
search paths.
================
*/
bool OSPathToRelativePath( const char *osPath, idStr &qpath, const char *game ) {
	char *s, *base;

	// skip a drive letter?

	// search for anything with BASE_GAMEDIR in it
	// Ase files from max may have the form of:
	// "//Purgatory/purgatory/doom/base/models/mapobjects/bitch/hologirl.tga"
	// which won't match any of our drive letter based search paths
	base = (char *)strstr( osPath, BASE_GAMEDIR );

	// _D3XP added mod support
	if ( base == NULL && strlen(game) > 0 ) {

		base = s = (char *)strstr( osPath, game );

		while( s = strstr( s, game ) ) {
			s += strlen( game );
			if ( s[0] == '/' || s[0] == '\\' ) {
				base = s;
			}
		}
	}

	if ( base ) {
		s = strstr( base, "/" );
		if ( !s ) {
			s = strstr( base, "\\" );
		}
		if ( s ) {
			qpath = s + 1;
			return true;
		}
	}

	common->Printf( "OSPathToRelativePath failed on %s\n", osPath );
	qpath = osPath;

	return false;
}

/*
===============
ConvertFromIdSpace
===============
*/
idMat3 ConvertFromIdSpace( const idMat3 &idmat ) {
	idMat3 mat;

	mat[ 0 ][ 0 ] = idmat[ 0 ][ 0 ];
	mat[ 0 ][ 2 ] = -idmat[ 0 ][ 1 ];
	mat[ 0 ][ 1 ] = idmat[ 0 ][ 2 ];

	mat[ 1 ][ 0 ] = idmat[ 1 ][ 0 ];
	mat[ 1 ][ 2 ] = -idmat[ 1 ][ 1 ];
	mat[ 1 ][ 1 ] = idmat[ 1 ][ 2 ];

	mat[ 2 ][ 0 ] = idmat[ 2 ][ 0 ];
	mat[ 2 ][ 2 ] = -idmat[ 2 ][ 1 ];
	mat[ 2 ][ 1 ] = idmat[ 2 ][ 2 ];

	return mat;
}

/*
===============
ConvertFromIdSpace
===============
*/
idVec3 ConvertFromIdSpace( const idVec3 &idpos ) {
	idVec3 pos;

	pos.x = idpos.x;
	pos.z = -idpos.y;
	pos.y = idpos.z;

	return pos;
}

/*
===============
ConvertToIdSpace
===============
*/
idMat3 ConvertToIdSpace( const idMat3 &mat ) {
	idMat3 idmat;

	idmat[ 0 ][ 0 ] = mat[ 0 ][ 0 ];
	idmat[ 0 ][ 1 ] = -mat[ 0 ][ 2 ];
	idmat[ 0 ][ 2 ] = mat[ 0 ][ 1 ];

	idmat[ 1 ][ 0 ] = mat[ 1 ][ 0 ];
	idmat[ 1 ][ 1 ] = -mat[ 1 ][ 2 ];
	idmat[ 1 ][ 2 ] = mat[ 1 ][ 1 ];

	idmat[ 2 ][ 0 ] = mat[ 2 ][ 0 ];
	idmat[ 2 ][ 1 ] = -mat[ 2 ][ 2 ];
	idmat[ 2 ][ 2 ] = mat[ 2 ][ 1 ];

	return idmat;
}

/*
===============
ConvertToIdSpace
===============
*/
idVec3 ConvertToIdSpace( const idVec3 &pos ) {
	idVec3 idpos;

	idpos.x = pos.x;
	idpos.y = -pos.z;
	idpos.z = pos.y;

	return idpos;
}

/*
===============
idVec
===============
*/
idVec3 idVec( const MFloatPoint &point ) {
	return idVec3( point[ 0 ], point[ 1 ], point[ 2 ] );
}

/*
===============
idVec
===============
*/
idVec3 idVec( const MMatrix &matrix ) {
	return idVec3( matrix[ 3 ][ 0 ], matrix[ 3 ][ 1 ], matrix[ 3 ][ 2 ] );
}

/*
===============
idMat
===============
*/
idMat3 idMat( const MMatrix &matrix ) {
	int		j, k;
	idMat3	mat;

	for( j = 0; j < 3; j++ ) {
		for( k = 0; k < 3; k++ ) {
			mat[ j ][ k ] = matrix[ j ][ k ];
		}
	}

	return mat;
}

/*
===============
GetParent
===============
*/
MFnDagNode *GetParent( MFnDagNode *joint ) {
	MStatus		status;
	MObject		parentObject;

	parentObject = joint->parent( 0, &status );
	if ( !status && status.statusCode() == MStatus::kInvalidParameter ) {
		return NULL;
	}

	while( !parentObject.hasFn( MFn::kTransform ) ) {
		MFnDagNode parentNode( parentObject, &status );
		if ( !status ) {
			return NULL;
		}

		parentObject = parentNode.parent( 0, &status );
		if ( !status && status.statusCode() == MStatus::kInvalidParameter ) {
			return NULL;
		}
	}

	MFnDagNode *parentNode;

	parentNode = new MFnDagNode( parentObject, &status );
	if ( !status ) {
		delete parentNode;
		return NULL;
	}

	return parentNode;
}

/*
==============================================================================================

	idTokenizer

==============================================================================================
*/

/*
====================
idTokenizer::SetTokens
====================
*/
int idTokenizer::SetTokens( const char *buffer ) {
	const char *cmd;

	Clear();

	// tokenize commandline
	cmd = buffer;
	while ( *cmd ) {
		// skip whitespace
		while( *cmd && isspace( *cmd ) ) {
			cmd++;
		}

		if ( !*cmd ) {
			break;
		}

		idStr &current = tokens.Alloc();
		while( *cmd && !isspace( *cmd ) ) {
			current += *cmd;
			cmd++;
		}
	}

	return tokens.Num();
}

/*
====================
idTokenizer::NextToken
====================
*/
const char *idTokenizer::NextToken( const char *errorstring ) {
	if ( currentToken < tokens.Num() ) {
		return tokens[ currentToken++ ];
	}

	if ( errorstring ) {
		MayaError( "Error: %s", errorstring );
	}

	return NULL;
}

/*
==============================================================================================

	idExportOptions

==============================================================================================
*/

/*
====================
idExportOptions::Reset
====================
*/
void idExportOptions::Reset( const char *commandline ) {
	scale			= 1.0f;
	type			= WRITE_MESH;
	startframe		= -1;
	endframe		= -1;
	ignoreMeshes	= false;
	clearOrigin		= false;
	clearOriginAxis	= false;
	framerate		= 24;
	align			= "";
	rotate			= 0.0f;
	commandLine		= commandline;
	prefix			= "";
	jointThreshold	= 0.05f;
	ignoreScale		= false;
	xyzPrecision	= DEFAULT_ANIM_EPSILON;
	quatPrecision	= DEFAULT_QUAT_EPSILON;
	cycleStart		= -1;

	src.Clear();
	dest.Clear();

	tokens.SetTokens( commandline );

	keepjoints.Clear();
	renamejoints.Clear();
	remapjoints.Clear();
	exportgroups.Clear();
	skipmeshes.Clear();
	keepmeshes.Clear();
	groups.Clear();
}

/*
====================
idExportOptions::idExportOptions
====================
*/
idExportOptions::idExportOptions( const char *commandline, const char *ospath ) {
	idStr		token;
	idNamePair	joints;
	int			i;
	idAnimGroup	*group;
	idStr		sourceDir;
	idStr		destDir;

	Reset( commandline );

	token = tokens.NextToken( "Missing export command" );
	if ( token == "mesh" ) {
		type = WRITE_MESH;
	} else if ( token == "anim" ) {
		type = WRITE_ANIM;
	} else if ( token == "camera" ) {
		type = WRITE_CAMERA;
	} else {
		MayaError( "Unknown export command '%s'", token.c_str() );
	}

	src = tokens.NextToken( "Missing source filename" );
	dest = src;

	for( token = tokens.NextToken(); token != ""; token = tokens.NextToken() ) {
		if ( token == "-force" ) {
			// skip
		} else if ( token == "-game" ) {
			// parse game name
			game = tokens.NextToken( "Expecting game name after -game" );

		} else if ( token == "-rename" ) {
			// parse joint to rename
			joints.from = tokens.NextToken( "Missing joint name for -rename.  Usage: -rename [joint name] [new name]" );
			joints.to	= tokens.NextToken( "Missing new name for -rename.  Usage: -rename [joint name] [new name]" );
			renamejoints.Append( joints );

		} else if ( token == "-prefix" ) {
			prefix = tokens.NextToken( "Missing name for -prefix.  Usage: -prefix [joint prefix]" );

		} else if ( token == "-parent" ) {
			// parse joint to reparent
			joints.from = tokens.NextToken( "Missing joint name for -parent.  Usage: -parent [joint name] [new parent]" );
			joints.to	= tokens.NextToken( "Missing new parent for -parent.  Usage: -parent [joint name] [new parent]" );
			remapjoints.Append( joints );

		} else if ( !token.Icmp( "-sourcedir" ) ) {
			// parse source directory
			sourceDir = tokens.NextToken( "Missing filename for -sourcedir.  Usage: -sourcedir [directory]" );

		} else if ( !token.Icmp( "-destdir" ) ) {
			// parse destination directory
			destDir = tokens.NextToken( "Missing filename for -destdir.  Usage: -destdir [directory]" );

		} else if ( token == "-dest" ) {
			// parse destination filename
			dest = tokens.NextToken( "Missing filename for -dest.  Usage: -dest [filename]" );

		} else if ( token == "-range" ) {
			// parse frame range to export
			token		= tokens.NextToken( "Missing start frame for -range.  Usage: -range [start frame] [end frame]" );
			startframe	= atoi( token );
			token		= tokens.NextToken( "Missing end frame for -range.  Usage: -range [start frame] [end frame]" );
			endframe	= atoi( token );

			if ( startframe > endframe ) {
				MayaError( "Start frame is greater than end frame." );
			}

		} else if ( !token.Icmp( "-cycleStart" ) ) {
			// parse start frame of cycle
			token		= tokens.NextToken( "Missing cycle start frame for -cycleStart.  Usage: -cycleStart [first frame of cycle]" );
			cycleStart	= atoi( token );

		} else if ( token == "-scale" ) {
			// parse scale
			token	= tokens.NextToken( "Missing scale amount for -scale.  Usage: -scale [scale amount]" );
			scale	= atof( token );

		} else if ( token == "-align" ) {
			// parse align joint
			align = tokens.NextToken( "Missing joint name for -align.  Usage: -align [joint name]" );

		} else if ( token == "-rotate" ) {
			// parse angle rotation
			token	= tokens.NextToken( "Missing value for -rotate.  Usage: -rotate [yaw]" );
			rotate	= -atof( token );

		} else if ( token == "-nomesh" ) {
			ignoreMeshes = true;

		} else if ( token == "-clearorigin" ) {
			clearOrigin = true;
			clearOriginAxis = true;

		} else if ( token == "-clearoriginaxis" ) {
			clearOriginAxis = true;

		} else if ( token == "-ignorescale" ) {
			ignoreScale = true;

		} else if ( token == "-xyzprecision" ) {
			// parse quaternion precision
			token = tokens.NextToken( "Missing value for -xyzprecision.  Usage: -xyzprecision [precision]" );
			xyzPrecision = atof( token );
			if ( xyzPrecision < 0.0f ) {
				MayaError( "Invalid value for -xyzprecision.  Must be >= 0" );
			}

		} else if ( token == "-quatprecision" ) {
			// parse quaternion precision
			token = tokens.NextToken( "Missing value for -quatprecision.  Usage: -quatprecision [precision]" );
			quatPrecision = atof( token );
			if ( quatPrecision < 0.0f ) {
				MayaError( "Invalid value for -quatprecision.  Must be >= 0" );
			}

		} else if ( token == "-jointthreshold" ) {
			// parse joint threshold
			token			= tokens.NextToken( "Missing weight for -jointthreshold.  Usage: -jointthreshold [minimum joint weight]" );
			jointThreshold	= atof( token );

		} else if ( token == "-skipmesh" ) {
			token = tokens.NextToken( "Missing name for -skipmesh.  Usage: -skipmesh [name of mesh to skip]" );
			skipmeshes.AddUnique( token );

		} else if ( token == "-keepmesh" ) {
			token = tokens.NextToken( "Missing name for -keepmesh.  Usage: -keepmesh [name of mesh to keep]" );
			keepmeshes.AddUnique( token );

		} else if ( token == "-jointgroup" ) {
			token	= tokens.NextToken( "Missing name for -jointgroup.  Usage: -jointgroup [group name] [joint1] [joint2]...[joint n]" );
			group = groups.Ptr();
			for( i = 0; i < groups.Num(); i++, group++ ) {
				if ( group->name == token ) {
					break;
				}
			}

			if ( i >= groups.Num() ) {
				// create a new group
				group = &groups.Alloc();
				group->name = token;
			}

			while( tokens.TokenAvailable() ) {
				token = tokens.NextToken();
				if ( token[ 0 ] == '-' ) {
					tokens.UnGetToken();
					break;
				}

				group->joints.AddUnique( token );
			}
		} else if ( token == "-group" ) {
			// add the list of groups to export (these don't affect the hierarchy)
			while( tokens.TokenAvailable() ) {
				token = tokens.NextToken();
				if ( token[ 0 ] == '-' ) {
					tokens.UnGetToken();
					break;
				}

				group = groups.Ptr();
				for( i = 0; i < groups.Num(); i++, group++ ) {
					if ( group->name == token ) {
						break;
					}
				}

				if ( i >= groups.Num() ) {
					MayaError( "Unknown group '%s'", token.c_str() );
				}

				exportgroups.AddUnique( group );
			}
		} else if ( token == "-keep" ) {
			// add joints that are kept whether they're used by a mesh or not
			while( tokens.TokenAvailable() ) {
				token = tokens.NextToken();
				if ( token[ 0 ] == '-' ) {
					tokens.UnGetToken();
					break;
				}
				keepjoints.AddUnique( token );
			}
		} else {
			MayaError( "Unknown option '%s'", token.c_str() );
		}
	}

	token = src;
	src = ospath;
	src.BackSlashesToSlashes();
	src.AppendPath( sourceDir );
	src.AppendPath( token );

	token = dest;
	dest = ospath;
	dest.BackSlashesToSlashes();
	dest.AppendPath( destDir );
	dest.AppendPath( token );

	// Maya only accepts unix style path separators
	src.BackSlashesToSlashes();
	dest.BackSlashesToSlashes();

	if ( skipmeshes.Num() && keepmeshes.Num() ) {
		MayaError( "Can't use -keepmesh and -skipmesh together." );
	}
}

/*
====================
idExportOptions::jointInExportGroup
====================
*/
bool idExportOptions::jointInExportGroup( const char *jointname ) {
	int			i;
	int			j;
	idAnimGroup	*group;

	if ( !exportgroups.Num() ) {
		// if we don't have any groups specified as export then export every joint
		return true;
	}

	// search through all exported groups to see if this joint is exported
	for( i = 0; i < exportgroups.Num(); i++ ) {
		group = exportgroups[ i ];
		for( j = 0; j < group->joints.Num(); j++ ) {
			if ( group->joints[ j ] == jointname ) {
				return true;
			}
		}
	}

	return false;
}

/*
==============================================================================

idExportJoint

==============================================================================
*/

idExportJoint::idExportJoint() {
	index				= 0;
	exportNum			= 0;

	mayaNode.SetOwner( this );
	exportNode.SetOwner( this );

	dagnode				= NULL;

	t					= vec3_zero;
	wm					= mat3_default;
	bindpos				= vec3_zero;
	bindmat				= mat3_default;
	keep				= false;
	scale				= 1.0f;
	invscale			= 1.0f;
	animBits			= 0;
	firstComponent		= 0;
	baseFrame.q.Set( 0.0f, 0.0f, 0.0f );
	baseFrame.t.Zero();
}

idExportJoint &idExportJoint::operator=( const idExportJoint &other ) {
	name		= other.name;
	realname	= other.realname;
	longname	= other.longname;
	index		= other.index;
	exportNum	= other.exportNum;
	keep		= other.keep;

	scale		= other.scale;
	invscale	= other.invscale;

	dagnode		= other.dagnode;

	mayaNode	= other.mayaNode;
	exportNode	= other.exportNode;

	t			= other.t;
	idt			= other.idt;
	wm			= other.wm;
	idwm		= other.idwm;
	bindpos		= other.bindpos;
	bindmat		= other.bindmat;

	animBits	= other.animBits;
	firstComponent = other.firstComponent;
	baseFrame	= other.baseFrame;

	mayaNode.SetOwner( this );
	exportNode.SetOwner( this );

	return *this;
}

/*
==============================================================================

idExportMesh

==============================================================================
*/

void idExportMesh::ShareVerts( void ) {
	int i, j, k;
	exportVertex_t vert;
	idList<exportVertex_t> v;

	v = verts;
	verts.Clear();
	for( i = 0; i < tris.Num(); i++ ) {
		for( j = 0; j < 3; j++ ) {
			vert = v[ tris[ i ].indexes[ j ] ];
			vert.texCoords[ 0 ] = uv[ i ].uv[ j ][ 0 ];
			vert.texCoords[ 1 ] = 1.0f - uv[ i ].uv[ j ][ 1 ];

			for( k = 0; k < verts.Num(); k++ ) {
				if ( vert.numWeights != verts[ k ].numWeights ) {
					continue;
				}
				if ( vert.startweight != verts[ k ].startweight ) {
					continue;
				}
				if ( !vert.pos.Compare( verts[ k ].pos, SLOP_VERTEX ) ) {
					continue;
				}
				if ( !vert.texCoords.Compare( verts[ k ].texCoords, SLOP_TEXCOORD ) ) {
					continue;
				}

				break;
			}

			if ( k < verts.Num() ) {
				tris[ i ].indexes[ j ] = k;
			} else {
				tris[ i ].indexes[ j ] = verts.Append( vert );
			}
		}
	}
}

void idExportMesh::Merge( idExportMesh *mesh ) {
	int i;
	int numverts;
	int numtris;
	int numweights;
	int numuvs;

	// merge name
	sprintf( name, "%s, %s", name.c_str(), mesh->name.c_str() );

	// merge verts
	numverts = verts.Num();
	verts.SetNum( numverts + mesh->verts.Num() );
	for( i = 0; i < mesh->verts.Num(); i++ ) {
		verts[ numverts + i ] = mesh->verts[ i ];
		verts[ numverts + i ].startweight += weights.Num();
	}

	// merge triangles
	numtris = tris.Num();
	tris.SetNum( numtris + mesh->tris.Num() );
	for( i = 0; i < mesh->tris.Num(); i++ ) {
		tris[ numtris + i ].indexes[ 0 ] = mesh->tris[ i ].indexes[ 0 ] + numverts;
		tris[ numtris + i ].indexes[ 1 ] = mesh->tris[ i ].indexes[ 1 ] + numverts;
		tris[ numtris + i ].indexes[ 2 ] = mesh->tris[ i ].indexes[ 2 ] + numverts;
	}

	// merge weights
	numweights = weights.Num();
	weights.SetNum( numweights + mesh->weights.Num() );
	for( i = 0; i < mesh->weights.Num(); i++ ) {
		weights[ numweights + i ] = mesh->weights[ i ];
	}

	// merge uvs
	numuvs = uv.Num();
	uv .SetNum( numuvs + mesh->uv.Num() );
	for( i = 0; i < mesh->uv.Num(); i++ ) {
		uv[ numuvs + i ] = mesh->uv[ i ];
	}
}

void idExportMesh::GetBounds( idBounds &bounds ) const {
	int						i;
	int						j;
	idVec3					pos;
	const exportWeight_t	*weight;
	const exportVertex_t	*vert;

	bounds.Clear();

	weight = weights.Ptr();
	vert = verts.Ptr();
	for( i = 0; i < verts.Num(); i++, vert++ ) {
		pos.Zero();
		weight = &weights[ vert->startweight ];
		for( j = 0; j < vert->numWeights; j++, weight++ ) {
			pos += weight->jointWeight * ( weight->joint->idwm * weight->offset + weight->joint->idt );
		}
		bounds.AddPoint( pos );
	}
}

/*
==============================================================================

idExportModel

==============================================================================
*/

/*
====================
idExportModel::idExportModel
====================
*/
ID_INLINE idExportModel::idExportModel() {
	export_joints		= 0;
	skipjoints			= 0;
	frameRate			= 24;
	numFrames			= 0;
	exportOrigin		= NULL;
}

/*
====================
idExportModel::~idExportModel
====================
*/
ID_INLINE idExportModel::~idExportModel() {
	meshes.DeleteContents( true );
}

idExportJoint *idExportModel::FindJointReal( const char *name ) {
	idExportJoint	*joint;
	int				i;

	joint = joints.Ptr();
	for( i = 0; i < joints.Num(); i++, joint++ ) {
		if ( joint->realname == name ) {
			return joint;
		}
	}

	return NULL;
}

idExportJoint *idExportModel::FindJoint( const char *name ) {
	idExportJoint	*joint;
	int				i;

	joint = joints.Ptr();
	for( i = 0; i < joints.Num(); i++, joint++ ) {
		if ( joint->name == name ) {
			return joint;
		}
	}

	return NULL;
}

bool idExportModel::WriteMesh( const char *filename, idExportOptions &options ) {
	int				i, j;
	int				numMeshes;
	idExportMesh	*mesh;
	idExportJoint	*joint;
	idExportJoint	*parent;
	idExportJoint	*sibling;
	FILE			*file;
	const char		*parentName;
	int				parentNum;
	idList<idExportJoint *> jointList;

	file = fopen( filename, "w" );
	if ( !file ) {
		return false;
	}

	for( joint = exportHead.GetNext(); joint != NULL; joint = joint->exportNode.GetNext() ) {
		jointList.Append( joint );
	}

	for( i = 0; i < jointList.Num(); i++ ) {
		joint = jointList[ i ];
		sibling = joint->exportNode.GetSibling();
		while( sibling ) {
			if ( idStr::Cmp( joint->name, sibling->name ) > 0 ) {
				joint->exportNode.MakeSiblingAfter( sibling->exportNode );
				sibling = joint->exportNode.GetSibling();
			} else {
				sibling = sibling->exportNode.GetSibling();
			}
		}
	}

	jointList.Clear();
	for( joint = exportHead.GetNext(); joint != NULL; joint = joint->exportNode.GetNext() ) {
		joint->exportNum = jointList.Append( joint );
	}

	numMeshes = 0;
	if ( !options.ignoreMeshes ) {
		for( i = 0; i < meshes.Num(); i++ ) {
			if ( meshes[ i ]->keep ) {
				numMeshes++;
			}
		}
	}

	// write version info
	WriteFloatString( file, MD5_VERSION_STRING " %d\n", MD5_VERSION );
	WriteFloatString( file, "commandline \"%s\"\n\n", options.commandLine.c_str() );

	// write joints
	WriteFloatString( file, "numJoints %d\n", jointList.Num() );
	WriteFloatString( file, "numMeshes %d\n\n", numMeshes );

	WriteFloatString( file, "joints {\n" );
	for( i = 0; i < jointList.Num(); i++ ) {
		joint = jointList[ i ];
		parent = joint->exportNode.GetParent();
		if ( parent ) {
			parentNum = parent->exportNum;
			parentName = parent->name.c_str();
		} else {
			parentNum = -1;
			parentName = "";
		}

		idCQuat	bindQuat = joint->bindmat.ToQuat().ToCQuat();
		WriteFloatString( file, "\t\"%s\"\t%d ( %f %f %f ) ( %f %f %f )\t\t// %s\n", joint->name.c_str(), parentNum,
			joint->bindpos.x, joint->bindpos.y, joint->bindpos.z, bindQuat[ 0 ], bindQuat[ 1 ], bindQuat[ 2 ], parentName );
	}
	WriteFloatString( file, "}\n" );

	// write meshes
	for( i = 0; i < meshes.Num(); i++ ) {
		mesh = meshes[ i ];
		if ( !mesh->keep ) {
			continue;
		}

		WriteFloatString( file, "\nmesh {\n" );
		WriteFloatString( file, "\t// meshes: %s\n", mesh->name.c_str() );
		WriteFloatString( file, "\tshader \"%s\"\n", mesh->shader.c_str() );

		WriteFloatString( file, "\n\tnumverts %d\n", mesh->verts.Num() );
		for( j = 0; j < mesh->verts.Num(); j++ ) {
			WriteFloatString( file, "\tvert %d ( %f %f ) %d %d\n", j, mesh->verts[ j ].texCoords[ 0 ], mesh->verts[ j ].texCoords[ 1 ],
				mesh->verts[ j ].startweight, mesh->verts[ j ].numWeights );
		}

		WriteFloatString( file, "\n\tnumtris %d\n", mesh->tris.Num() );
		for( j = 0; j < mesh->tris.Num(); j++ ) {
			WriteFloatString( file, "\ttri %d %d %d %d\n", j, mesh->tris[ j ].indexes[ 2 ], mesh->tris[ j ].indexes[ 1 ], mesh->tris[ j ].indexes[ 0 ] );
		}

		WriteFloatString( file, "\n\tnumweights %d\n", mesh->weights.Num() );
		for( j = 0; j < mesh->weights.Num(); j++ ) {
			exportWeight_t *weight;

			weight = &mesh->weights[ j ];
			WriteFloatString( file, "\tweight %d %d %f ( %f %f %f )\n", j,
				weight->joint->exportNum, weight->jointWeight, weight->offset.x, weight->offset.y, weight->offset.z );
		}

		WriteFloatString( file, "}\n" );
	}

	fclose( file );

	return true;
}

bool idExportModel::WriteAnim( const char *filename, idExportOptions &options ) {
	int				i, j;
	idExportJoint	*joint;
	idExportJoint	*parent;
	idExportJoint	*sibling;
	jointFrame_t	*frame;
	FILE			*file;
	int				numAnimatedComponents;
	idList<idExportJoint *> jointList;

	file = fopen( filename, "w" );
	if ( !file ) {
		return false;
	}

	for( joint = exportHead.GetNext(); joint != NULL; joint = joint->exportNode.GetNext() ) {
		jointList.Append( joint );
	}

	for( i = 0; i < jointList.Num(); i++ ) {
		joint = jointList[ i ];
		sibling = joint->exportNode.GetSibling();
		while( sibling ) {
			if ( idStr::Cmp( joint->name, sibling->name ) > 0 ) {
				joint->exportNode.MakeSiblingAfter( sibling->exportNode );
				sibling = joint->exportNode.GetSibling();
			} else {
				sibling = sibling->exportNode.GetSibling();
			}
		}
	}

	jointList.Clear();
	for( joint = exportHead.GetNext(); joint != NULL; joint = joint->exportNode.GetNext() ) {
		joint->exportNum = jointList.Append( joint );
	}

	numAnimatedComponents = 0;
	for( i = 0; i < jointList.Num(); i++ ) {
		joint = jointList[ i ];
		joint->exportNum = i;
		joint->baseFrame = frames[ 0 ][ joint->index ];
		joint->animBits = 0;
		for( j = 1; j < numFrames; j++ ) {
			frame = &frames[ j ][ joint->index ];
			if ( fabs( frame->t[ 0 ] - joint->baseFrame.t[ 0 ] ) > options.xyzPrecision ) {
				joint->animBits |= ANIM_TX;
			}
			if ( fabs( frame->t[ 1 ] - joint->baseFrame.t[ 1 ] ) > options.xyzPrecision ) {
				joint->animBits |= ANIM_TY;
			}
			if ( fabs( frame->t[ 2 ] - joint->baseFrame.t[ 2 ] ) > options.xyzPrecision ) {
				joint->animBits |= ANIM_TZ;
			}
			if ( fabs( frame->q[ 0 ] - joint->baseFrame.q[ 0 ] ) > options.quatPrecision ) {
				joint->animBits |= ANIM_QX;
			}
			if ( fabs( frame->q[ 1 ] - joint->baseFrame.q[ 1 ] ) > options.quatPrecision ) {
				joint->animBits |= ANIM_QY;
			}
			if ( fabs( frame->q[ 2 ] - joint->baseFrame.q[ 2 ] ) > options.quatPrecision ) {
				joint->animBits |= ANIM_QZ;
			}
			if ( ( joint->animBits & 63 ) == 63 ) {
				break;
			}
		}
		if ( joint->animBits ) {
			joint->firstComponent = numAnimatedComponents;
			for( j = 0; j < 6; j++ ) {
				if ( joint->animBits & BIT( j ) ) {
					numAnimatedComponents++;
				}
			}
		}
	}

	// write version info
	WriteFloatString( file, MD5_VERSION_STRING " %d\n", MD5_VERSION );
	WriteFloatString( file, "commandline \"%s\"\n\n", options.commandLine.c_str() );

	WriteFloatString( file, "numFrames %d\n", numFrames );
	WriteFloatString( file, "numJoints %d\n", jointList.Num() );
	WriteFloatString( file, "frameRate %d\n", frameRate );
	WriteFloatString( file, "numAnimatedComponents %d\n", numAnimatedComponents );

	// write out the hierarchy
	WriteFloatString( file, "\nhierarchy {\n" );
	for( i = 0; i < jointList.Num(); i++ ) {
		joint = jointList[ i ];
		parent = joint->exportNode.GetParent();
		if ( parent ) {
			WriteFloatString( file, "\t\"%s\"\t%d %d %d\t// %s", joint->name.c_str(), parent->exportNum, joint->animBits, joint->firstComponent, parent->name.c_str() );
		} else {
			WriteFloatString( file, "\t\"%s\"\t-1 %d %d\t//", joint->name.c_str(), joint->animBits, joint->firstComponent );
		}

		if ( !joint->animBits ) {
			WriteFloatString( file, "\n" );
		} else {
			WriteFloatString( file, " ( " );
			for( j = 0; j < 6; j++ ) {
				if ( joint->animBits & BIT( j ) ) {
					WriteFloatString( file, "%s ", componentNames[ j ] );
				}
			}
			WriteFloatString( file, ")\n" );
		}
	}
	WriteFloatString( file, "}\n" );

	// write the frame bounds
	WriteFloatString( file, "\nbounds {\n" );
	for( i = 0; i < numFrames; i++ ) {
		WriteFloatString( file, "\t( %f %f %f ) ( %f %f %f )\n", bounds[ i ][ 0 ].x, bounds[ i ][ 0 ].y, bounds[ i ][ 0 ].z, bounds[ i ][ 1 ].x, bounds[ i ][ 1 ].y, bounds[ i ][ 1 ].z );
	}
	WriteFloatString( file, "}\n" );

	// write the base frame
	WriteFloatString( file, "\nbaseframe {\n" );
	for( i = 0; i < jointList.Num(); i++ ) {
		joint = jointList[ i ];
		WriteFloatString( file, "\t( %f %f %f ) ( %f %f %f )\n", joint->baseFrame.t[ 0 ], joint->baseFrame.t[ 1 ], joint->baseFrame.t[ 2 ],
			joint->baseFrame.q[ 0 ], joint->baseFrame.q[ 1 ], joint->baseFrame.q[ 2 ] );
	}
	WriteFloatString( file, "}\n" );

	// write the frames
	for( i = 0; i < numFrames; i++ ) {
		WriteFloatString( file, "\nframe %d {\n", i );
		for( j = 0; j < jointList.Num(); j++ ) {
			joint = jointList[ j ];
			frame = &frames[ i ][ joint->index ];
			if ( joint->animBits ) {
				WriteFloatString( file, "\t" );
				if ( joint->animBits & ANIM_TX ) {
					WriteFloatString( file, " %f", frame->t[ 0 ] );
				}
				if ( joint->animBits & ANIM_TY ) {
					WriteFloatString( file, " %f", frame->t[ 1 ] );
				}
				if ( joint->animBits & ANIM_TZ ) {
					WriteFloatString( file, " %f", frame->t[ 2 ] );
				}
				if ( joint->animBits & ANIM_QX ) {
					WriteFloatString( file, " %f", frame->q[ 0 ] );
				}
				if ( joint->animBits & ANIM_QY ) {
					WriteFloatString( file, " %f", frame->q[ 1 ] );
				}
				if ( joint->animBits & ANIM_QZ ) {
					WriteFloatString( file, " %f", frame->q[ 2 ] );
				}
				WriteFloatString( file, "\n" );
			}
		}
		WriteFloatString( file, "}\n" );
	}

	fclose( file );

	return true;
}

bool idExportModel::WriteCamera( const char *filename, idExportOptions &options ) {
	int		i;
	FILE	*file;

	file = fopen( filename, "w" );
	if ( !file ) {
		return false;
	}

	// write version info
	WriteFloatString( file, MD5_VERSION_STRING " %d\n", MD5_VERSION );
	WriteFloatString( file, "commandline \"%s\"\n\n", options.commandLine.c_str() );

	WriteFloatString( file, "numFrames %d\n", camera.Num() );
	WriteFloatString( file, "frameRate %d\n", frameRate );
	WriteFloatString( file, "numCuts %d\n", cameraCuts.Num() );

	// write out the cuts
	WriteFloatString( file, "\ncuts {\n" );
	for( i = 0; i < cameraCuts.Num(); i++ ) {
		WriteFloatString( file, "\t%d\n", cameraCuts[ i ] );
	}
	WriteFloatString( file, "}\n" );

	// write out the frames
	WriteFloatString( file, "\ncamera {\n" );
	cameraFrame_t *frame = camera.Ptr();
	for( i = 0; i < camera.Num(); i++, frame++ ) {
		WriteFloatString( file, "\t( %f %f %f ) ( %f %f %f ) %f\n", frame->t.x, frame->t.y, frame->t.z, frame->q[ 0 ], frame->q[ 1 ], frame->q[ 2 ], frame->fov );
	}
	WriteFloatString( file, "}\n" );

	fclose( file );

	return true;
}

/*
==============================================================================

Maya

==============================================================================
*/

/*
===============
idMayaExport::~idMayaExport

===============
*/
idMayaExport::~idMayaExport() {
	FreeDagNodes();

	// free up the file in Maya
	MFileIO::newFile( true );
}

/*
===============
idMayaExport::TimeForFrame
===============
*/
float idMayaExport::TimeForFrame( int num ) const {
	MTime	time;

	// set time unit to 24 frames per second
	time.setUnit( MTime::kFilm );
	time.setValue( num );
	return time.as( MTime::kSeconds );
}

/*
===============
idMayaExport::GetMayaFrameNum
===============
*/
int idMayaExport::GetMayaFrameNum( int num ) const {
	int	frameNum;

	if ( options.cycleStart > options.startframe ) {
		// in cycles, the last frame is a duplicate of the first frame, so with cycleStart we need to
		// duplicate one of the interior frames instead and chop off the first frame.
		frameNum = options.cycleStart + num;
		if ( frameNum > options.endframe ) {
			frameNum -= options.endframe - options.startframe;
		}
		if ( frameNum < options.startframe ) {
			frameNum  = options.startframe + 1;
		}
	} else {
		frameNum = options.startframe + num;
		if ( frameNum > options.endframe ) {
			frameNum -= options.endframe + 1 - options.startframe;
		}
		if ( frameNum < options.startframe ) {
			frameNum  = options.startframe;
		}
	}

	return frameNum;
}

/*
===============
idMayaExport::SetFrame
===============
*/
void idMayaExport::SetFrame( int num ) {
	MTime	time;
	int		frameNum;

	frameNum = GetMayaFrameNum( num );

	// set time unit to 24 frames per second
	time.setUnit( MTime::kFilm );
	time.setValue( frameNum );
	MGlobal::viewFrame( time );
}

/*
===============
idMayaExport::PruneJoints
===============
*/
void idMayaExport::PruneJoints( idStrList &keepjoints, idStr &prefix ) {
	int				i;
	int				j;
	idExportMesh	*mesh;
	idExportJoint	*joint;
	idExportJoint	*joint2;
	idExportJoint	*parent;
	int				num_weights;

	// if we don't have any joints specified to keep, mark the ones used by the meshes as keep
	if ( !keepjoints.Num() && !prefix.Length() ) {
		if ( !model.meshes.Num() || options.ignoreMeshes ) {
			// export all joints
			joint = model.joints.Ptr();
			for( i = 0; i < model.joints.Num(); i++, joint++ ) {
				joint->keep = true;
			}
		} else {
			for( i = 0; i < model.meshes.Num(); i++, mesh++ ) {
				mesh = model.meshes[ i ];
				for( j = 0; j < mesh->weights.Num(); j++ ) {
					mesh->weights[ j ].joint->keep = true;
				}
			}
		}
	} else {
		// mark the joints to keep
		for( i = 0; i < keepjoints.Num(); i++ ) {
			joint = model.FindJoint( keepjoints[ i ] );
			if ( joint ) {
				joint->keep = true;
			}
		}

		// count valid meshes
		for( i = 0; i < model.meshes.Num(); i++ ) {
			mesh = model.meshes[ i ];
			num_weights = 0;
			for( j = 0; j < mesh->weights.Num(); j++ ) {
				if ( mesh->weights[ j ].joint->keep ) {
					num_weights++;
				} else if ( prefix.Length() && !mesh->weights[ j ].joint->realname.Cmpn( prefix, prefix.Length() ) ) {
					// keep the joint if it's used by the mesh and it has the right prefix
					mesh->weights[ j ].joint->keep = true;
					num_weights++;
				}
			}

			if ( num_weights != mesh->weights.Num() ) {
				mesh->keep = false;
			}
		}
	}

	// find all joints aren't exported and reparent joint's children
	model.export_joints = 0;
	joint = model.joints.Ptr();
	for( i = 0; i < model.joints.Num(); i++, joint++ ) {
		if ( !joint->keep ) {
			joint->exportNode.RemoveFromHierarchy();
		} else {
			joint->index = model.export_joints;
			model.export_joints++;

			// make sure we are parented to an exported joint
			for( parent = joint->exportNode.GetParent(); parent != NULL; parent = parent->exportNode.GetParent() ) {
				if ( parent->keep ) {
					break;
				}
			}

			if ( parent != NULL ) {
				joint->exportNode.ParentTo( parent->exportNode );
			} else {
				joint->exportNode.ParentTo( model.exportHead );
			}
		}
	}

	// check if we have any duplicate joint names
	for( joint = model.exportHead.GetNext(); joint != NULL; joint = joint->exportNode.GetNext() ) {
		if ( !joint->keep ) {
			MayaError( "Non-kept joint in export tree ('%s')", joint->name.c_str() );
		}

		for( joint2 = model.exportHead.GetNext(); joint2 != NULL; joint2 = joint2->exportNode.GetNext() ) {
			if ( ( joint2 != joint ) && ( joint2->name == joint->name ) ) {
				MayaError( "Two joints found with the same name ('%s')", joint->name.c_str() );
			}
		}
	}
}

/*
===============
idMayaExport::FreeDagNodes
===============
*/
void idMayaExport::FreeDagNodes( void ) {
	int i;

	for( i = 0; i < model.joints.Num(); i++ ) {
		delete model.joints[ i ].dagnode;
		model.joints[ i ].dagnode = NULL;
	}
}

/*
===============
idMayaExport::GetBindPose
===============
*/
void idMayaExport::GetBindPose( MObject &jointNode, idExportJoint *joint, float scale ) {
	MStatus				status;
	MFnDependencyNode	fnJoint( jointNode );
	MObject				aBindPose = fnJoint.attribute( "bindPose", &status );

	joint->bindpos = vec3_zero;
	joint->bindmat = mat3_default;

	if ( MS::kSuccess == status ) {
		unsigned	ii;
		unsigned	jointIndex;
		unsigned	connLength;
		MPlugArray	connPlugs;
		MPlug		pBindPose( jointNode, aBindPose );

		pBindPose.connectedTo( connPlugs, false, true );
		connLength = connPlugs.length();
		for( ii = 0; ii < connLength; ++ii ) {
			if ( connPlugs[ ii ].node().apiType() == MFn::kDagPose ) {
				MObject			aMember = connPlugs[ ii ].attribute();
				MFnAttribute	fnAttr( aMember );

				if ( fnAttr.name() == "worldMatrix" ) {
					jointIndex = connPlugs[ ii ].logicalIndex();

					MFnDependencyNode nDagPose( connPlugs[ ii ].node() );

					// construct plugs for this joint's world matrix
					MObject aWorldMatrix = nDagPose.attribute( "worldMatrix" );
					MPlug	pWorldMatrix( connPlugs[ ii ].node(), aWorldMatrix );

					pWorldMatrix.selectAncestorLogicalIndex( jointIndex, aWorldMatrix );

					// get the world matrix data
					MObject worldMatrix;
					MStatus status = pWorldMatrix.getValue( worldMatrix );
					if ( MS::kSuccess != status ) {
						// Problem retrieving world matrix
						return;
					}

					MFnMatrixData	dMatrix( worldMatrix );
					MMatrix			wMatrix = dMatrix.matrix( &status );

					joint->bindmat = ConvertToIdSpace( idMat( wMatrix ) );
					joint->bindpos = ConvertToIdSpace( idVec( wMatrix ) ) * scale;
					if ( !options.ignoreScale ) {
						joint->bindpos *= joint->scale;
					} else {
						joint->bindmat[ 0 ].Normalize();
						joint->bindmat[ 1 ].Normalize();
						joint->bindmat[ 2 ].Normalize();
					}

					return;
				}
			}
		}
	}
}

/*
===============
idMayaExport::GetLocalTransform
===============
*/
void idMayaExport::GetLocalTransform( idExportJoint *joint, idVec3 &pos, idMat3 &mat ) {
	MStatus		status;
	MDagPath	dagPath;

	pos.Zero();
	mat.Identity();

	if ( !joint->dagnode ) {
		return;
	}

	status = joint->dagnode->getPath( dagPath );
	if ( !status ) {
		return;
	}

	MObject	transformNode = dagPath.transform( &status );
	if ( !status && ( status.statusCode () == MStatus::kInvalidParameter ) ) {
		return;
	}

	MFnDagNode transform( transformNode, &status );
	if ( !status ) {
		return;
	}

	pos = idVec( transform.transformationMatrix() );
	mat = idMat( transform.transformationMatrix() );
}

/*
===============
idMayaExport::GetWorldTransform
===============
*/
void idMayaExport::GetWorldTransform( idExportJoint *joint, idVec3 &pos, idMat3 &mat, float scale ) {
	idExportJoint *parent;

	GetLocalTransform( joint, pos, mat );
	mat.OrthoNormalizeSelf();
	pos *= scale;

	parent = joint->mayaNode.GetParent();
	if ( parent ) {
		idVec3 parentpos;
		idMat3 parentmat;

		GetWorldTransform( parent, parentpos, parentmat, scale );

		pos = parentpos + ( parentmat * ( pos * parent->scale ) );
		mat = mat * parentmat;
	}
}

/*
===============
idMayaExport::CreateJoints
===============
*/
void idMayaExport::CreateJoints( float scale ) {
	int				i;
	int				j;
	idExportJoint	*joint;
	idExportJoint	*parent;
	MStatus			status;
	MDagPath		dagPath;
	MFnDagNode		*parentNode;

	SetFrame( 0 );

	// create an initial list with all of the transformable nodes in the scene
	MItDag dagIterator( MItDag::kDepthFirst, MFn::kTransform, &status );
	for ( ; !dagIterator.isDone(); dagIterator.next() ) {
		status = dagIterator.getPath( dagPath );
		if ( !status ) {
			MayaError( "CreateJoints: MItDag::getPath failed (%s)", status.errorString().asChar() );
			continue;
		}

		joint = &model.joints.Alloc();
		joint->index	= model.joints.Num() - 1;
		joint->dagnode	= new MFnDagNode( dagPath, &status );
		if ( !status ) {
			MayaError( "CreateJoints: MFnDagNode constructor failed (%s)", status.errorString().asChar() );
			continue;
		}

		joint->name		= joint->dagnode->name().asChar();
		joint->realname = joint->name;
	}

	// allocate an extra joint in case we need to add an origin later
	model.exportOrigin = &model.joints.Alloc();
	model.exportOrigin->index	= model.joints.Num() - 1;

	// create scene hierarchy
	joint = model.joints.Ptr();
	for( i = 0; i < model.joints.Num(); i++, joint++ ) {
		if ( !joint->dagnode ) {
			continue;
		}
		joint->mayaNode.ParentTo( model.mayaHead );
		joint->exportNode.ParentTo( model.exportHead );

		parentNode = GetParent( joint->dagnode );
		if ( parentNode ) {
			// find the parent joint in our jointlist
			for( j = 0; j < model.joints.Num(); j++ ) {
				if ( !model.joints[ j ].dagnode ) {
					continue;
				}
				if ( model.joints[ j ].dagnode->name() == parentNode->name() ) {
					joint->mayaNode.ParentTo( model.joints[ j ].mayaNode );
					joint->exportNode.ParentTo( model.joints[ j ].exportNode );
					break;
				}
			}

			delete parentNode;
		}

		// create long name
		parent = joint->mayaNode.GetParent();
		if ( parent ) {
			joint->longname = parent->longname + "/" + joint->name;
		} else {
			joint->longname = joint->name;
		}

		// get the joint's scale
		GetLocalTransform( &model.joints[ i ], joint->t, joint->wm );
		joint->scale = joint->wm[ 0 ].Length();

		if ( parent ) {
			joint->scale *= parent->scale;
			if ( joint->scale != 0 ) {
				joint->invscale = 1.0f / joint->scale;
			} else {
				joint->invscale = 0;
			}
		}

		joint->dagnode->getPath( dagPath );
		GetBindPose( dagPath.node( &status ), joint, scale );
	}
}

/*
===============
idMayaExport::RenameJoints
===============
*/
void idMayaExport::RenameJoints( idList<idNamePair> &renamejoints, idStr &prefix ) {
	int				i;
	idExportJoint	*joint;

	// rename joints that match the prefix
	if ( prefix.Length() ) {
		joint = model.joints.Ptr();
		for( i = 0; i < model.joints.Num(); i++, joint++ ) {
			if ( !joint->name.Cmpn( prefix, prefix.Length() ) ) {
				// remove the prefix from the name
				joint->name = joint->name.Right( joint->name.Length() - prefix.Length() );
			}
		}
	}

	// rename joints if necessary
	for( i = 0; i < renamejoints.Num(); i++ ) {
		joint = model.FindJoint( renamejoints[ i ].from );
		if ( joint ) {
			joint->name = renamejoints[ i ].to;
		}
	}
}

/*
===============
idMayaExport::RemapParents
===============
*/
bool idMayaExport::RemapParents( idList<idNamePair> &remapjoints ) {
	int				i;
	idExportJoint	*joint;
	idExportJoint	*parent;
	idExportJoint	*origin;
	idExportJoint	*sibling;

	for( i = 0; i < remapjoints.Num(); i++ ) {
		// find joint to reparent
		joint = model.FindJoint( remapjoints[ i ].from );
		if ( !joint ) {
			// couldn't find joint, fail
			MayaError( "Couldn't find joint '%s' to reparent\n", remapjoints[ i ].from.c_str() );
		}

		// find new parent joint
		parent = model.FindJoint( remapjoints[ i ].to );
		if ( !parent ) {
			// couldn't find parent, fail
			MayaError( "Couldn't find joint '%s' to be new parent for '%s'\n", remapjoints[ i ].to.c_str(), remapjoints[ i ].from.c_str() );
		}

		if ( parent->exportNode.ParentedBy( joint->exportNode ) ) {
			MayaError( "Joint '%s' is a child of joint '%s' and can't become the parent.", joint->name.c_str(), parent->name.c_str() );
		}
		joint->exportNode.ParentTo( parent->exportNode );
	}

	// if we have an origin, make it the first node in the export list, otherwise add one
	origin = model.FindJoint( "origin" );
	if ( !origin ) {
		origin = model.exportOrigin;
		origin->dagnode	= NULL;
		origin->name	= "origin";
		origin->realname = "origin";
		origin->bindmat.Identity();
		origin->bindpos.Zero();
	}

	origin->exportNode.ParentTo( model.exportHead );

	// force the joint to be kept
	origin->keep = true;

	// make all root joints children of the origin joint
	joint = model.exportHead.GetChild();
	while( joint ) {
		sibling = joint->exportNode.GetSibling();
		if ( joint != origin ) {
			joint->exportNode.ParentTo( origin->exportNode );
		}
		joint = sibling;
	}

	return true;
}

/*
===============
idMayaExport::FindShader

Find the shading node for the given shading group set node.
===============
*/
MObject idMayaExport::FindShader( MObject& setNode ) {
	MStatus				status;
	MFnDependencyNode	fnNode( setNode );
	MPlug				shaderPlug;

	shaderPlug = fnNode.findPlug( "surfaceShader" );
	if ( !shaderPlug.isNull() ) {
		MPlugArray connectedPlugs;
		bool asSrc = false;
		bool asDst = true;
		shaderPlug.connectedTo( connectedPlugs, asDst, asSrc, &status );

		if ( connectedPlugs.length() != 1 ) {
			MayaError( "FindShader: Error getting shader (%s)", status.errorString().asChar() );
		} else {
			return connectedPlugs[ 0 ].node();
		}
	}

	return MObject::kNullObj;
}

/*
===============
idMayaExport::GetTextureForMesh

Find the texture files that apply to the color of each polygon of
a selected shape if the shape has its polygons organized into sets.
===============
*/
void idMayaExport::GetTextureForMesh( idExportMesh *mesh, MFnDagNode &dagNode ) {
	MStatus		status;
	MDagPath	path;
	int			i;
	int			instanceNum;

	status = dagNode.getPath( path );
	if ( !status ) {
		return;
	}

	path.extendToShape();

	// If the shape is instanced then we need to determine which
	// instance this path refers to.
	//
	instanceNum = 0;
	if ( path.isInstanced() ) {
		instanceNum = path.instanceNumber();
	}

	// Get a list of all sets pertaining to the selected shape and the
	// members of those sets.
	//
	MFnMesh fnMesh( path );
	MObjectArray sets;
	MObjectArray comps;
	status = fnMesh.getConnectedSetsAndMembers( instanceNum, sets, comps, true );
	if ( !status ) {
		MayaError( "GetTextureForMesh: MFnMesh::getConnectedSetsAndMembers failed (%s)", status.errorString().asChar() );
	}

	// Loop through all the sets.  If the set is a polygonal set, find the
	// shader attached to the and print out the texture file name for the
	// set along with the polygons in the set.
	//
	for ( i = 0; i < ( int )sets.length(); i++ ) {
		MObject set = sets[i];
		MObject comp = comps[i];

		MFnSet fnSet( set, &status );
		if ( status == MS::kFailure ) {
			MayaError( "GetTextureForMesh: MFnSet constructor failed (%s)", status.errorString().asChar() );
			continue;
		}

		// Make sure the set is a polygonal set.  If not, continue.
		MItMeshPolygon piter(path, comp, &status);
		if (status == MS::kFailure) {
			continue;
		}

		// Find the texture that is applied to this set.  First, get the
		// shading node connected to the set.  Then, if there is an input
		// attribute called "color", search upstream from it for a texture
		// file node.
		//
		MObject shaderNode = FindShader( set );
		if ( shaderNode == MObject::kNullObj ) {
			continue;
		}

		MPlug colorPlug = MFnDependencyNode(shaderNode).findPlug("color", &status);
		if ( status == MS::kFailure ) {
			continue;
		}

		MItDependencyGraph dgIt(colorPlug, MFn::kFileTexture,
						   MItDependencyGraph::kUpstream,
						   MItDependencyGraph::kBreadthFirst,
						   MItDependencyGraph::kNodeLevel,
						   &status);

		if ( status == MS::kFailure ) {
			continue;
		}

		dgIt.disablePruningOnFilter();

		// If no texture file node was found, just continue.
		//
		if ( dgIt.isDone() ) {
			continue;
		}

		// Print out the texture node name and texture file that it references.
		//
		MObject textureNode = dgIt.thisNode();
		MPlug filenamePlug = MFnDependencyNode( textureNode ).findPlug( "fileTextureName" );
		MString textureName;
		filenamePlug.getValue( textureName );

		// remove the OS path and save it in the mesh
		OSPathToRelativePath( textureName.asChar(), mesh->shader, options.game );
		mesh->shader.StripFileExtension();

		return;
	}
}

/*
===============
idMayaExport::CopyMesh
===============
*/
idExportMesh *idMayaExport::CopyMesh( MFnSkinCluster &skinCluster, float scale ) {
	MStatus			status;
	MObjectArray	objarray;
	MObjectArray	outputarray;
	int				nGeom;
	int				i, j, k;
	idExportMesh	*mesh;
	float			uv_u, uv_v;
	idStr			name, altname;
	int				pos;

	status = skinCluster.getInputGeometry( objarray );
	if ( !status ) {
		MayaError( "CopyMesh: Error getting input geometry (%s)", status.errorString().asChar() );
		return NULL;
	}

	nGeom = objarray.length();
	for( i = 0; i < nGeom; i++ ) {
		MFnDagNode dagNode( objarray[ i ], &status );
		if ( !status ) {
			common->Printf( "CopyMesh: MFnDagNode Constructor failed (%s)", status.errorString().asChar() );
			continue;
		}

		MFnMesh fnmesh( objarray[ i ], &status );
		if ( !status ) {
			// object isn't an MFnMesh
			continue;
		}

		status = skinCluster.getOutputGeometry( outputarray );
		if ( !status ) {
			common->Printf( "CopyMesh: Error getting output geometry (%s)", status.errorString().asChar() );
			return NULL;
		}

		if ( outputarray.length() < 1 ) {
			return NULL;
		}

		name = fnmesh.name().asChar();
		if ( options.prefix.Length() ) {
			if ( !name.Cmpn( options.prefix, options.prefix.Length() ) ) {
				// remove the prefix from the name
				name = name.Right( name.Length() - options.prefix.Length() );
			} else {
				// name doesn't match prefix, so don't use this mesh
				//return NULL;
			}
		}

		pos = name.Find( "ShapeOrig" );
		if ( pos >= 0 ) {
			name.CapLength( pos );
		}

		MFnDagNode dagNode2( outputarray[ 0 ], &status );
		if ( !status ) {
			common->Printf( "CopyMesh: MFnDagNode Constructor failed (%s)", status.errorString().asChar() );
			continue;
		}

		altname = name;
		MObject	parent = fnmesh.parent( 0, &status );
		if ( status ) {
			MFnDagNode parentNode( parent, &status );
			if ( status ) {
				altname = parentNode.name().asChar();
			}
		}

		name.StripLeadingOnce( options.prefix );
		altname.StripLeadingOnce( options.prefix );
		if ( options.keepmeshes.Num() ) {
			if ( !options.keepmeshes.Find( name ) && !options.keepmeshes.Find( altname ) ) {
				if ( altname != name ) {
					common->Printf( "Skipping mesh '%s' ('%s')\n", name.c_str(), altname.c_str() );
				} else {
					common->Printf( "Skipping mesh '%s'\n", name.c_str() );
				}
				return NULL;
			}
		}

		if ( options.skipmeshes.Find( name ) || options.skipmeshes.Find( altname ) ) {
			common->Printf( "Skipping mesh '%s' ('%s')\n", name.c_str(), altname.c_str() );
			return NULL;
		}

		mesh = new idExportMesh();
		model.meshes.Append( mesh );

		if ( altname.Length() ) {
			mesh->name = altname;
		} else {
			mesh->name = name;
		}
		GetTextureForMesh( mesh, dagNode2 );

		int v = fnmesh.numVertices( &status );
		mesh->verts.SetNum( v );

		MFloatPointArray vertexArray;

		fnmesh.getPoints( vertexArray, MSpace::kPreTransform );

		for( j = 0; j < v; j++ ) {
			memset( &mesh->verts[ j ], 0, sizeof( mesh->verts[ j ] ) );
			mesh->verts[ j ].pos = ConvertToIdSpace( idVec( vertexArray[ j ] ) ) * scale;
		}

		MIntArray vertexList;
		int p;

		p = fnmesh.numPolygons( &status );
		mesh->tris.SetNum( p );
		mesh->uv.SetNum( p );

		MString setName;

		status = fnmesh.getCurrentUVSetName( setName );
		if ( !status ) {
			MayaError( "CopyMesh: MFnMesh::getCurrentUVSetName failed (%s)", status.errorString().asChar() );
		}

		for( j = 0; j < p; j++ ) {
			fnmesh.getPolygonVertices( j, vertexList );
			if ( vertexList.length() != 3 ) {
				MayaError( "CopyMesh: Too many vertices on a face (%d)\n", vertexList.length() );
			}

			for( k = 0; k < 3; k++ ) {
				mesh->tris[ j ].indexes[ k ] = vertexList[ k ];

				status = fnmesh.getPolygonUV( j, k, uv_u, uv_v, &setName );
				if ( !status ) {
					MayaError( "CopyMesh: MFnMesh::getPolygonUV failed (%s)", status.errorString().asChar() );
				}

				mesh->uv[ j ].uv[ k ][ 0 ] = uv_u;
				mesh->uv[ j ].uv[ k ][ 1 ] = uv_v;
			}
		}

		return mesh;
	}

	return NULL;
}

/*
===============
idMayaExport::CreateMesh
===============
*/
void idMayaExport::CreateMesh( float scale ) {
	size_t			count;
	idExportMesh	*mesh;
	MStatus			status;
	exportWeight_t	weight;
	unsigned int	nGeoms;

	// Iterate through graph and search for skinCluster nodes
	MItDependencyNodes iter( MFn::kSkinClusterFilter );
	count = 0;
	for ( ; !iter.isDone(); iter.next() ) {
		MObject object = iter.item();

		count++;

		// For each skinCluster node, get the list of influence objects
		MFnSkinCluster skinCluster( object, &status );
		if ( !status ) {
			MayaError( "%s: Error getting skin cluster (%s)", object.apiTypeStr(), status.errorString().asChar() );
		}

		mesh = CopyMesh( skinCluster, scale );
		if ( !mesh ) {
			continue;
		}

		MDagPathArray infs;
		unsigned int nInfs = skinCluster.influenceObjects(infs, &status);
		if ( !status ) {
			MayaError( "Mesh '%s': Error getting influence objects (%s)", mesh->name.c_str(), status.errorString().asChar() );
		}

		if ( 0 == nInfs ) {
			MayaError( "Mesh '%s': No influence objects found", mesh->name.c_str() );
		}

		// loop through the geometries affected by this cluster
		nGeoms = skinCluster.numOutputConnections();
		for (size_t ii = 0; ii < nGeoms; ++ii) {
			unsigned int index = skinCluster.indexForOutputConnection(ii,&status);

			if ( !status ) {
				MayaError( "Mesh '%s': Error getting geometry index (%s)", mesh->name.c_str(), status.errorString().asChar() );
			}

			// get the dag path of the ii'th geometry
			MDagPath skinPath;
			status = skinCluster.getPathAtIndex(index,skinPath);
			if ( !status ) {
				MayaError( "Mesh '%s': Error getting geometry path (%s)", mesh->name.c_str(), status.errorString().asChar() );
			}

			// iterate through the components of this geometry
			MItGeometry gIter( skinPath );

			// print out the influence objects
			idList<idExportJoint *> joints;
			idExportJoint			*joint;
			exportVertex_t			*vert;

			joints.Resize( nInfs );
			for (size_t kk = 0; kk < nInfs; ++kk) {
				const char *c;
				MString s;

				s = infs[kk].partialPathName();
				c = s.asChar();
				joint = model.FindJointReal( c );
				if ( !joint ) {
					MayaError( "Mesh '%s': joint %s not found", mesh->name.c_str(), c );
				}

				joints.Append( joint );
			}

			for ( /* nothing */ ; !gIter.isDone(); gIter.next() ) {
				MObject comp = gIter.component( &status );
				if ( !status ) {
					MayaError( "Mesh '%s': Error getting component (%s)", mesh->name.c_str(), status.errorString().asChar() );
				}

				// Get the weights for this vertex (one per influence object)
				MFloatArray wts;
				unsigned infCount;
				status = skinCluster.getWeights(skinPath,comp,wts,infCount);
				if ( !status ) {
					MayaError( "Mesh '%s': Error getting weights (%s)", mesh->name.c_str(), status.errorString().asChar() );
				}
				if (0 == infCount) {
					MayaError( "Mesh '%s': Error: 0 influence objects.", mesh->name.c_str() );
				}

				int num = gIter.index();
				vert = &mesh->verts[ num ];
				vert->startweight = mesh->weights.Num();

				float totalweight = 0.0f;

				// copy the weight data for this vertex
				int numNonZeroWeights = 0;
				int jj;
				for ( jj = 0; jj < (int)infCount ; ++jj ) {
					float w = ( float )wts[ jj ];
					if ( w > 0.0f ) {
						numNonZeroWeights++;
					}
					if ( w > options.jointThreshold ) {
						weight.joint = joints[ jj ];
						weight.jointWeight = wts[ jj ];

						if ( !options.ignoreScale ) {
							weight.joint->bindmat.ProjectVector( vert->pos - ( weight.joint->bindpos * weight.joint->invscale ), weight.offset );
							weight.offset *= weight.joint->scale;
						} else {
							weight.joint->bindmat.ProjectVector( vert->pos - weight.joint->bindpos, weight.offset );
						}
						mesh->weights.Append( weight );
						totalweight += weight.jointWeight;
					}
				}

				vert->numWeights = mesh->weights.Num() - vert->startweight;
				if ( !vert->numWeights ) {
					if ( numNonZeroWeights ) {
						MayaError( "Error on mesh '%s': Vertex %d doesn't have any joint weights exceeding jointThreshold (%f).", mesh->name.c_str(), num, options.jointThreshold );
					} else {
						MayaError( "Error on mesh '%s': Vertex %d doesn't have any joint weights.", mesh->name.c_str(), num );
					}
				} else if ( !totalweight ) {
					MayaError( "Error on mesh '%s': Combined weight of 0 on vertex %d.", mesh->name.c_str(), num );
				}
				//if ( numNonZeroWeights ) {
				//	common->Printf( "Mesh '%s': skipped %d out of %d weights on vertex %d\n", mesh->name.c_str(), numNonZeroWeights, numNonZeroWeights + vert->numWeights, num );
				//}

				// normalize the joint weights
				for( jj = 0; jj < vert->numWeights; jj++ ) {
					mesh->weights[ vert->startweight + jj ].jointWeight /= totalweight;
				}
			}
			break;
		}
	}

	if ( !count && !options.ignoreMeshes ) {
		MayaError( "CreateMesh: No skinClusters found in this scene.\n" );
	}
}

/*
===============
idMayaExport::CombineMeshes

combine surfaces with the same shader.
===============
*/
void idMayaExport::CombineMeshes( void ) {
	int						i, j;
	int						count;
	idExportMesh			*mesh;
	idExportMesh			*combine;
	idList<idExportMesh *>	oldmeshes;

	oldmeshes = model.meshes;
	model.meshes.Clear();

	count = 0;
	for( i = 0; i < oldmeshes.Num(); i++ ) {
		mesh = oldmeshes[ i ];
		if ( !mesh->keep ) {
			delete mesh;
			continue;
		}

		combine = NULL;
		for( j = 0; j < model.meshes.Num(); j++ ) {
			if ( model.meshes[ j ]->shader == mesh->shader ) {
				combine = model.meshes[ j ];
				break;
			}
		}

		if ( combine ) {
			combine->Merge( mesh );
			delete mesh;
			count++;
		} else {
			model.meshes.Append( mesh );
		}
	}

	// share verts
	for( i = 0; i < model.meshes.Num(); i++ ) {
		model.meshes[ i ]->ShareVerts();
	}

	common->Printf( "Merged %d meshes\n", count );
}

/*
===============
idMayaExport::GetAlignment
===============
*/
void idMayaExport::GetAlignment( idStr &alignName, idMat3 &align, float rotate, int startframe ) {
	idVec3			pos;
	idExportJoint	*joint;
	idAngles		ang( 0, rotate, 0 );
	idMat3			mat;

	align.Identity();

	if ( alignName.Length() ) {
		SetFrame( 0 );

		joint = model.FindJoint( alignName );
		if ( !joint ) {
			MayaError( "could not find joint '%s' to align model to.\n", alignName.c_str() );
		}

		// found it
		GetWorldTransform( joint, pos, mat, 1.0f );
		align[ 0 ][ 0 ] = mat[ 2 ][ 0 ];
		align[ 0 ][ 1 ] = -mat[ 2 ][ 2 ];
		align[ 0 ][ 2 ] = mat[ 2 ][ 1 ];

		align[ 1 ][ 0 ] = mat[ 0 ][ 0 ];
		align[ 1 ][ 1 ] = -mat[ 0 ][ 2 ];
		align[ 1 ][ 2 ] = mat[ 0 ][ 1 ];

		align[ 2 ][ 0 ] = mat[ 1 ][ 0 ];
		align[ 2 ][ 1 ] = -mat[ 1 ][ 2 ];
		align[ 2 ][ 2 ] = mat[ 1 ][ 1 ];

		if ( rotate ) {
			align *= ang.ToMat3();
		}
	} else if ( rotate ) {
		align = ang.ToMat3();
	}

	align.TransposeSelf();
}

/*
===============
idMayaExport::GetObjectType

return the type of the object
===============
*/
const char *idMayaExport::GetObjectType( MObject object ) {
	if( object.isNull() ) {
		return "(Null)";
	}

	MStatus				stat;
	MFnDependencyNode	dgNode;
	MString				typeName;

	stat		= dgNode.setObject( object );
	typeName	= dgNode.typeName( &stat );
	if( MS::kSuccess != stat ) {
		// can not get the type name of this object
		return "(Unknown)";
	}

	return typeName.asChar();
}

/*
===============
idMayaExport::GetCameraFov
===============
*/
float idMayaExport::GetCameraFov( idExportJoint *joint ) {
	int		childCount;
	int		j;
	double	horiz;
	double	focal;
	MStatus	status;
	const char *n1, *n2;
	MFnDagNode *dagnode;
	float	fov;

	dagnode = joint->dagnode;

	MObject cameraNode = dagnode->object();
	childCount = dagnode->childCount();

	fov = 90.0f;
	for( j = 0; j < childCount; j++ ) {
		MObject childNode = dagnode->child( j );

		n1 = GetObjectType( cameraNode );
		n2 = GetObjectType( childNode );
		if ( ( !strcmp( "transform", n1 ) ) && ( !strcmp( "camera", n2 ) ) ) {
			MFnCamera camera( childNode );
			focal = camera.focalLength();
			horiz = camera.horizontalFilmAperture();
			fov = RAD2DEG( 2 * atan( ( horiz * 0.5 ) / ( focal / 25.4 ) ) );
			break;
		}
	}

	return fov;
}

/*
===============
idMayaExport::GetCameraFrame
===============
*/
void idMayaExport::GetCameraFrame( idExportJoint *camera, idMat3 &align, cameraFrame_t *cam ) {
	idMat3 mat;
	idMat3 axis;
	idVec3 pos;

	// get the worldspace positions of the joint
	GetWorldTransform( camera, pos, axis, 1.0f );

	// convert to id coordinates
	cam->t	= ConvertToIdSpace( pos ) * align;

	// correct the orientation for the game
	axis	= ConvertToIdSpace( axis ) * align;
	mat[ 0 ] = -axis[ 2 ];
	mat[ 1 ] = -axis[ 0 ];
	mat[ 2 ] = axis[ 1 ];
	cam->q = mat.ToQuat().ToCQuat();

	// get it's fov
	cam->fov = GetCameraFov( camera );
}

/*
===============
idMayaExport::CreateCameraAnim
===============
*/
void idMayaExport::CreateCameraAnim( idMat3 &align ) {
	float				start, end;
	MDagPath			dagPath;
	int					frameNum;
	short				v;
	MStatus				status;
	cameraFrame_t		cam;
	idExportJoint		*refCam;
	idExportJoint		*camJoint;
	idStr				currentCam;
	idStr				newCam;
	MPlug				plug;
	MFnEnumAttribute	cameraAttribute;

	start = TimeForFrame( options.startframe );
	end   = TimeForFrame( options.endframe );

#if 0
	options.framerate = 60;
	model.numFrames = ( int )( ( end - start ) * ( float )options.framerate ) + 1;
	model.frameRate = options.framerate;
#else
	model.numFrames = options.endframe + 1 - options.startframe;
	model.frameRate = options.framerate;
#endif

	common->Printf( "start frame = %d\n  end frame = %d\n", options.startframe, options.endframe );
	common->Printf( " start time = %f\n   end time = %f\n total time = %f\n", start, end, end - start );

	if ( start > end ) {
		MayaError( "Start frame is greater than end frame." );
	}

	refCam = model.FindJoint( "refcam" );
	if ( refCam == NULL ) {
		currentCam = MAYA_DEFAULT_CAMERA;
	} else {
		MObject cameraNode = refCam->dagnode->object();
		MFnDependencyNode cameraDG( cameraNode, &status );
		if( MS::kSuccess != status ) {
			MayaError( "Can't find 'refcam' dependency node." );
			return;
		}

		MObject attr = cameraDG.attribute( MString( "Camera" ), &status );
		if( MS::kSuccess != status ) {
			MayaError( "Can't find 'Camera' attribute on 'refcam'." );
			return;
		}

		plug = MPlug( cameraNode, attr );
		status = cameraAttribute.setObject( attr );
		if( MS::kSuccess != status ) {
			MayaError( "Bad 'Camera' attribute on 'refcam'." );
			return;
		}

		model.camera.Clear();
		model.cameraCuts.Clear();

		SetFrame( 0 );
		status = plug.getValue( v );
		currentCam = cameraAttribute.fieldName( v, &status ).asChar();
		if( MS::kSuccess != status ) {
			MayaError( "Error getting camera name on frame %d", GetMayaFrameNum( 0 ) );
		}
	}

	camJoint = model.FindJoint( currentCam );
	if ( !camJoint ) {
		MayaError( "Couldn't find camera '%s'", currentCam.c_str() );
	}

	for( frameNum = 0; frameNum < model.numFrames; frameNum++ ) {
		common->Printf( "\rFrame %d/%d...", options.startframe + frameNum, options.endframe );

#if 0
		MTime	time;
		time.setUnit( MTime::kSeconds );
		time.setValue( start + ( ( float )frameNum / ( float )options.framerate ) );
		MGlobal::viewFrame( time );
#else
		SetFrame( frameNum );
#endif

		// get the position for this frame
		GetCameraFrame( camJoint, align, &model.camera.Alloc() );

		if ( refCam != NULL ) {
			status = plug.getValue( v );
			newCam = cameraAttribute.fieldName( v, &status ).asChar();
			if( MS::kSuccess != status ) {
				MayaError( "Error getting camera name on frame %d", GetMayaFrameNum( frameNum ) );
			}

			if ( newCam != currentCam ) {
				// place a cut at our last frame
				model.cameraCuts.Append( model.camera.Num() - 1 );

				currentCam = newCam;
				camJoint = model.FindJoint( currentCam );
				if ( !camJoint ) {
					MayaError( "Couldn't find camera '%s'", currentCam.c_str() );
				}

				// get the position for this frame
				GetCameraFrame( camJoint, align, &model.camera.Alloc() );
			}
		}
	}

	common->Printf( "\n" );
}

/*
===============
idMayaExport::GetDefaultPose
===============
*/
void idMayaExport::GetDefaultPose( idMat3 &align ) {
	float			start;
	MDagPath		dagPath;
	idMat3			jointaxis;
	idVec3			jointpos;
	idExportJoint	*joint, *parent;
	idBounds		bnds;
	idBounds		meshBounds;
	idList<jointFrame_t> frame;

	start = TimeForFrame( options.startframe );

	common->Printf( "default pose frame = %d\n", options.startframe );
	common->Printf( " default pose time = %f\n", start );

	frame.SetNum( model.joints.Num() );
	SetFrame( 0 );

	// convert joints into local coordinates and save in channels
	for( joint = model.exportHead.GetNext(); joint != NULL; joint = joint->exportNode.GetNext() ) {
		if ( !joint->dagnode ) {
			// custom origin joint
			joint->idwm.Identity();
			joint->idt.Zero();
			frame[ joint->index ].t.Zero();
			frame[ joint->index ].q.Set( 0.0f, 0.0f, 0.0f );
			continue;
		}

		// get the worldspace positions of the joint
		GetWorldTransform( joint, jointpos, jointaxis, options.scale );

		// convert to id coordinates
		jointaxis	= ConvertToIdSpace( jointaxis ) * align;
		jointpos	= ConvertToIdSpace( jointpos ) * align;

		// save worldspace position of joint for children
		joint->idwm	= jointaxis;
		joint->idt	= jointpos;

		parent = joint->exportNode.GetParent();
		if ( parent ) {
			// convert to local coordinates
			jointpos = ( jointpos - parent->idt ) * parent->idwm.Transpose();
			jointaxis = jointaxis * parent->idwm.Transpose();
		} else if ( joint->name == "origin" ) {
			if ( options.clearOrigin ) {
				jointpos.Zero();
			}
			if ( options.clearOriginAxis ) {
				jointaxis.Identity();
			}
		}

		frame[ joint->index ].t = jointpos;
		frame[ joint->index ].q = jointaxis.ToQuat().ToCQuat();
	}

	// relocate origin to start at 0, 0, 0 for first frame
	joint = model.FindJoint( "origin" );
	if ( joint ) {
		frame[ joint->index ].t.Zero();
	}

	// transform the hierarchy
	for( joint = model.exportHead.GetNext(); joint != NULL; joint = joint->exportNode.GetNext() ) {
		jointpos	= frame[ joint->index ].t;
		jointaxis	= frame[ joint->index ].q.ToQuat().ToMat3();

		parent = joint->exportNode.GetParent();
		if ( parent ) {
			joint->idwm = jointaxis * parent->idwm;
			joint->idt = parent->idt + jointpos * parent->idwm;
		} else {
			joint->idwm = jointaxis;
			joint->idt = jointpos;
		}

		joint->bindmat = joint->idwm;
		joint->bindpos = joint->idt;
	}

	common->Printf( "\n" );
}

/*
===============
idMayaExport::CreateAnimation
===============
*/
void idMayaExport::CreateAnimation( idMat3 &align ) {
	int				i;
	float			start, end;
	MDagPath		dagPath;
	idMat3			jointaxis;
	idVec3			jointpos;
	int				frameNum;
	idExportJoint	*joint, *parent;
	idBounds		bnds;
	idBounds		meshBounds;
	jointFrame_t	*frame;
	int				cycleStart;
	idVec3			totalDelta;
	idList<jointFrame_t>	copyFrames;

	start = TimeForFrame( options.startframe );
	end   = TimeForFrame( options.endframe );

	model.numFrames = options.endframe + 1 - options.startframe;
	model.frameRate = options.framerate;

	common->Printf( "start frame = %d\n  end frame = %d\n", options.startframe, options.endframe );
	common->Printf( " start time = %f\n   end time = %f\n total time = %f\n", start, end, end - start );

	if ( start > end ) {
		MayaError( "Start frame is greater than end frame." );
	}

	model.bounds.SetNum( model.numFrames );
	model.jointFrames.SetNum( model.numFrames * model.joints.Num() );
	model.frames.SetNum( model.numFrames );
	for( i = 0; i < model.numFrames; i++ ) {
		model.frames[ i ] = &model.jointFrames[ model.joints.Num() * i ];
	}

	// *sigh*.  cyclestart doesn't work nicely with the anims.
	// may just want to not do it in SetTime anymore.
	cycleStart = options.cycleStart;
	options.cycleStart = options.startframe;

	for( frameNum = 0; frameNum < model.numFrames; frameNum++ ) {
		common->Printf( "\rFrame %d/%d...", options.startframe + frameNum, options.endframe );

		frame = model.frames[ frameNum ];
		SetFrame( frameNum );

		// convert joints into local coordinates and save in channels
		for( joint = model.exportHead.GetNext(); joint != NULL; joint = joint->exportNode.GetNext() ) {
			if ( !joint->dagnode ) {
				// custom origin joint
				joint->idwm.Identity();
				joint->idt.Zero();
				frame[ joint->index ].t.Zero();
				frame[ joint->index ].q.Set( 0.0f, 0.0f, 0.0f );
				continue;
			}

			// get the worldspace positions of the joint
			GetWorldTransform( joint, jointpos, jointaxis, options.scale );

			// convert to id coordinates
			jointaxis	= ConvertToIdSpace( jointaxis ) * align;
			jointpos	= ConvertToIdSpace( jointpos ) * align;

			// save worldspace position of joint for children
			joint->idwm	= jointaxis;
			joint->idt	= jointpos;

			parent = joint->exportNode.GetParent();
			if ( parent ) {
				// convert to local coordinates
				jointpos = ( jointpos - parent->idt ) * parent->idwm.Transpose();
				jointaxis = jointaxis * parent->idwm.Transpose();
			} else if ( joint->name == "origin" ) {
				if ( options.clearOrigin ) {
					jointpos.Zero();
				}
				if ( options.clearOriginAxis ) {
					jointaxis.Identity();
				}
			}

			frame[ joint->index ].t = jointpos;
			frame[ joint->index ].q = jointaxis.ToQuat().ToCQuat();
		}
	}

	options.cycleStart = cycleStart;
	totalDelta.Zero();

	joint = model.FindJoint( "origin" );
	if ( joint ) {
		frame = model.frames[ 0 ];
		idVec3 origin = frame[ joint->index ].t;

		frame = model.frames[ model.numFrames - 1 ];
		totalDelta = frame[ joint->index ].t - origin;
	}

	// shift the frames when cycleStart is used
	if ( options.cycleStart > options.startframe ) {
		copyFrames = model.jointFrames;
		for( i = 0; i < model.numFrames; i++ ) {
			bool shiftorigin = false;
			frameNum = i + ( options.cycleStart - options.startframe );
			if ( frameNum >= model.numFrames ) {
				// wrap around, skipping the first frame, since it's a dupe of the last frame
				frameNum -= model.numFrames - 1;
				shiftorigin = true;
			}

			memcpy( &model.jointFrames[ model.joints.Num() * i ], &copyFrames[ model.joints.Num() * frameNum ], model.joints.Num() * sizeof( copyFrames[ 0 ] ) );

			if ( joint && shiftorigin ) {
				model.frames[ i ][ joint->index ].t += totalDelta;
			}
		}
	}

	if ( joint ) {
		// relocate origin to start at 0, 0, 0 for first frame
		frame = model.frames[ 0 ];
		idVec3 origin = frame[ joint->index ].t;
		for( i = 0; i < model.numFrames; i++ ) {
			frame = model.frames[ i ];
			frame[ joint->index ].t -= origin;
		}
	}

	// get the bounds for each frame
	for( frameNum = 0; frameNum < model.numFrames; frameNum++ ) {
		frame = model.frames[ frameNum ];

		// transform the hierarchy
		for( joint = model.exportHead.GetNext(); joint != NULL; joint = joint->exportNode.GetNext() ) {
			jointpos	= frame[ joint->index ].t;
			jointaxis	= frame[ joint->index ].q.ToQuat().ToMat3();

			parent = joint->exportNode.GetParent();
			if ( parent ) {
				joint->idwm = jointaxis * parent->idwm;
				joint->idt = parent->idt + jointpos * parent->idwm;
			} else {
				joint->idwm = jointaxis;
				joint->idt = jointpos;
			}
		}

		// get bounds for this frame
		bnds.Clear();
		for( i = 0; i < model.meshes.Num(); i++ ) {
			if ( model.meshes[ i ]->keep ) {
				model.meshes[ i ]->GetBounds( meshBounds );
				bnds.AddBounds( meshBounds );
			}
		}
		model.bounds[ frameNum ][ 0 ] = bnds[ 0 ];
		model.bounds[ frameNum ][ 1 ] = bnds[ 1 ];
	}

	common->Printf( "\n" );
}

/*
===============
idMayaExport::ConvertModel
===============
*/
void idMayaExport::ConvertModel( void ) {
	MStatus	status;
	idMat3	align;

	common->Printf( "Converting %s to %s...\n", options.src.c_str(), options.dest.c_str() );

	// see if the destination file exists
	FILE *file = fopen( options.dest, "r" );
	if ( file ) {
		fclose( file );

		// make sure we can write to the destination
		FILE *file = fopen( options.dest, "r+" );
		if ( !file ) {
			MayaError( "Unable to write to the file '%s'", options.dest.c_str() );
		}
		fclose( file );
	}

	MString	filename( options.src );
	MFileIO::newFile( true );

	// Load the file into Maya
	common->Printf( "Loading file...\n" );
	status = MFileIO::open( filename, NULL, true );
	if ( !status ) {
		MayaError( "Error loading '%s': '%s'\n", filename.asChar(), status.errorString().asChar() );
	}

	// force Maya to update the frame.  When using references, sometimes
	// the model is posed the way it is in the source.  Since Maya only
	// updates it when the frame time changes to a value other than the
	// current, just setting the time to the min time doesn't guarantee
	// that the model gets updated.
	MGlobal::viewFrame( MAnimControl::maxTime() );
	MGlobal::viewFrame( MAnimControl::minTime() );

	if ( options.startframe < 0 ) {
		options.startframe = MAnimControl::minTime().as( MTime::kFilm );
	}

	if ( options.endframe < 0 ) {
		options.endframe = MAnimControl::maxTime().as( MTime::kFilm );
	}
	if ( options.cycleStart < 0 ) {
		options.cycleStart = options.startframe;
	} else if ( ( options.cycleStart < options.startframe ) || ( options.cycleStart > options.endframe ) ) {
		MayaError( "cycleStart (%d) out of frame range (%d to %d)\n", options.cycleStart, options.startframe, options.endframe );
	} else if ( options.cycleStart == options.endframe ) {
		// end frame is a duplicate of the first frame in cycles, so just disable cycleStart
		options.cycleStart = options.startframe;
	}

	// create a list of the transform nodes that will make up our heirarchy
	common->Printf( "Creating joints...\n" );
	CreateJoints( options.scale );
	if ( options.type != WRITE_CAMERA ) {
		common->Printf( "Creating meshes...\n" );
		CreateMesh( options.scale );
		common->Printf( "Renaming joints...\n" );
		RenameJoints( options.renamejoints, options.prefix );
		common->Printf( "Remapping parents...\n" );
		RemapParents( options.remapjoints );
		common->Printf( "Pruning joints...\n" );
		PruneJoints( options.keepjoints, options.prefix );
		common->Printf( "Combining meshes...\n" );
		CombineMeshes();
	}

	common->Printf( "Align model...\n" );
	GetAlignment( options.align, align, options.rotate, 0 );

	switch( options.type ) {
	case WRITE_MESH :
		common->Printf( "Grabbing default pose:\n" );
		GetDefaultPose( align );
		common->Printf( "Writing file...\n" );
		if ( !model.WriteMesh( options.dest, options ) ) {
			MayaError( "error writing to '%s'", options.dest.c_str() );
		}
		break;

	case WRITE_ANIM :
		common->Printf( "Creating animation frames:\n" );
		CreateAnimation( align );
		common->Printf( "Writing file...\n" );
		if ( !model.WriteAnim( options.dest, options ) ) {
			MayaError( "error writing to '%s'", options.dest.c_str() );
		}
		break;

	case WRITE_CAMERA :
		common->Printf( "Creating camera frames:\n" );
		CreateCameraAnim( align );

		common->Printf( "Writing file...\n" );
		if ( !model.WriteCamera( options.dest, options ) ) {
			MayaError( "error writing to '%s'", options.dest.c_str() );
		}
		break;
	}

	common->Printf( "done\n\n" );
}

/*
===============
idMayaExport::ConvertToMD3
===============
*/
void idMayaExport::ConvertToMD3( void ) {
#if 0
	int					i, j;
	md3Header_t			*pinmodel;
	md3Frame_t			*frame;
	md3Surface_t		*surf;
	md3Shader_t			*shader;
	md3Triangle_t		*tri;
	md3St_t				*st;
	md3XyzNormal_t		*xyz;
	md3Tag_t			*tag;
	int					version;
	int					size;

	//model_t *mod, int lod, void *buffer, const char *mod_name

	pinmodel = (md3Header_t *)buffer;

	version = LittleInt (pinmodel->version);
	if (version != MD3_VERSION) {
		common->Printf( "R_LoadMD3: %s has wrong version (%i should be %i)\n",
				 mod_name, version, MD3_VERSION);
		return qfalse;
	}

	mod->type = MOD_MESH;
	size = LittleInt(pinmodel->ofsEnd);
	mod->dataSize += size;
	mod->md3[lod] = ri.Hunk_Alloc( size );

	memcpy (mod->md3[lod], buffer, LittleInt(pinmodel->ofsEnd) );

	LL(mod->md3[lod]->ident);
	LL(mod->md3[lod]->version);
	LL(mod->md3[lod]->numFrames);
	LL(mod->md3[lod]->numTags);
	LL(mod->md3[lod]->numSurfaces);
	LL(mod->md3[lod]->ofsFrames);
	LL(mod->md3[lod]->ofsTags);
	LL(mod->md3[lod]->ofsSurfaces);
	LL(mod->md3[lod]->ofsEnd);

	if ( mod->md3[lod]->numFrames < 1 ) {
		common->Printf( "R_LoadMD3: %s has no frames\n", mod_name );
		return qfalse;
	}

	// swap all the frames
	frame = (md3Frame_t *) ( (byte *)mod->md3[lod] + mod->md3[lod]->ofsFrames );
	for ( i = 0 ; i < mod->md3[lod]->numFrames ; i++, frame++) {
		frame->radius = LittleFloat( frame->radius );
		for ( j = 0 ; j < 3 ; j++ ) {
			frame->bounds[0][j] = LittleFloat( frame->bounds[0][j] );
			frame->bounds[1][j] = LittleFloat( frame->bounds[1][j] );
			frame->localOrigin[j] = LittleFloat( frame->localOrigin[j] );
		}
	}

	// swap all the tags
	tag = (md3Tag_t *) ( (byte *)mod->md3[lod] + mod->md3[lod]->ofsTags );
	for ( i = 0 ; i < mod->md3[lod]->numTags * mod->md3[lod]->numFrames ; i++, tag++) {
		for ( j = 0 ; j < 3 ; j++ ) {
			tag->origin[j] = LittleFloat( tag->origin[j] );
			tag->axis[0][j] = LittleFloat( tag->axis[0][j] );
			tag->axis[1][j] = LittleFloat( tag->axis[1][j] );
			tag->axis[2][j] = LittleFloat( tag->axis[2][j] );
		}
	}

	// swap all the surfaces
	surf = (md3Surface_t *) ( (byte *)mod->md3[lod] + mod->md3[lod]->ofsSurfaces );
	for ( i = 0 ; i < mod->md3[lod]->numSurfaces ; i++) {

		LL(surf->ident);
		LL(surf->flags);
		LL(surf->numFrames);
		LL(surf->numShaders);
		LL(surf->numTriangles);
		LL(surf->ofsTriangles);
		LL(surf->numVerts);
		LL(surf->ofsShaders);
		LL(surf->ofsSt);
		LL(surf->ofsXyzNormals);
		LL(surf->ofsEnd);

		if ( surf->numVerts > SHADER_MAX_VERTEXES ) {
			ri.Error (ERR_DROP, "R_LoadMD3: %s has more than %i verts on a surface (%i)",
				mod_name, SHADER_MAX_VERTEXES, surf->numVerts );
		}
		if ( surf->numTriangles*3 > SHADER_MAX_INDEXES ) {
			ri.Error (ERR_DROP, "R_LoadMD3: %s has more than %i triangles on a surface (%i)",
				mod_name, SHADER_MAX_INDEXES / 3, surf->numTriangles );
		}

		// change to surface identifier
		surf->ident = SF_MD3;

		// lowercase the surface name so skin compares are faster
		Q_strlwr( surf->name );

		// strip off a trailing _1 or _2
		// this is a crutch for q3data being a mess
		j = strlen( surf->name );
		if ( j > 2 && surf->name[j-2] == '_' ) {
			surf->name[j-2] = 0;
		}

		// register the shaders
		shader = (md3Shader_t *) ( (byte *)surf + surf->ofsShaders );
		for ( j = 0 ; j < surf->numShaders ; j++, shader++ ) {
			shader_t	*sh;

			sh = R_FindShader( shader->name, LIGHTMAP_NONE, qtrue );
			if ( sh->defaultShader ) {
				shader->shaderIndex = 0;
			} else {
				shader->shaderIndex = sh->index;
			}
		}

		// swap all the triangles
		tri = (md3Triangle_t *) ( (byte *)surf + surf->ofsTriangles );
		for ( j = 0 ; j < surf->numTriangles ; j++, tri++ ) {
			LL(tri->indexes[0]);
			LL(tri->indexes[1]);
			LL(tri->indexes[2]);
		}

		// swap all the ST
		st = (md3St_t *) ( (byte *)surf + surf->ofsSt );
		for ( j = 0 ; j < surf->numVerts ; j++, st++ ) {
			st->st[0] = LittleFloat( st->st[0] );
			st->st[1] = LittleFloat( st->st[1] );
		}

		// swap all the XyzNormals
		xyz = (md3XyzNormal_t *) ( (byte *)surf + surf->ofsXyzNormals );
		for ( j = 0 ; j < surf->numVerts * surf->numFrames ; j++, xyz++ )
		{
			xyz->xyz[0] = LittleShort( xyz->xyz[0] );
			xyz->xyz[1] = LittleShort( xyz->xyz[1] );
			xyz->xyz[2] = LittleShort( xyz->xyz[2] );

			xyz->normal = LittleShort( xyz->normal );
		}


		// find the next surface
		surf = (md3Surface_t *)( (byte *)surf + surf->ofsEnd );
	}
	return true;
#endif
}

/*
==============================================================================

dll setup

==============================================================================
*/

/*
===============
Maya_Shutdown
===============
*/
void Maya_Shutdown( void ) {
	if ( initialized ) {
		errorMessage.Clear();
		initialized = false;

		// This shuts down the entire app somehow, so just ignore it.
		//MLibrary::cleanup();
	}
}

/*
===============
Maya_ConvertModel
===============
*/
const char *Maya_ConvertModel( const char *ospath, const char *commandline ) {

	errorMessage = "Ok";

	try {
		idExportOptions options( commandline, ospath );
		idMayaExport	exportM( options );

		exportM.ConvertModel();
	}

	catch( idException &exception ) {
		errorMessage = exception.error;
	}

	return errorMessage;
}

/*
===============
dllEntry
===============
*/
bool dllEntry( int version, idCommon *common, idSys *sys ) {

	if ( !common || !sys ) {
		return false;
	}

	::common = common;
	::sys = sys;
	::cvarSystem = NULL;

	idLib::sys			= sys;
	idLib::common		= common;
	idLib::cvarSystem	= NULL;
	idLib::fileSystem	= NULL;

	idLib::Init();

	if ( version != MD5_VERSION ) {
		common->Printf( "Error initializing Maya exporter: DLL version %d different from .exe version %d\n", MD5_VERSION, version );
		return false;
	}

	if ( !initialized ) {
		MStatus	status;

		status = MLibrary::initialize( GAME_NAME, true );
		if ( !status ) {
			common->Printf( "Error calling MLibrary::initialize (%s)\n", status.errorString().asChar() );
			return false;
		}

		initialized = true;
	}

	return true;
}

// Force type checking on the interface functions to help ensure that they match the ones in the .exe
const exporterDLLEntry_t	ValidateEntry = &dllEntry;
const exporterInterface_t	ValidateConvert = &Maya_ConvertModel;
const exporterShutdown_t	ValidateShutdown = &Maya_Shutdown;
