/*==LICENSE==*

CyanWorlds.com Engine - MMOG client, server and tools
Copyright (C) 2011  Cyan Worlds, Inc.

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.

You can contact Cyan Worlds, Inc. by email legal@cyan.com
 or by snail mail at:
      Cyan Worlds, Inc.
      14617 N Newport Hwy
      Mead, WA   99021

*==LICENSE==*/
//////////////////////////////////////////////////////////////////////////////
//																			//
//	plPassMtlBase - Base class for all Plasma MAX materials					//
//																			//
//////////////////////////////////////////////////////////////////////////////

#include "hsTypes.h"
#include "hsBitVector.h"

#include "Max.h"
#include "iparamb2.h"
#include "notify.h"
#include "notetrck.h"

#include "plPassMtlBase.h"
#include "plPassBaseParamIDs.h"
#include "plNoteTrackWatcher.h"
#include "plAnimStealthNode.h"

#include "../MaxComponent/plMaxAnimUtils.h"
#include "../MaxMain/plPlasmaRefMsgs.h"

// For converting from a MAX Mtl
#include "plPassMtl.h"
#include "plBumpMtl.h"
#include "plDecalMtl.h"

using namespace plPassBaseParamIDs;

IMtlParams *plPassMtlBase::fIMtlParams = nil;

//// plPostLoadHandler ///////////////////////////////////////////////////////
//	Small class to keep track of all the materials to update after load

class plPostLoadHandler
{
	static bool fLoading;
	static hsTArray<plPassMtlBase *> fPostLoads;

	public:
		static bool IsLoading() { return fLoading; }

		static void PostLoadFixupFunction( void *param, NotifyInfo *info )
		{
			fLoading = false;

			for( int i = 0; i < fPostLoads.GetCount(); i++ )
				fPostLoads[ i ]->PostLoadAnimPBFixup();

			fPostLoads.Reset();
			UnRegisterNotification( PostLoadFixupFunction, param, NOTIFY_FILE_POST_OPEN );
			UnRegisterNotification( PostLoadFixupFunction, param, NOTIFY_FILE_POST_MERGE );
		}

		static void	AddPostLoad( plPassMtlBase *mtl )
		{
			fLoading = true;

			if( fPostLoads.GetCount() == 0 )
			{
				RegisterNotification( PostLoadFixupFunction, mtl, NOTIFY_FILE_POST_OPEN );
				RegisterNotification( PostLoadFixupFunction, mtl, NOTIFY_FILE_POST_MERGE );
			}

			mtl->SetLoadingFlag( true );
			fPostLoads.Append( mtl );
		}

		static void	RemovePostLoad( plPassMtlBase *mtl )
		{
			for( int i = 0; i < fPostLoads.GetCount(); i++ )
			{
				if( fPostLoads[ i ] == mtl )
				{
					fPostLoads.Remove( i );
					break;
				}
			}
		}
};
hsTArray<plPassMtlBase *>	plPostLoadHandler::fPostLoads;
bool plPostLoadHandler::fLoading = false;


plPassMtlBase::plPassMtlBase( BOOL loading ) : fNTWatcher( nil ), fBasicPB(NULL), fAdvPB(NULL), fLayersPB(NULL), fAnimPB(NULL),
								 fLoading( loading )
{
	fNTWatcher = TRACKED_NEW plNoteTrackWatcher( this );
	Reset();
}

plPassMtlBase::~plPassMtlBase()
{
	if( fLoading )
		plPostLoadHandler::RemovePostLoad( this );

	// Force the watcher's parent pointer to nil, otherwise the de-ref will attempt to re-delete us
	fNTWatcher->SetReference( plNoteTrackWatcher::kRefParentMtl, nil );
	delete fNTWatcher;
	fNTWatcher = nil;

	// Manually delete our notetrack refs, otherwise there'll be hell to pay
	for( int i = 0; i < fNotetracks.GetCount(); i++ )
	{
		if( fNotetracks[ i ] != nil )
			DeleteReference( kRefNotetracks + i );
	}
}

void	plPassMtlBase::Reset( void ) 
{
	fIValid.SetEmpty();
}

//// Stealth Accessors ///////////////////////////////////////////////////////

int		plPassMtlBase::GetNumStealths( void )
{
	return IGetNumStealths( true );
}

plAnimStealthNode	*plPassMtlBase::GetStealth( int index )
{
	return IGetStealth( index, false );
}

int	plPassMtlBase::IGetNumStealths( hsBool update )
{
	if( update )
		IUpdateAnimNodes();

	return fAnimPB->Count( (ParamID)kPBAnimStealthNodes );
}

plAnimStealthNode	*plPassMtlBase::IGetStealth( int index, hsBool update )
{
	if( update )
		IUpdateAnimNodes();

	return (plAnimStealthNode *)fAnimPB->GetReferenceTarget( (ParamID)kPBAnimStealthNodes, 0, index );
}

plAnimStealthNode	*plPassMtlBase::IFindStealth( const char *segmentName )
{
	int		i;


	for( i = 0; i < fAnimPB->Count( (ParamID)kPBAnimStealthNodes ); i++ )
	{
		plAnimStealthNode *node = (plAnimStealthNode *)fAnimPB->GetReferenceTarget( (ParamID)kPBAnimStealthNodes, 0, i );
		const char *name = node->GetSegmentName();

		if( node != nil && strcmp( name, segmentName ) == 0 )
		{
			return node;
		}
	}

	return nil;
}

//// IVerifyStealthPresent ///////////////////////////////////////////////////
//	Ensures that we have a stealth for the given segment.

plAnimStealthNode	*plPassMtlBase::IVerifyStealthPresent( const char *animName )
{
	// If we're in the middle of loading, don't check
	if (plPostLoadHandler::IsLoading())
		return nil;

	plAnimStealthNode *stealth = IFindStealth( animName );
	if( stealth == nil )
	{
		// New segment, add a new stealth node
		stealth = (plAnimStealthNode *)GetCOREInterface()->CreateInstance( HELPER_CLASS_ID, ANIMSTEALTH_CLASSID );
		INode *node = GetCOREInterface()->CreateObjectNode( stealth );
		stealth->SetSegment( ( strcmp(animName, ENTIRE_ANIMATION_NAME) != 0 ) ? animName : nil );
		stealth->SetNodeName( GetName() );
		node->Freeze( true );

		// Skip the attach, since we might not find a valid INode. This will leave the node attached to the scene
		// root, which is fine. Since we just care about it being SOMEWHERE in the scene hierarchy
		/*
		if( fAnimPB->Count( (ParamID)kPBAnimStealthNodes ) > 0 )
		{
			plAnimStealthNode *first = (plAnimStealthNode *)fAnimPB->GetReferenceTarget( (ParamID)kPBAnimStealthNodes, 0, 0 );
			first->GetINode()->AttachChild( node );
		}
		*/

		fAnimPB->Append( (ParamID)kPBAnimStealthNodes, 1, (ReferenceTarget **)&stealth );

		const char *realName = stealth->GetSegmentName();

		fStealthsChanged = true;
	}
	else
	{
		// Exists already, we're ok
		stealth->SetParentMtl( this );
	}
	return stealth;
}

//// Change Callbacks ////////////////////////////////////////////////////////

void	plPassMtlBase::RegisterChangeCallback( plMtlChangeCallback *callback )
{
	if( fChangeCallbacks.Find( callback ) == fChangeCallbacks.kMissingIndex )
		fChangeCallbacks.Append( callback );
}

void	plPassMtlBase::UnregisterChangeCallback( plMtlChangeCallback *callback )
{
	int idx = fChangeCallbacks.Find( callback );
	if( idx != fChangeCallbacks.kMissingIndex )
		fChangeCallbacks.Remove( idx );
}

//// IUpdateAnimNodes ////////////////////////////////////////////////////////
//	Updates the list of stealth nodes in the anim paramBlock to match our
//	list of anim segments.

void	plPassMtlBase::IUpdateAnimNodes( void )
{
	// Our beautiful hack, to make sure we don't update until we actually are loaded
	if( fLoading )
		return;

	SegmentMap *segMap = GetAnimSegmentMap( this, nil );

	hsTArray<plAnimStealthNode *>	goodNodes;
	

	// Keep track of whether we change anything
	fStealthsChanged = false;

	// Verify one for "entire animation"
	plAnimStealthNode *stealth = IVerifyStealthPresent( ENTIRE_ANIMATION_NAME );
	goodNodes.Append( stealth );

	// Verify segment nodes
	if( segMap != nil )
	{
		for( SegmentMap::iterator i = segMap->begin(); i != segMap->end(); i++ )
		{
			SegmentSpec *spec = (*i).second;

			if( spec->fType == SegmentSpec::kAnim )
			{
				plAnimStealthNode *stealth = IVerifyStealthPresent( spec->fName );
				goodNodes.Append( stealth );
			}
		}

		DeleteSegmentMap( segMap );
	}

	// Remove nodes that no longer have segments
	int	idx;
	for( idx = 0; idx < fAnimPB->Count( (ParamID)kPBAnimStealthNodes ); )
	{
		plAnimStealthNode *node = (plAnimStealthNode *)fAnimPB->GetReferenceTarget( (ParamID)kPBAnimStealthNodes, 0, idx );

		if( node != nil && goodNodes.Find( node ) == goodNodes.kMissingIndex )
		{
			fAnimPB->Delete( (ParamID)kPBAnimStealthNodes, idx, 1 );
//			GetCOREInterface()->DeleteNode( node->GetINode() );
			fStealthsChanged = true;
		}
		else
			idx++;
	}

	if( fStealthsChanged )
	{
		// Yup, our list of stealths got updated. Notify everyone of such.
		for( idx = 0; idx < fChangeCallbacks.GetCount(); idx++ )
			fChangeCallbacks[ idx ]->SegmentListChanged();
	}
}

//// NameChanged /////////////////////////////////////////////////////////////
//	Notify from NTWatcher so we can update the names of our stealth nodes

void	plPassMtlBase::NameChanged( void )
{
	for( int idx = 0; idx < fAnimPB->Count( (ParamID)kPBAnimStealthNodes ); idx++ )
	{
		plAnimStealthNode *node = (plAnimStealthNode *)fAnimPB->GetReferenceTarget( (ParamID)kPBAnimStealthNodes, 0, idx );
		if( node != nil )
			node->SetNodeName( GetName() );
	}
}

//// NoteTrackAdded/Removed //////////////////////////////////////////////////
//	Notifies from NTWatcher so we can update our list of stealths

void	plPassMtlBase::NoteTrackAdded( void )
{
	int		i;


	// Make a ref to our new notetrack
	for( i = 0; i < NumNoteTracks(); i++ )
	{
		NoteTrack *track = GetNoteTrack( i );

		if( fNotetracks.Find( track ) == fNotetracks.kMissingIndex )
		{
			MakeRefByID( FOREVER, kRefNotetracks + fNotetracks.GetCount(), track );
			break;
		}
	}

	for( i = 0; i < fChangeCallbacks.GetCount(); i++ )
		fChangeCallbacks[ i ]->NoteTrackListChanged();
	IUpdateAnimNodes();
}

void	plPassMtlBase::NoteTrackRemoved( void )
{
	int			i;
	hsBitVector	stillThere;


	// Make a ref to our new notetrack
	for( i = 0; i < NumNoteTracks(); i++ )
	{
		NoteTrack *track = GetNoteTrack( i );

		int idx = fNotetracks.Find( track );
		if( idx != fNotetracks.kMissingIndex )
			stillThere.Set( idx );
	}

	for( i = 0; i < fNotetracks.GetCount(); i++ )
	{
		if( !stillThere.IsBitSet( i ) && fNotetracks[ i ] != nil )
		{
//			DeleteReference( kRefNotetracks + i );
			SetReference( kRefNotetracks + i, nil );
		}
	}

	for( i = 0; i < fChangeCallbacks.GetCount(); i++ )
		fChangeCallbacks[ i ]->NoteTrackListChanged();
	IUpdateAnimNodes();
}


//////////////////////////////////////////////////////////////////////////////
//// MAX Ref Stuff ///////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////

//// NumRefs /////////////////////////////////////////////////////////////////

int	plPassMtlBase::NumRefs()
{
	return 4 + fNotetracks.GetCount();
}

//// GetReference ////////////////////////////////////////////////////////////

RefTargetHandle plPassMtlBase::GetReference( int i )
{
	if( i >= kRefNotetracks && i < kRefNotetracks + fNotetracks.GetCount() )
		return fNotetracks[ i - kRefNotetracks ];

	return NULL;
}

//// SetReference ////////////////////////////////////////////////////////////

void plPassMtlBase::SetReference(int i, RefTargetHandle rtarg)
{
	if( i >= kRefNotetracks )
	{
		fNotetracks.ExpandAndZero(i - kRefNotetracks + 1);
		fNotetracks[i - kRefNotetracks] = (NoteTrack*)rtarg;
	}
}

//// NotifyRefChanged ////////////////////////////////////////////////////////

RefResult plPassMtlBase::NotifyRefChanged( Interval changeInt, RefTargetHandle hTarget, 
										   PartID &partID, RefMessage message ) 
{
	switch( message )
	{
		case REFMSG_CHANGE:
			fIValid.SetEmpty();

			// see if this message came from a changing parameter in the pblock,
			// if so, limit rollout update to the changing item
			if (hTarget == fBasicPB || hTarget == fAdvPB || hTarget == fLayersPB || hTarget == fAnimPB)
			{
				IParamBlock2 *pb = (IParamBlock2*)hTarget;

				ParamID changingParam = pb->LastNotifyParamID();
				pb->GetDesc()->InvalidateUI( changingParam );
				
				// And let the SceneWatcher know that the material on some of it's
				// referenced objects changed.
				NotifyDependents( FOREVER, PART_ALL, REFMSG_USER_MAT );
			}
			else 
			{
				// Was it a notetrack ref?
				if( fNotetracks.Find( (NoteTrack *)hTarget ) != fNotetracks.kMissingIndex )
				{
					// Yup, so update our notetrack list
					IUpdateAnimNodes();
				}
			}

			break;

		case REFMSG_TARGET_DELETED:
			NoteTrackRemoved();
			break;

	}

	return REF_SUCCEED;
}

//////////////////////////////////////////////////////////////////////////////
//// Standard IO (or non-standard, as the case may be) ///////////////////////
//////////////////////////////////////////////////////////////////////////////

//// PostLoadAnimPBFixup /////////////////////////////////////////////////////
//	Takes the old version of the anim paramblock and translates it into the
//	new version.
//	Note that there's an interesting (?) side effect of this: for new materials,
//	we'll incorrectly detect them as the "old" format and fix-up them as well.
//	This means that we'll end up with the same defaults for (Entire Animation)
//	that we had for the old materials. We can easily change the defaults by
//	changing the defaults for the old paramblock though.
//	Also, we go ahead and re-work the stealth parent pointers here, since it
//	appears to be the ONLY time we can do it and have it reliably work. ARRGH!
//
//	History of the options that we CAN'T do and why (and hence why this):
//		- ParamBlock accessor doesn't work. For some reason, the accessor isn't
//		  always called on load, even with P_CALLSETS_ON_LOAD specified
//		- Doing it on Load() doesn't work, because neither the ParamBlocks are
//		  guaranteed to be fully loaded (with their tabs filled) nor are the
//		  notetracks necessarily attached yet
//		- Notetracks can also possibly be attached BEFORE load, which doesn't
//		  do us a damned bit of good, so we need to make sure we're fully
//		  loaded before we run this function. Unfortunately, the only time
//		  we're guaranteed THAT is by a FILE_POST_OPEN notify. (post-load
//		  callbacks don't work because they're called right after our object
//		  is loaded but not necessarily before the notetracks are attached)

void	plPassMtlBase::PostLoadAnimPBFixup( void )
{
	SetLoadingFlag( false );

#ifdef MCN_UPGRADE_OLD_ANIM_BLOCKS
	if( fAnimPB->Count( (ParamID)kPBAnimStealthNodes ) == 0 )
	{
		// Yup, old style. So our update process looks like this:
		// 1) Create stealths for all our segments as we are now
		// 2) Set the parameters on all of them to our old defaults (no autostart,
		//    loop on entire, no ease).
		// 3) Copy the old paramblock values to the single stealth indicated by
		//    the old PB

		// Step 1...
		IUpdateAnimNodes();

		// Step 2...
		for( int i = 0; i < fAnimPB->Count( (ParamID)kPBAnimStealthNodes ); i++ )
		{
			plAnimStealthNode *node = (plAnimStealthNode *)fAnimPB->GetReferenceTarget( (ParamID)kPBAnimStealthNodes, 0, i );
		const char *name = node->GetSegmentName();
			node->SetAutoStart( false );
			node->SetLoop( true, ENTIRE_ANIMATION_NAME );
			node->SetEaseIn( plAnimEaseTypes::kNoEase, 1.f, 1.f, 1.f );
			node->SetEaseOut( plAnimEaseTypes::kNoEase, 1.f, 1.f, 1.f );
		}

		// Step 3...
		const char *oldSel = (const char *)fAnimPB->GetStr( (ParamID)kPBAnimName );
		if( oldSel == nil )
			oldSel = ENTIRE_ANIMATION_NAME;
		plAnimStealthNode *myNew = IFindStealth( oldSel );
		if( myNew != nil )
		{
#pragma warning( push )	// Forcing value to bool true or false (go figure, i'm even explicitly casting)
#pragma warning( disable:4800 )	// Forcing value to bool true or false (go figure, i'm even explicitly casting)
			myNew->SetAutoStart( (bool)fAnimPB->GetInt( (ParamID)kPBAnimAutoStart ) );
			myNew->SetLoop( (bool)fAnimPB->GetInt( (ParamID)kPBAnimLoop ),
							(char *)fAnimPB->GetStr( (ParamID)kPBAnimLoopName ) );
			myNew->SetEaseIn( (UInt8)fAnimPB->GetInt( (ParamID)kPBAnimEaseInType ),
								(hsScalar)fAnimPB->GetFloat( (ParamID)kPBAnimEaseInLength ),
								(hsScalar)fAnimPB->GetFloat( (ParamID)kPBAnimEaseInMin ),
								(hsScalar)fAnimPB->GetFloat( (ParamID)kPBAnimEaseInMax ) );
			myNew->SetEaseOut( (UInt8)fAnimPB->GetInt( (ParamID)kPBAnimEaseOutType ),
								(hsScalar)fAnimPB->GetFloat( (ParamID)kPBAnimEaseOutLength ),
								(hsScalar)fAnimPB->GetFloat( (ParamID)kPBAnimEaseOutMin ),
								(hsScalar)fAnimPB->GetFloat( (ParamID)kPBAnimEaseOutMax ) );
#pragma warning( pop )
		}				
	}
#endif // MCN_UPGRADE_OLD_ANIM_BLOCKS


	// Make sure the parent is set tho. Note: we have to do this because, for some *(#$&(* reason,
	// when we're loading a file, MAX can somehow add the stealths to our tab list WITHOUT calling
	// the accessor for it (and the tab is empty on the CallSetsOnLoad() pass, for some reason).
	for( int i = 0; i < fAnimPB->Count( (ParamID)kPBAnimStealthNodes ); i++ )
	{
		plAnimStealthNode *node = (plAnimStealthNode *)fAnimPB->GetReferenceTarget( (ParamID)kPBAnimStealthNodes, 0, i );
		node->SetParentMtl( this );
	}
}


#define MTL_HDR_CHUNK 0x4000

//// Load ////////////////////////////////////////////////////////////////////
//	Our actual MAX load function

IOResult plPassMtlBase::Load(ILoad *iload)
{
	plPostLoadHandler::AddPostLoad( this );

	IOResult res;
	int id;
	while (IO_OK==(res=iload->OpenChunk()))
	{
		switch(id = iload->CurChunkID())
		{
			case MTL_HDR_CHUNK:
				res = MtlBase::Load(iload);
				break;
		}
		iload->CloseChunk();
		if (res!=IO_OK) 
			return res;
	}

	return IO_OK;
}

//// Save ////////////////////////////////////////////////////////////////////
//	The MAX flip-side

IOResult plPassMtlBase::Save(ISave *isave)
{ 
	IOResult res;
	isave->BeginChunk(MTL_HDR_CHUNK);
	res = MtlBase::Save(isave);
	if (res!=IO_OK) return res;
	isave->EndChunk();

	return IO_OK;
}	

//// ICloneBase //////////////////////////////////////////////////////////////

void	plPassMtlBase::ICloneBase( plPassMtlBase *target, RemapDir &remap )
{
	*((MtlBase*)target) = *((MtlBase*)this); 
	ICloneRefs( target, remap );

	for( int idx = 0; idx < fAnimPB->Count( (ParamID)kPBAnimStealthNodes ); idx++ )
	{
		IParamBlock2 *pb = target->fAnimPB;
		plAnimStealthNode *stealth = (plAnimStealthNode *)fAnimPB->GetReferenceTarget( (ParamID)kPBAnimStealthNodes, 0, idx );
		pb->SetValue( (ParamID)kPBAnimStealthNodes, 0, remap.CloneRef( stealth ), idx );

		stealth = (plAnimStealthNode *)pb->GetReferenceTarget( (ParamID)kPBAnimStealthNodes, 0, idx );
		INode *node = GetCOREInterface()->CreateObjectNode( stealth );
		stealth->SetNodeName( GetName() );
		node->Freeze( true );
	}

	BaseClone(this, target, remap);
	target->fIValid.SetEmpty();	
}

//// ConvertToPassMtl ////////////////////////////////////////////////////////
//	Static convert to our plPassMtlBase type, if possible

plPassMtlBase	*plPassMtlBase::ConvertToPassMtl( Mtl *mtl )
{
	if( mtl == nil )
		return nil;

	if( mtl->ClassID() == PASS_MTL_CLASS_ID 
		|| mtl->ClassID() == BUMP_MTL_CLASS_ID
		|| mtl->ClassID() == DECAL_MTL_CLASS_ID )
	{
		return (plPassMtlBase *)mtl;
	}

	return nil;
}

//// SetupProperties /////////////////////////////////////////////////////////

hsBool	plPassMtlBase::SetupProperties( plMaxNode *node, plErrorMsg *pErrMsg )
{
	hsBool ret = true;

	// Call SetupProperties on all our animStealths if we have any
	int i, count = IGetNumStealths();
	for( i = 0; i < count; i++ )
	{
		if( !IGetStealth( i, false )->SetupProperties( node, pErrMsg ) )
			ret = false;
	}

	return ret;
}

//// ConvertDeInit ///////////////////////////////////////////////////////////

hsBool	plPassMtlBase::ConvertDeInit( plMaxNode *node, plErrorMsg *pErrMsg )
{
	hsBool ret = true;

	// Call ConvertDeInit on all our animStealths if we have any
	int i, count = IGetNumStealths();
	for( i = 0; i < count; i++ )
	{
		if( !IGetStealth( i, false )->ConvertDeInit( node, pErrMsg ) )
			ret = false;
	}

	return ret;
}