TweenManager.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Sandbox;

namespace Braxnet;

public sealed class TweenManager : GameObjectSystem
{
	public static TweenManager Instance => Game.ActiveScene.GetSystem<TweenManager>();

	private List<Tween> _tweens = new();

	public TweenManager( Scene scene ) : base( scene )
	{
		Listen( Stage.StartUpdate, 0, Update, "Tween" );
	}

	public void RemoveTween( Tween tween )
	{
		_tweens.Remove( tween );
	}

	private void Update()
	{
		foreach ( var tween in _tweens.ToList() )
		{
			// Log.Info( $"Tween > {tween.PropertyCount}" );
			tween.Update();
			/*if ( tween.IsFinished )
			{
				// Log.Info( "Tween finished" );
				_tweens.Remove( tween );
			}*/
		}
	}

	/*public Tween Add( float duration, Action<float> onUpdate, Action onFinish = null )
	{
		var tween = new Tween
		{
			Duration = duration,
			OnUpdate = onUpdate,
			OnFinish = onFinish,
		};
		tween.StartTime = 0f;
		_tweens.Add( tween );
		return tween;
	}*/

	public Tween Create()
	{
		var tween = new Tween();
		_tweens.Add( tween );
		return tween;
	}

	public static Tween CreateTween()
	{
		var instance = Game.ActiveScene.GetSystem<TweenManager>();
		return instance.Create();
	}
}

public class BaseTween
{
	public GameObject GameObject;
	public Action OnFinish;
	public Action OnBounce;

	public Action<GameObject, float> OnUpdate;

	public float Duration;
	public TimeSince StartTime;

	public bool Parallel;

	public float Progress => Math.Clamp( StartTime / Duration, 0, 1 );

	public bool IsFinished => StartTime > Duration;

	public Func<float, float> EaseFunction;

	public bool IsSetup;

	/// <summary>
	///  Execute before the tween starts
	/// </summary>
	public virtual void Setup()
	{
		GameObject?.Tags.Add( "tweening" );
		StartTime = 0f;
	}

	/// <summary>
	///  Execute after the tween finishes
	/// </summary>
	public virtual void Cleanup()
	{
		GameObject?.Tags.Remove( "tweening" );
	}

	public BaseTween SetEasing( Func<float, float> easing )
	{
		EaseFunction = easing;
		return this;
	}

	public BaseTween SetDelay( float delay )
	{
		StartTime = -delay;
		return this;
	}

	public async Task Wait()
	{
		await GameTask.DelaySeconds( Duration );
	}

	public virtual void Update()
	{
	}

	protected float GetFraction()
	{
		return EaseFunction != null
			? EaseFunction( Math.Clamp( StartTime / Duration, 0, 1 ) )
			: Math.Clamp( StartTime / Duration, 0, 1 );
	}
}

public sealed class FloatTween : BaseTween
{
	public override void Update()
	{
		// OnUpdate( GameObject, Math.Clamp( StartTime / Duration, 0, 1 ) );

		if ( !GameObject.IsValid() )
		{
			StartTime = Duration;
			return;
		}

		/*if ( EaseFunction != null )
		{
			OnUpdate( GameObject, EaseFunction( Math.Clamp( StartTime / Duration, 0, 1 ) ) );
		}
		else
		{
			OnUpdate( GameObject, Math.Clamp( StartTime / Duration, 0, 1 ) );
		}
		*/

		OnUpdate?.Invoke( GameObject, GetFraction() );

		if ( IsFinished )
		{
			OnFinish?.Invoke();
		}
	}
}

public sealed class PositionTween : BaseTween
{
	public Vector3 StartPosition;
	public Vector3 EndPosition;

	public override void Setup()
	{
		base.Setup();
		StartPosition = GameObject.WorldPosition;
	}

	private bool _bounceDebounce;

	public override void Update()
	{
		if ( !GameObject.IsValid() )
		{
			StartTime = Duration;
			return;
		}

		if ( StartTime < 0 ) return;

		/*if ( EaseFunction != null )
		{
			GameObject.WorldPosition = Vector3.Lerp( StartPosition, EndPosition, EaseFunction( Math.Clamp( StartTime / Duration, 0, 1 ) ) );
		}
		else
		{
			GameObject.WorldPosition = Vector3.Lerp( StartPosition, EndPosition, Math.Clamp( StartTime / Duration, 0, 1 ) );
		}*/

		// var frac = EaseFunction != null ? EaseFunction( Math.Clamp( StartTime / Duration, 0, 1 ) ) : Math.Clamp( StartTime / Duration, 0, 1 );
		var frac = GetFraction();

		GameObject.WorldPosition = Vector3.Lerp( StartPosition, EndPosition, frac );

		// Log.Info( GameObject.Name + ": " + GameObject.WorldPosition.Distance( EndPosition ) + " " + frac );
		if ( Progress < 0.9f )
		{
			var dist = GameObject.WorldPosition.Distance( EndPosition );
			if ( dist.Floor() == 0 )
			{
				if ( !_bounceDebounce )
				{
					OnBounce?.Invoke();
					_bounceDebounce = true;
				}
			}
			else
			{
				_bounceDebounce = false;
			}
		}

		if ( IsFinished )
		{
			OnFinish?.Invoke();
		}
	}
}

public sealed class ScaleTween : BaseTween
{
	public Vector3 StartScale;
	public Vector3 EndScale;

	public override void Setup()
	{
		base.Setup();
		StartScale = GameObject.WorldScale;
	}

	public override void Update()
	{
		if ( !GameObject.IsValid() )
		{
			StartTime = Duration;
			return;
		}

		/*if ( EaseFunction != null )
		{
			GameObject.WorldScale = Vector3.Lerp( StartScale, EndScale, EaseFunction( Math.Clamp( StartTime / Duration, 0, 1 ) ) );
		}
		else
		{
			GameObject.WorldScale = Vector3.Lerp( StartScale, EndScale, Math.Clamp( StartTime / Duration, 0, 1 ) );
		}*/

		GameObject.WorldScale = Vector3.Lerp( StartScale, EndScale, GetFraction() );

		// ( $"{GameObject.Name} {GameObject.WorldScale.z} {GetFraction()}" );

		if ( IsFinished )
		{
			OnFinish?.Invoke();
		}
	}
}

public sealed class RotationTween : BaseTween
{
	public Rotation StartRotation;
	public Rotation EndRotation;
	public bool Local;

	public override void Setup()
	{
		base.Setup();
		StartRotation = Local ? GameObject.LocalRotation : GameObject.WorldRotation;
	}

	public override void Update()
	{
		if ( !GameObject.IsValid() )
		{
			StartTime = Duration;
			return;
		}

		/*if ( EaseFunction != null )
		{
			GameObject.WorldRotation = Rotation.Lerp( StartRotation, EndRotation, EaseFunction( Math.Clamp( StartTime / Duration, 0, 1 ) ) );
		}
		else
		{
			GameObject.WorldRotation = Rotation.Lerp( StartRotation, EndRotation, Math.Clamp( StartTime / Duration, 0, 1 ) );
		}*/

		var frac = GetFraction();

		if ( Local )
		{
			GameObject.LocalRotation = Rotation.Lerp( StartRotation, EndRotation, frac );
		}
		else
		{
			GameObject.WorldRotation = Rotation.Lerp( StartRotation, EndRotation, frac );
		}

		if ( StartRotation.Distance( EndRotation ).AlmostEqual( 0 ) && frac < 0.9f )
		{
			OnBounce?.Invoke();
		}

		if ( IsFinished )
		{
			OnFinish?.Invoke();
		}
	}
}

public sealed class Tween
{
	public int PropertyCount => _propertyTweens.Count;

	// private int _currentTweenIndex;
	private List<BaseTween> _propertyTweens = new();

	public bool IsFinished => _propertyTweens.Count == 0;

	public Action OnFinish;

	private TaskCompletionSource<bool> _taskCompletionSource;

	// private int _currentTweenIndex;

	public FloatTween AddFloat( GameObject gameObject, float duration, Action<GameObject, float> onUpdate,
		Action onFinish = null )
	{
		var tween = new FloatTween
		{
			GameObject = gameObject, Duration = duration, OnUpdate = onUpdate, OnFinish = onFinish,
		};
		_propertyTweens.Add( tween );
		// tween.Setup();
		return tween;
	}

	public PositionTween AddPosition( GameObject resourceModel, Vector3 transformPosition, float duration )
	{
		var tween = new PositionTween
		{
			GameObject = resourceModel, Duration = duration, EndPosition = transformPosition,
		};
		_propertyTweens.Add( tween );
		// tween.Setup();
		return tween;
	}

	public ScaleTween AddScale( GameObject resourceModel, Vector3 transformScale, float duration )
	{
		var tween = new ScaleTween { GameObject = resourceModel, Duration = duration, EndScale = transformScale, };
		_propertyTweens.Add( tween );
		// tween.Setup();
		return tween;
	}

	public RotationTween AddRotation( GameObject resourceModel, Rotation rotation, float duration )
	{
		var tween = new RotationTween { GameObject = resourceModel, Duration = duration, EndRotation = rotation, };
		_propertyTweens.Add( tween );
		// tween.Setup();
		return tween;
	}

	public RotationTween AddLocalRotation( GameObject model, Rotation rotation, float duration )
	{
		var tween = new RotationTween { GameObject = model, Duration = duration, EndRotation = rotation, Local = true };
		_propertyTweens.Add( tween );
		// tween.Setup();
		return tween;
	}

	public void Update()
	{
		/*foreach ( var tween in _propertyTweens.ToList() )
		{
			tween.Update();
			if ( tween.IsFinished )
			{
				// Log.Info( "PropertyTween finished" );
				tween.Cleanup();
				_propertyTweens.Remove( tween );
			}
		}*/

		if ( _propertyTweens.Count == 0 )
		{
			// Log.Info( "Tween finished" );
			OnFinish?.Invoke();
			_taskCompletionSource?.SetResult( true );
			_taskCompletionSource = null;
			TweenManager.Instance.RemoveTween( this );
			return;
		}

		// Log.Info( $"Tween has {_propertyTweens.Count} tweens" );

		// separately handle parallel tweens
		var parallelTweens = _propertyTweens.Where( x => x.Parallel ).ToList();
		foreach ( var tween in parallelTweens )
		{
			if ( !tween.IsSetup )
			{
				tween.Setup();
				tween.IsSetup = true;
			}

			tween.Update();

			if ( tween.IsFinished )
			{
				// Log.Info( $"Parallel tween finished, {_propertyTweens.Count} tweens left" );
				tween.Cleanup();
				_propertyTweens.Remove( tween );
			}
		}

		var currentTween = _propertyTweens.FirstOrDefault( x => !x.Parallel );

		if ( currentTween == null )
		{
			// Log.Warning( $"Tween is null, no tweens left" );
			return;
		}

		if ( !currentTween.IsSetup )
		{
			currentTween.Setup();
			currentTween.IsSetup = true;
		}

		currentTween.Update();

		if ( currentTween.IsFinished )
		{
			currentTween.Cleanup();
			_propertyTweens.Remove( currentTween );

			// Log.Info( $"Sync tween finished, {_propertyTweens.Count} tweens left" );

			/*if ( _propertyTweens.Count > 0 )
			{
				_currentTweenIndex = Math.Clamp( _currentTweenIndex + 1, 0, _propertyTweens.Count - 1 );
			}*/
		}
	}

	public async Task Wait()
	{
		_taskCompletionSource = new TaskCompletionSource<bool>();
		await _taskCompletionSource.Task;
	}
}