/*
**	Command & Conquer Renegade(tm)
**	Copyright 2025 Electronic Arts 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/>.
*/

#include "blender2.h"
#include <istdplug.h>
#include "utilapi.h"
#include "appdata.h"

//----------------------------------------------------------------------------
// Blender_Class
//----------------------------------------------------------------------------

class Blender_Class : public UtilityObj
{
public:

	Blender_Class();

	void BeginEditParams(Interface *ip,IUtil *iu);
	void EndEditParams(Interface *ip,IUtil *iu);
	void SelectionSetChanged ( Interface * ip, IUtil * ui );
	void DeleteThis() {}

	void Init(HWND hWnd);
	void Destroy(HWND hWnd);
	void Close () { iu->CloseUtility (); }

	void Get_Active_Time_Range ();

	void Blend_Keys ();

	void Loop_Controllers ();

	void OutputObject(INode *node,TCHAR *fname);

private:

	BOOL Is_Root ( INode * node );
	float Heading_Delta_From_Quat ( Quat q );
	void Set_Data_Chunk ( INode * node, const Blender_Data_Chunk & new_data );
	void Remove_Data_Chunk ( INode * node );

	int first_frame;
	int first_match;
	int last_frame;
	int last_match;

	ISpinnerControl * first_frame_spin;
	ISpinnerControl * first_match_spin;
	ISpinnerControl * last_frame_spin;
	ISpinnerControl * last_match_spin;

	IUtil * iu;
	Interface *ip;
	HWND hPanel;
};

//----------------------------------------------------------------------------
// the_blender
//----------------------------------------------------------------------------

static Blender_Class the_blender;

//----------------------------------------------------------------------------
// Blender_Desc_Class
//----------------------------------------------------------------------------

class Blender_Desc_Class:public ClassDesc
{
public:
	int 			IsPublic()     {return 1;}
	void *			Create(BOOL)   {return &the_blender;}
	const TCHAR *	ClassName()    {return _T("Key Blender");}
	SClass_ID		SuperClassID() {return UTILITY_CLASS_ID;}
	Class_ID		ClassID()      {return Blender_Class_ID;}
	const TCHAR* 	Category()     {return _T("Westwood Tools");}
};

//----------------------------------------------------------------------------
// blender_desc
//----------------------------------------------------------------------------

static Blender_Desc_Class blender_desc;

//----------------------------------------------------------------------------
// BlenderDesc
//----------------------------------------------------------------------------

ClassDesc* BlenderDesc() {return &blender_desc;}

//----------------------------------------------------------------------------
// BlenderDlgProc
//----------------------------------------------------------------------------

static BOOL CALLBACK BlenderDlgProc
(
	HWND hWnd,
	UINT msg,
	WPARAM wParam,
	LPARAM lParam
)
{
	switch (msg)
	{
	case WM_INITDIALOG:
		the_blender.Init(hWnd);			
		break;
	
	case WM_DESTROY:
		the_blender.Destroy(hWnd);
		break;

	case WM_COMMAND:
		switch (LOWORD(wParam))
		{
		case ID_CLOSE:
			the_blender.Close ();
			break;

		case ID_GET_ACTIVE_TIME_RANGE:
			the_blender.Get_Active_Time_Range ();
			break;

		case ID_APPLY:
			the_blender.Blend_Keys ();
			break;
		}
		break;

	default:
		return FALSE;
	}
	return TRUE; 
}

//----------------------------------------------------------------------------
// Blender_Class::Blender_Class
//----------------------------------------------------------------------------

Blender_Class::Blender_Class()
{
	iu = NULL;
	ip = NULL;	
	hPanel = NULL;

	first_frame = 0;
	first_match = 0;
	last_frame  = 0;
	last_match  = 0;
}

//----------------------------------------------------------------------------
// Blender_Class::BeginEditParams
//----------------------------------------------------------------------------

void Blender_Class::BeginEditParams(Interface *ip,IUtil *iu) 
{
	this->iu = iu;
	this->ip = ip;
	hPanel = ip->AddRollupPage
	(
		hInstance,
		MAKEINTRESOURCE(IDD_ASCIIOUT_PANEL),
		BlenderDlgProc,
		_T("Key Blender"),
		0
	);
}
	
//----------------------------------------------------------------------------
// Blender_Class::EndEditParams
//----------------------------------------------------------------------------

void Blender_Class::EndEditParams ( Interface *ip, IUtil *iu ) 
{
	this->iu = NULL;
	this->ip = NULL;
	ip->DeleteRollupPage(hPanel);
	hPanel = NULL;
}

//----------------------------------------------------------------------------
// Blender_Class::SelectionSetChanged
//----------------------------------------------------------------------------

void Blender_Class::SelectionSetChanged ( Interface * ip, IUtil * iu )
{
	if ( ip->GetSelNodeCount () == 0 )
	{
		EnableWindow ( GetDlgItem ( hPanel, ID_APPLY ), FALSE );
	}
	else
	{
		EnableWindow ( GetDlgItem ( hPanel, ID_APPLY ), TRUE );
	}
}

//----------------------------------------------------------------------------
// Blender_Class::Init
//----------------------------------------------------------------------------

void Blender_Class::Init ( HWND hWnd )
{
	if ( ip->GetSelNodeCount () == 0 )
	{
		EnableWindow ( GetDlgItem ( hWnd, ID_APPLY ), FALSE );
	}
	else
	{
		EnableWindow ( GetDlgItem ( hWnd, ID_APPLY ), TRUE );
	}

	int ticks_per_frame = GetTicksPerFrame ();

	int start_frame   = ip->GetAnimRange ().Start () / ticks_per_frame;
	int end_frame     = ip->GetAnimRange ().End () / ticks_per_frame;

	first_frame = start_frame;
	first_match = start_frame;
	last_frame  = end_frame;
	last_match  = start_frame;

	first_frame_spin = SetupIntSpinner
	(
		hWnd,
		IDC_FIRST_SPIN,
		IDC_FIRST_EDIT,
		-65536,
		65535,
		first_frame
	);

	first_frame_spin->SetResetValue ( first_frame );

	first_match_spin = SetupIntSpinner
	(
		hWnd,
		IDC_FIRST_MATCH_SPIN,
		IDC_FIRST_MATCH_EDIT,
		-65536,
		65535,
		first_match
	);

	first_match_spin->SetResetValue ( first_match );

	last_frame_spin = SetupIntSpinner
	(
		hWnd,
		IDC_LAST_SPIN,
		IDC_LAST_EDIT,
		-65536,
		65535,
		last_frame
	);

	last_frame_spin->SetResetValue ( last_frame );

	last_match_spin = SetupIntSpinner
	(
		hWnd,
		IDC_LAST_MATCH_SPIN,
		IDC_LAST_MATCH_EDIT,
		-65536,
		65535,
		last_match
	);

	last_match_spin->SetResetValue ( last_match );
}

//----------------------------------------------------------------------------
// Blender_Class::Destroy
//----------------------------------------------------------------------------

void Blender_Class::Destroy(HWND hWnd)
{
}

//----------------------------------------------------------------------------
// Blender_Class::Get_Active_Time_Range
//----------------------------------------------------------------------------

void Blender_Class::Get_Active_Time_Range ()
{
	int ticks_per_frame = GetTicksPerFrame ();

	int start_frame   = ip->GetAnimRange ().Start () / ticks_per_frame;
	int end_frame     = ip->GetAnimRange ().End () / ticks_per_frame;

	first_frame_spin->SetValue ( start_frame, FALSE );
	last_frame_spin->SetValue  ( end_frame,   FALSE );
	first_match_spin->SetValue ( start_frame, FALSE );
	last_match_spin->SetValue  ( start_frame, FALSE );
}

//----------------------------------------------------------------------------
// Blender_Class::Blend_Keys
//----------------------------------------------------------------------------

static const Quat zero (0.0,0.0,0.0,1.0);

void Blender_Class::Blend_Keys ()
{
	Interval interval; // Used for returned validity intervals (which are not used).

	int ticks_per_frame = GetTicksPerFrame ();

	TimeValue start_time  = first_frame_spin->GetIVal () * ticks_per_frame;
	TimeValue end_time    = last_frame_spin->GetIVal  () * ticks_per_frame;
	TimeValue start_match = first_match_spin->GetIVal () * ticks_per_frame;
	TimeValue end_match   = last_match_spin->GetIVal  () * ticks_per_frame;

	float t_scale = 1.0f / (float) (end_time - start_time);

	BOOL bad_controller_found = FALSE;

	theHold.Begin ();

	SetCursor ( LoadCursor (NULL, IDC_WAIT) );
	SuspendAnimate ();
	AnimateOn ();

	int number_of_nodes = ip->GetSelNodeCount ();

	for ( int i = 0; i < number_of_nodes; ++ i )
	{
		// Get the inode.

		INode * inode = ip->GetSelNode ( i );

		Control * tm_controller = inode->GetTMController ();
		if ( tm_controller == NULL )
			continue;

		Quat rot_start_delta = zero;
		Quat rot_end_delta   = zero;

		Blender_Data_Chunk data;
		data.position_delta = Point3 (0.0f, 0.0f, 0.0f);
		data.heading_delta  = 0.0f;

		Control * c;

		//--------------------------------------------------------------------
		// Get the rotation controller and change its keys.

		c = tm_controller->GetRotationController ();

		if ( c != NULL )
		{
			if ( c->ClassID () != Class_ID (TCBINTERP_ROTATION_CLASS_ID, 0) )
			{
#if 0
				char m [ 256 ];
				sprintf ( m, "Node %s has rotation controller ID %u.",
					inode->GetName (), c->ClassID ().PartA () );
				MessageBox ( GetActiveWindow (), m, "Debug", MB_OK );
#endif
				bad_controller_found = TRUE;
			}
			else
			{
				Quat actual_start_orientation;
				Quat desired_start_orientation;
				Quat actual_end_orientation;
				Quat desired_end_orientation;

				c->GetValue ( start_time,  & actual_start_orientation,  interval );
				c->GetValue ( start_match, & desired_start_orientation, interval );
				c->GetValue ( end_time,    & actual_end_orientation,    interval );
				c->GetValue ( end_match,   & desired_end_orientation,   interval );

				// Ensure there are keys at the beginning and end of the blend interval.

				c->SetValue ( start_time,  & actual_start_orientation );
				c->SetValue ( end_time,    & actual_end_orientation   );

				actual_end_orientation.MakeClosest ( actual_start_orientation );
				desired_start_orientation.MakeClosest ( actual_start_orientation );
				desired_end_orientation.MakeClosest ( actual_end_orientation );

				rot_start_delta = desired_start_orientation / actual_start_orientation;
				rot_end_delta   = desired_end_orientation   / actual_end_orientation;

				data.heading_delta = Heading_Delta_From_Quat
					( actual_end_orientation / actual_start_orientation );

#if 1
				int number_of_keys = c->NumKeys ();

				for ( int j = 0; j < number_of_keys; ++ j )
				{
					TimeValue key_time = c->GetKeyTime ( j );

					if ( key_time >= start_time && key_time <= end_time )
					{
						Quat orientation;

						c->GetValue ( key_time, & orientation, interval );

						float t = (float) (key_time - start_time) * t_scale;

						Quat delta = Slerp ( rot_start_delta, zero, t ) *
							Slerp ( zero, rot_end_delta, t );

						orientation = orientation * delta;

						c->SetValue ( key_time, & orientation );
					}
				}
#else
				IKeyControl * keys = GetKeyControlInterface ( c );

				if ( keys != NULL )
				{
					int number_of_keys = keys->GetNumKeys ();

					Quat prev_key_absolute (0.0,0.0,0.0,1.0);
					Quat new_prev_key_absolute (0.0,0.0,0.0,1.0);

					for ( int j = 0; j < number_of_keys; ++ j )
					{
						ITCBRotKey key;

						keys->GetKey ( j, & key );

						Quat this_key_absolute = prev_key_absolute * (Quat) key.val;
						Quat new_key_absolute = this_key_absolute;

						if ( key.time >= start_time && key.time <= end_time )
						{
							float t = (float) (key.time - start_time) * t_scale;

							Quat delta = Slerp ( rot_start_delta, zero, t ) *
								Slerp ( zero, rot_end_delta, t );

							new_key_absolute = this_key_absolute * delta;

							key.val = new_key_absolute / new_prev_key_absolute;

							keys->SetKey ( j, & key );
						}

						prev_key_absolute = this_key_absolute;
						new_prev_key_absolute = new_key_absolute;
					}
				}
#endif
			}
		}

		//--------------------------------------------------------------------
		// Get the position controller and change its keys.

		c = tm_controller->GetPositionController ();

		if ( c != NULL )
		{
			if ( c->ClassID () != Class_ID (TCBINTERP_POSITION_CLASS_ID, 0) )
			{
				bad_controller_found = TRUE;
			}
			else
			{
				Point3 actual_start_position;
				Point3 desired_start_position;
				Point3 actual_end_position;
				Point3 desired_end_position;

				c->GetValue ( start_time,  & actual_start_position,  interval );
				c->GetValue ( start_match, & desired_start_position, interval );
				c->GetValue ( end_time,    & actual_end_position,    interval );
				c->GetValue ( end_match,   & desired_end_position,   interval );

				// Ensure there are keys at the beginning and end of the blend interval.

				c->SetValue ( start_time,  & actual_start_position );
				c->SetValue ( end_time,    & actual_end_position   );

				data.position_delta = actual_end_position - actual_start_position;

#if 1
				int number_of_keys = c->NumKeys ();

				for ( int j = 0; j < number_of_keys; ++ j )
				{
					TimeValue key_time = c->GetKeyTime ( j );

					if ( key_time >= start_time && key_time <= end_time )
					{
						Point3 position;

						c->GetValue ( key_time, & position, interval );

						float t = (float) (key_time - start_time) * t_scale;

						// Calculate the position the node would be in if it
						// traveled in a straight line.

						Point3 actual_position = actual_start_position * (1.0f - t) +
							actual_end_position * t;

						// Find the delta between the straight-line position and
						// the real position.

						Point3 delta = position - actual_position;

						// Rotate the delta by the change in orientation
						// at this time.

						Quat rot_delta = Slerp ( rot_start_delta, zero, t ) *
							Slerp ( zero, rot_end_delta, t );
						Matrix3 rot_matrix;
						rot_delta.MakeMatrix ( rot_matrix );

						delta = delta * rot_matrix;

						// Add the delta to the straight-line position on the
						// desired line.

						Point3 desired_position = desired_start_position * (1.0f - t) +
							desired_end_position * t;

						position = desired_position + delta;

						// Store the new key value.

						c->SetValue ( key_time, & position );
					}
				}
#else
				IKeyControl * keys = GetKeyControlInterface ( c );

				if ( keys != NULL )
				{
					int number_of_keys = keys->GetNumKeys ();

					for ( int j = 0; j < number_of_keys; ++ j )
					{
						ITCBPoint3Key key;

						keys->GetKey ( j, & key );

						if ( key.time >= start_time && key.time <= end_time )
						{
							float t = (float) (key.time - start_time) * t_scale;

							// Calculate the position the node would be in if it
							// traveled in a straight line.

							Point3 actual_position = actual_start_position * (1.0f - t) +
								actual_end_position * t;

							// Find the delta between the straight-line position and
							// the real position.

							Point3 delta = key.val - actual_position;

							// Rotate the delta by the change in orientation
							// at this time.

							Quat rot_delta = Slerp ( rot_start_delta, zero, t ) *
								Slerp ( zero, rot_end_delta, t );
							Matrix3 rot_matrix;
							rot_delta.MakeMatrix ( rot_matrix );

							delta = delta * rot_matrix;

							// Add the delta to the straight-line position on the
							// desired line.

							Point3 desired_position = desired_start_position * (1.0f - t) +
								desired_end_position * t;

							key.val = desired_position + delta;

							// Store the new key value.

							keys->SetKey ( j, & key );
						}
					}
				}
#endif
			}
		}

		// If this node is a root node (its parent is not selected), attach
		// a data chunk that describes its motion during the blended section.

		if ( Is_Root ( inode ) )
			Set_Data_Chunk ( inode, data );
		else
			Remove_Data_Chunk ( inode );

		inode->InvalidateTM ();
	}

	ResumeAnimate ();

	TSTR undostr;
	undostr.printf ( "Blend Keys" );
	theHold.Accept ( undostr );
	SetCursor ( LoadCursor (NULL, IDC_ARROW) );

	ip->RedrawViews ( ip->GetTime () );

	if ( bad_controller_found )
	{
		MessageBox ( GetActiveWindow (),
			"Warning: Some controllers were not blended\n"
			"because they were not TCB controllers.", "Warning", MB_OK );
	}
}

//----------------------------------------------------------------------------
// Blender_Class::Is_Root
//----------------------------------------------------------------------------

BOOL Blender_Class::Is_Root ( INode * node )
{
	node = node->GetParentNode ();

	int number_of_nodes = ip->GetSelNodeCount ();

	for ( int i = 0; i < number_of_nodes; ++ i )
	{
		if ( ip->GetSelNode ( i ) == node )
			return FALSE;
	}

	return TRUE;
}

//----------------------------------------------------------------------------
// Blender_Class::Heading_Delta_From_Quat
//----------------------------------------------------------------------------

float Blender_Class::Heading_Delta_From_Quat ( Quat q )
{
	Matrix3 rot_matrix;
	q.MakeMatrix ( rot_matrix );

	Point3 row = rot_matrix.GetRow ( 0 );

	return (float) atan2 ( row.y, row.x );
}

//----------------------------------------------------------------------------
// Blender_Class::Set_Data_Chunk
//----------------------------------------------------------------------------

void Blender_Class::Set_Data_Chunk
(
	INode * node,
	const Blender_Data_Chunk & new_data
)
{
	AppDataChunk * chunk = node->GetAppDataChunk
	(
		Blender_Class_ID,
		UTILITY_CLASS_ID,
		1
	);

	Blender_Data_Chunk * data;

	if ( chunk != NULL )
	{
		data = (Blender_Data_Chunk *) chunk->data;
	}
	else
	{
		data = (Blender_Data_Chunk *) malloc ( sizeof (Blender_Data_Chunk) );

		node->AddAppDataChunk
		(
			Blender_Class_ID,
			UTILITY_CLASS_ID,
			1,
			sizeof (Blender_Data_Chunk),
			data
		);
	}

	*data = new_data;

#if 0
	char m [ 256 ];
	sprintf ( m, "Setting data chunk for %s:\n"
		"Position delta = (%.1f, %.1f, %.1f)\n"
		"Heading delta = %.1f",
		node->GetName (),
		data->position_delta.x,
		data->position_delta.y,
		data->position_delta.z,
		data->heading_delta * 180 / PI );
	MessageBox ( GetActiveWindow (), m, "Debug", MB_OK );
#endif
}

//----------------------------------------------------------------------------
// Blender_Class::Remove_Data_Chunk
//----------------------------------------------------------------------------

void Blender_Class::Remove_Data_Chunk ( INode * node )
{
	node->RemoveAppDataChunk
	(
		Blender_Class_ID,
		UTILITY_CLASS_ID,
		1
	);
}

//----------------------------------------------------------------------------
// Blender_Class::Loop_Controllers
//----------------------------------------------------------------------------

void Blender_Class::Loop_Controllers ()
{
	int number_of_nodes = ip->GetSelNodeCount ();

	for ( int i = 0; i < number_of_nodes; ++ i )
	{
		// Get the inode.

		INode * inode = ip->GetSelNode ( i );

		Control * tm_controller = inode->GetTMController ();
		if ( tm_controller == NULL )
			continue;

		Control * c;

		c = tm_controller->GetRotationController ();
		if ( c )
		{
			c->SetORT ( ORT_LOOP, ORT_BEFORE );
			c->SetORT ( ORT_LOOP, ORT_AFTER );
		}

		c = tm_controller->GetPositionController ();
		if ( c )
		{
			c->SetORT ( ORT_LOOP, ORT_BEFORE );
			c->SetORT ( ORT_LOOP, ORT_AFTER );
		}

		c = tm_controller->GetScaleController ();
		if ( c )
		{
			c->SetORT ( ORT_LOOP, ORT_BEFORE );
			c->SetORT ( ORT_LOOP, ORT_AFTER );
		}
	}
}
