//================================================================
// EQUINOX-3D(TM) plugin
//
// Path constraint animation
//
// AUTHOR:  Gabor Nagy
// DATE:    2026-Apr-24 21:47:12
//
// Copyright (C) 2014-2026 By Gabor Nagy. All rights reserved.
//================================================================
#include <stdio.h>
#include <math.h>

#include <ELists.h>
#include <EMalloc.h>
#include <EPlugins.h>

#include <E3D/E3D.h>
#include <E3D/Matrix.h>
#include <E3D/Scene.h>

#include <E3D/UI/ModeStack.h>  // E3dFIXME : For E3dp_ResetWorkModes, temporarily
#include <E3D/UI/Panel.h>
#include <E3D/UI/StatusPanel.h>
#include <E3D/UI/TimePanel.h>

#include <Xe/PushButton.h>

static EPlugin*  _Plugin = NULL;

static XeDWidget  _MenuButtons[2];static int _MenuButtons_Count = 0;

enum
{
	CR_PATH_ANIM = 1
};


typedef struct
{
// E3dAnimation part
//
	E3dAnimationCore;

// E3dPathAnimation part
//
	E3dModel*   PathModel;                // Pointer to the Spline's Model
	E3dSpline*  PathSpline;               // Pointer to the Spline
	E3dModel*   TargetModel;              // The animated Model
	float       AutoBankFactor;           // Automatic banking in turns

	E3dCoordinate  T;                     // T parameter 0 : beginning of path  1.0 : end of path
	E3dCoordinate  CameraForwardLooking;  // If the animated Model is a Camera, put its interest point in front automatically
} E3dPathAnimation;



#define ROffset(field)  (void*)(EPtrOffset(E3dPathAnimation, field))
static EResource  _Resources[]=
{
	{ "PathModel",     EResOBJECT,          ROffset(PathModel) },
	{ "PathSpline",    EResOBJECT,          ROffset(PathSpline) },
	{ "TargetModel",   EResOBJECT,          ROffset(TargetModel) },
	{ "T",	           E3dCOORDINATE_TYPE,  ROffset(T) },

//	{ "EaseIn",        EResFLOAT32,         ROffset(EaseIn) },
//	{ "EaseOut",       EResFLOAT32,         ROffset(EaseOut) },
//	{ "AutoBank",      EResFLOAT32          ROffset(AutoBankFactor) },
};
#undef ROffset



static E3dAnimationClass*  _Animation_class = NULL;


//================================================
// Path animation SetFrame procedure
//================================================
static EChangeFlags _SetFrame(E3dChangeList* PChangeList, E3dAnimation* PAnimation, EtTimeCallbackStruct* PTimeCBStruct)
{
	E3dPathAnimation*  LPathAnimation = (E3dPathAnimation*)PAnimation;
	E3dModel*          LModel = LPathAnimation->TargetModel;

	if(LModel == NULL)
	{
		printf("%s() %s:%d [%s] %sNO MODEL%s\n", __FUNCTION__, __FILE__, __LINE__, LPathAnimation->Name, ECodeYellow, ECodeNormal);fflush(stdout);
		return(0);
	}

	E3dSpline*  LPathSpline = LPathAnimation->PathSpline;

	if(LPathSpline == NULL)
	{
		printf("%s() %s:%d [%s] %sNO PATH%s\n", __FUNCTION__, __FILE__, __LINE__, LPathAnimation->Name, ECodeYellow, ECodeNormal);fflush(stdout);
		return(0);
	}

	E3dCoordinate  LT;

// For seamless looping on closed Splines...
//
/*
// Old way: PathAnimation directly registered in Scene
//
	{
		EtTime  LFrame = PTimeCBStruct->Frame;

		if(LPathSpline->Closed) LT = (LFrame - E3dp_TimePanel->StartFrame) / (E3dp_TimePanel->EndFrame - E3dp_TimePanel->StartFrame + 1.0);
		else LT = (LFrame - E3dp_TimePanel->StartFrame) / (E3dp_TimePanel->EndFrame - E3dp_TimePanel->StartFrame);
	}
*/

// New way: PathAnimation's t parameter is driven by a keyframe animation
//
	if(LPathSpline->Closed) LT = LPathAnimation->T;
	else LT = LPathAnimation->T;


	E3dCoord3      L3DPosition;E3dSpline_Get3DPosition(LPathSpline, LT,  &L3DPosition, NULL, NULL);


// Transform the position by the LocalToWorldMatrix of the 0th Model of the Spline
//
	{
		E3dModel*  LSplModel = LPathAnimation->PathModel;if(LSplModel == NULL)
		{
			printf("%s() %s:%d [%s] NO PATH MODEL\n", __FUNCTION__, __FILE__, __LINE__, LPathAnimation->Name);fflush(stdout);
			return(0);
		}

		EMatrix_Transform3x4_v(LSplModel->LocalToWorldMatrix, L3DPosition);
	}

	LModel->TransformSet.Translation = L3DPosition;

	EChangeFlags   LSceneChanged = 0;

	E3dScene*  LScene = E3d_Scene;

	{
		EIndex  LI = 0;const EIndex  LN = LModel->Objects.Count;for(;LI < LN;LI++)
		{
			E3dObject*  LObject = LModel->Objects.Items[LI];

			if(EObject_IsSubClass(LObject, E3dCamera))
			{
				E3dCamera*  LCamera = (E3dCamera*)LObject;

#if !ERELEASE_BUILD
printf("%s() %s:%d FIXMECAMERA\n", __FUNCTION__, __FILE__, __LINE__);fflush(stdout);
#endif

// Automatic forward-looking camera
//
				if(LPathAnimation->CameraForwardLooking != 0.0)
				{
/*
					E3dSpline_Get3DPosition(LPathSpline, LT + LPathAnimation->CameraForwardLooking,  &L3DPosition, NULL, NULL);

					if(LInterestModel) LInterestModel->TransformSet.Translation = L3DPosition;
*/
				}

				if(LPathAnimation->AutoBankFactor != 0.0)
				{
				}

				{
					E3dModel*  LCameraModel = LModel;
					E3dCoord3  LAimPoint, LUpVector;E3dCoordinate  LBankAngle;E3dModel_GetAim(LScene, LCameraModel,  &LAimPoint, NULL, NULL, &LUpVector, &LBankAngle);

					E3dCamera_LookAt(LScene,  LCamera, LCameraModel,  &L3DPosition, &LAimPoint, &LUpVector, LBankAngle);
				}

				{
					ECallbackStruct  LCBS;ECallbackStruct_Init(&LCBS, NULL,  E3dCR_CAMERA_CHANGED);
					E3d_CallCallbacks((ECoreObject*)LCamera,  LCamera->Callbacks, LCamera->Callbacks_Count,  &LCBS);
				}
			}
		}

//{ char  s[256];printf("%s() %s:%d [%s]\n", __FUNCTION__, __FILE__, __LINE__, E3dSceneChangeFlags_ToString(LSceneChanged, EfCOLOR,  s));fflush(stdout); }
		LSceneChanged |= E3dNodeHrc_GetTransformChangeFlagsByObjects((E3dNode*)LModel) | E3dChgNodeTRANSFORM;

//printf("%s() %s:%d  %d  %p\n", __FUNCTION__, __FILE__, __LINE__, PChangeList->Entries_Count, LModel);fflush(stdout);
		{
			E3dChangeListEntry*  LEntry = E3dChangeList_EntryGetOrAdd(PChangeList, (ECoreObject*)LModel, NULL);if(LEntry)
			{
				LEntry->ChangeFlags |= E3dChgNodeTRANSFORM;
			}
		}

//printf("%s() %s:%d  %d %p %p %p\n", __FUNCTION__, __FILE__, __LINE__, PChangeList->Entries_Count, PChangeList, PChangeList->Entries, PChangeList->Entries[0]);fflush(stdout);
	}


//printf("SetFrame %f\n", LFrame);fflush(stdout);
	return(LSceneChanged);
}


//================================================================
// Path animation pick-spline callback
//================================================================
static void _PickCB(E3dPickCallbackStruct* PPickStruct, EPointer PClientData,  const EIndex PNumOfItems, E3dItem* PPickedItems)
{
	if(PPickStruct->Reason == E3dCR_ABORT) return;

	if(PPickStruct->Button == Button3)
	{
		E3dp_PickEnd();
		return;
	}

	switch(PPickStruct->Button)
	{
		case Button1:
			if(PNumOfItems)
			{
				if(PPickedItems[0].Item.Type == E3dItemSPLINE)
				{
					E3dScene*       LScene = E3d_Scene;
					E3dSplineItem*  LSplineItem = (E3dSplineItem*)PPickedItems;
					E3dModel*       LPathModel = (E3dModel*)(LSplineItem->Node);
					E3dSpline*      LSpline = LSplineItem->Spline;

					E3dp_PickEnd();

					if(LPathModel == NULL) LPathModel = LSpline->Models.Items[0];

					EListAI_s(, E3dNodePtr, LSelectedNodes);const EIndex  LNSelectedNodes = E3dScene_GetSelectedNodes(LScene, E3dNodeSelANY, FALSE,  &LSelectedNodes);if(LNSelectedNodes > 0)
					{
						EChangeFlags  LSceneChanged = 0;

						E3dChangeList_Clear(&LScene->ChangeList, FALSE);
						EIndex  LI = 0;for(;LI < LNSelectedNodes;LI++)
						{
							E3dNode*   LNode = LSelectedNodes.Items[LI];
							E3dModel*  LAnimatedModel = E_DynamicCast(LNode, E3dModel);if(LAnimatedModel)
							{
								const EtTimePanel*  LTimePanel = E3dp_TimePanel;
								E3dPathAnimation*   LPathAnimation;E_NewC(_Animation_class, LPathAnimation);EObject_SetName(LPathAnimation, EStrDup("Path"), FALSE);

// A Path animations is not driven directly by the main timeline. It has a key-frame animation to drive it.
// However, we need to add it to the Scene, so it will get saved to files.
//
								E3dScene_AnimationAppend(LScene, (E3dAnimation*)LPathAnimation);

								if(LPathAnimation)
								{
// Add keyframe animation that drives the path Animation's "T" parameter
//
									EResource*                    LResource = EResource_ByName(&_Animation_class->Resources, "T");
									E3dKeyFrameAnimation*         LAnimation = (E3dKeyFrameAnimation*)E3dScene_AnimationAdd(LScene, E3dKeyFrameAnimation_class);
									E3dKeyFrameAnimationChannel*  LChannel;

									EObject_SetName(LAnimation, EStrDup("PathKeys"), FALSE);

									E3dKeyFrameAnimation_AddKey(LAnimation, &LChannel, "T", Ec_RGBAiToRGBAp8(&E3dp_Prefs.AxisWRGBAiColor),  LTimePanel->StartFrame / LTimePanel->FramesPerSecond, 0.0, 0.0, E3dSplineBEZIER, E3dp_Prefs.NewAnimKeyEaseInOut,   (EObject*)LPathAnimation, NULL, LResource, 0,  TRUE);

									if(LSpline->Closed) E3dKeyFrameAnimation_AddKey(LAnimation, &LChannel,  "T", Ec_RGBAiToRGBAp8(&E3dp_Prefs.AxisWRGBAiColor),  LTimePanel->EndFrame / LTimePanel->FramesPerSecond, 0.0, (LTimePanel->EndFrame - LTimePanel->StartFrame) / (LTimePanel->EndFrame - LTimePanel->StartFrame + 1), E3dSplineLINEAR, E3dp_Prefs.NewAnimKeyEaseInOut,  (EObject*)LPathAnimation, NULL, LResource, 0,  TRUE);
									else E3dKeyFrameAnimation_AddKey(LAnimation, &LChannel,  "T", Ec_RGBAiToRGBAp8(&E3dp_Prefs.AxisWRGBAiColor),  LTimePanel->EndFrame/LTimePanel->FramesPerSecond, 0.0, 1.0, E3dSplineBEZIER, E3dp_Prefs.NewAnimKeyEaseInOut,  (EObject*)LPathAnimation, NULL, LResource, 0,  TRUE);

									E_AssignToMember(LAnimatedModel, LPathAnimation,TargetModel);
									E_AssignToMember(LPathModel, LPathAnimation,PathModel);
									E_AssignToMember(LSpline, LPathAnimation,PathSpline);

									LPathAnimation->AutoBankFactor = 0.2;
//									LPathAnimation->CameraForwardLooking = 0.005;
									LPathAnimation->CameraForwardLooking = 0.0;

//									LSceneChanged |= E3dAnimation_CallWithCurrentFrame((E3dAnimation*)LPathAnimation);
									LSceneChanged |= E3dAnimation_CallWithCurrentFrame((E3dAnimation*)LAnimation);

									{
										E3dNode*  LRootNode = (E3dNode*)LAnimatedModel;while(LRootNode->Parent) LRootNode = LRootNode->Parent;
										
										E3dNodeHrc_UpdateMatrices(LRootNode, TRUE);
										E3dNodeHrc_CallNodeInfoUpdateProcs((E3dNode*)LAnimatedModel, E3dChgNodeTRANSFORM, FALSE);
									}
								}
							}
						}
						EList_FreeA_s(LSelectedNodes);

						LPathModel->Selection = E3dNodeSelOBJECT;
						LSpline->Selection = E3dObjectSelOBJECT;

						const E3dpPrefs*  LPrefs = &E3dp_Prefs;
						const uint32_t    LLabelRGBAp8Color = Xe_ForegroundRGBA8pColor;
						E3dp_PrintCompoundMessageVa(ELogMESSAGE, 3000,  XeFONT, XeFontDEFAULT,  XeTEXT, "Picked '",  E3dp_NodeS(LPathModel->Name),  XeCOLOR_RGBA, LLabelRGBAp8Color,  XeTEXT, "' for path",  0);

						E3dWindows_RedrawByChangeFlags(LScene, LSceneChanged|E3dChgANIMATION);
					}
				}
				else E3dp_PrintMessage(ELogWARNING, 3000, "Pick a Spline for path");
			}
		break;
	}
}


//========================================
// Menu callback
//========================================
static void _MCB(XeWidget PW, EPointer PClientData, EPointer PCallData)
{
	switch((EPointerInt)PClientData)
	{
		case CR_PATH_ANIM:
			{
				const EIndex  LNSelectedNodes = E3dScene_GetSelectedNodes(E3d_Scene, E3dNodeSelANY, FALSE,  NULL);if(LNSelectedNodes > 0)
				{
					const E3dpPrefs*  LPrefs = &E3dp_Prefs;
					const uint32_t    LLabelRGBAp8Color = Xe_ForegroundRGBA8pColor;

					XeString  LXeString = XeString_NewVa(TRUE,
					 XeFONT, XeFontDEFAULT,  XeCOLOR_RGBA, LLabelRGBAp8Color,  XeTEXT, "Path animation   ",
					 E3dp_LMouseButtonS,     XeTEXT, ": pick path Spline    ",
					 E3dp_RMouseButtonEscS,  XeTEXT, ": end mode",
					 0);

					E3dp_ResetWorkModes();
					E3dp_PickRequest(LXeString, E3dItemSPLINE,  _PickCB, NULL,  0, FALSE);
				}
				else E3dp_PrintMessage(ELogWARNING, 3000, "Select Model(s) to animate");
			}
		break;
	}
}


//========================================
// Key callback
//========================================
static void _KCB(const EKeyCode PKey, const unsigned int PModifiers, const int PKeyEventType, EPointer PClientData)
{
	_MCB(NULL, PClientData, NULL);
}


//================================================
// Animation constructor
//================================================
static void _Init(E3dAnimation* PAnimation)
{
	E3dPathAnimation*  LThis = (E3dPathAnimation*)PAnimation;

	LThis->Active = FALSE;  // PathAnimations are not directly driven from the main timeline (its SetFrame method is not called from there). It's driven by a KeyFrameAnimation created below:
	LThis->TargetModel = NULL;
	LThis->PathModel = NULL;
	LThis->PathSpline = NULL;
	LThis->AutoBankFactor = 0.0;
	LThis->T = 0.0;
	LThis->CameraForwardLooking = 0.0;
}

//================================================
// Animation destructor
//================================================
static int _Free(E3dAnimation* PAnimation)
{
	E3dPathAnimation*  LThis = (E3dPathAnimation*)PAnimation;

	E_DeleteChk(LThis->TargetModel);
	E_DeleteChk(LThis->PathModel);
	E_DeleteChk(LThis->PathSpline);

	return(E_SUCCESS);
}

//================================================
// Animation TargetSelected method
//================================================
static EBool _TargetSelected(const E3dAnimation* PAnimation)
{
	E3dPathAnimation*  LThis = (E3dPathAnimation*)PAnimation;

	if(LThis->TargetModel && LThis->TargetModel->Selection) return(TRUE);
	return(FALSE);
}


//========================================
// Entry point of the plugin
//========================================
int E_PluginInit(EPlugin* PPlugin)
{
	_Plugin = PPlugin;

	{
		E3dAnimationClass  LClass=
		{
			.SuperClass=         E3dAnimation_class,
			.Name=               "Path animation",
			.StructSize=         sizeof(E3dPathAnimation),
			.Resources=          EListS_s(_Resources),

			.Init=               _Init,
			.Free=               _Free,
			.SetFrame=           _SetFrame,
			.TargetSelected=     _TargetSelected,
			.ChannelStructSize=  sizeof(E3dPathAnimation)
		};
		_Animation_class = E3dAnimation_ClassRegister(&LClass);
	}

	if(_Animation_class)
	{
		_MenuButtons[_MenuButtons_Count++] = XePushButton_Add(Xe_App, "Menu->Animation", "Simple path animation", '\0', NULL, "J", FALSE, NULL, _MCB, (EPointer)CR_PATH_ANIM);

		E_KeySetCallback("Path animation", 'J', 0, EKeyPRESS, _KCB, (EPointer)CR_PATH_ANIM);
		return(E_SUCCESS);
	}

	return(E_UNKNOWN_ERROR);
}


//========================================
// Exit method of the plugin
//========================================
int E_PluginExit(const EBool PExiting)
{
	if(!PExiting || Xe_GUIFreeOnExit)
	{
		int  LI = 0;for(;LI < _MenuButtons_Count;LI++) { XeItemFree(_MenuButtons[LI]); } _MenuButtons_Count = 0;
	}

	if(_Animation_class) E3dAnimation_ClassDeactivate(_Animation_class);

	E_KeyRemoveCallback('J', 0, EKeyPRESS, _KCB, (EPointer)CR_PATH_ANIM);
	return(E_SUCCESS);
}
