Code/SbTween/Core/BaseTween.cs
using Sandbox;
using System;

namespace SbTween;

public class BaseTween
{
	// Core Settings 
	public float Duration { get; set; }
	public float Elapsed { get; private set; }
	public float Delay { get; private set; }
	public EaseType Ease { get; set; } = EaseType.Linear;
	public Func<float, float> TimingFunction { get; set; }
	public GameObject Target { get; set; }
	public string Id { get; set; }


	// Looping 
	public LoopType LoopMode { get; set; } = LoopType.Restart;
	public int Loops { get; private set; } = 0; // 0 = once, -1 = infinity.
	private int _loopsDone = 0;

	// State 
	public bool IsFinished { get; private set; }
	public bool IsPaused { get; private set; }
	public bool IsReversed { get; private set; }

	// Events
	private Action _onStart;
	private Action _onComplete;
	private Action _onLoop;
	private Action<float> _onUpdate;

	private bool _hasStarted = false;

	public BaseTween( float duration )
	{
		Duration = duration;
	}

	public BaseTween SetDelay( float seconds )
	{
		Delay = seconds;
		return this;
	}

	public BaseTween SetId( string id )
	{
		Id = id;
		return this;
	}
	protected float GetEasedProgress( float p )
	{
		return TimingFunction != null ? TimingFunction( p ) : p;
	}

	public void Update( float deltaTime )
	{
		if ( IsFinished || IsPaused ) return;

		if ( Delay > 0 )
		{
			Delay -= deltaTime;
			return;
		}

		if ( !_hasStarted )
		{
			_onStart?.Invoke();
			_hasStarted = true;
		}

		if ( IsReversed )
			Elapsed -= deltaTime;
		else
			Elapsed += deltaTime;

		float progress = Math.Clamp( Elapsed / Duration, 0, 1 );
		float finalProgress = TimingFunction != null ? TimingFunction( progress ) : Easing.Apply( Ease, progress );

		if ( LoopMode == LoopType.Incremental )
		{
			_onUpdate?.Invoke( finalProgress + _loopsDone );
		}
		else
		{
			_onUpdate?.Invoke( finalProgress );
		}

		if ( (!IsReversed && progress >= 1.0f) || (IsReversed && progress <= 0f) )
		{
			if ( Loops == -1 || _loopsDone < Loops )
			{
				_loopsDone++;
				_onLoop?.Invoke();

				if ( LoopMode == LoopType.YoYo )
				{
					IsReversed = !IsReversed;
				}
				else
				{
					Elapsed = 0;
				}
			}
			else
			{
				IsFinished = true;
				_onComplete?.Invoke();
			}
		}
	}

	// Chaining Method
	public BaseTween SetEase( EaseType ease ) { Ease = ease; return this; }
	public BaseTween SetLoops( int loops, LoopType type = LoopType.Restart )
	{
		Loops = loops;
		LoopMode = type;
		return this;
	}

	public BaseTween OnStart( Action a ) { _onStart = a; return this; }
	public BaseTween OnUpdate( Action<float> a ) { _onUpdate = a; return this; }
	public BaseTween OnComplete( Action a ) { _onComplete = a; return this; }
	public BaseTween OnLoop( Action a ) { _onLoop = a; return this; }

	public BaseTween WithCurve( Curve curve )
	{
		TimingFunction = t => curve.Evaluate( t );
		return this;
	}

	// Playback 
	public void Pause() => IsPaused = true;
	public void Play()
	{
		IsPaused = false;
		IsFinished = false;

		if ( TweenManager.Current is { } Tweener )
		{
			Tweener.AddTween( this );
		}
	}
	public void Kill() => IsFinished = true;
	public void Reverse() => IsReversed = !IsReversed;
	public void Reset()
	{
		Elapsed = 0;
		IsFinished = false;
		IsPaused = true;
		_hasStarted = false;
	}

	public void Stop()
	{
		IsFinished = true;
		IsPaused = true;

		if ( TweenManager.Current is { } Tweener )
		{
			Tweener.RemoveTween( this );
		}
	}
}

public enum LoopType
{
	Restart,    // 0 to 1, 0 -> 1
	YoYo,       // 0 to 1 to 0
	Incremental // 0 to 1, then 1 -> 2, then 2 -> 3 etc etc.
}