Code/SbTween/Core/TweenPlayer.cs
using Sandbox;
using Sandbox.Services;
using System;

namespace SbTween;

[Title( "SbTween Player" )]
[Category( "Tweening" )]
[Icon( "play_arrow" )]
public sealed class SbTweenPlayer : Component, Component.ExecuteInEditor
{
	// BUTTONS
	[Group( "Buttons" ), Button( "Play", "play_arrow" )]
	public void PlayButton() => Play();

	[Group( "Buttons" ), Button( "Stop", "stop" )]
	public void StopButton() => Stop();


	// SETUP
	[Property, Group( "Setup" )] public TweenType Type { get; set; } = TweenType.Move;


	// COLOR
	[Property, Group( "Setup" ), ShowIf( nameof( IsVector ), true )] public Vector3 From { get; set; }
	[Property, Group( "Setup" ), ShowIf( nameof( IsVector ), true )] public Vector3 To { get; set; }

	[Property, Group( "Setup" ), ShowIf( nameof( Type ), TweenType.Tint )] public Color ColorFrom { get; set; } = Color.White;
	[Property, Group( "Setup" ), ShowIf( nameof( Type ), TweenType.Tint )] public Color ColorTo { get; set; } = Color.White;

	// SHAKE
	[Property, Group( "Setup" ), ShowIf( nameof( IsShake ), true )] public float Strength { get; set; } = 1.0f;

	// SPIRAL
	[Property, Group( "Setup" ), ShowIf( nameof(IsCircularOrSpiral ), true )] public Vector3 Axis { get; set; } = Vector3.Up * 100f;

	[Property, Group( "Setup" ), ShowIf( nameof( IsCircularOrSpiral ), true )] public float Speed { get; set; } = 1f;

	[Property, Group( "Setup" ), ShowIf( nameof( IsCircularOrSpiral ), true )] public float Frequency { get; set; } = 5f;

	[Property, Group( "Setup" ), ShowIf( nameof( IsCircularOrSpiral ), true )] public float Range { get; set; } = 5f;

	// In Circle
	[Property, Group( "Setup" ), ShowIf( nameof( Type ), TweenType.InCircle )] public bool Snapping { get; set; } = false;
	

	// SETTINGS
	[Property, Group( "Settings" )] public float Duration { get; set; } = 1.0f;
	[Property, Group( "Settings" )] public float Delay { get; set; } = 0.0f;

	[Property, Group( "Settings" )]
	[Title( "Loops (0 = Once, -1 = Infinite)" )]
	public int Loops { get; set; } = 0;

	[Property, Group( "Setup" )] public LoopType LoopMode { get; set; } = LoopType.Restart;

	[Property, Group( "Settings" )] public EaseType Easing { get; set; } = EaseType.Linear;
	[Property, Group( "Settings" )] public bool AutoPlay { get; set; } = true;
	[Property, Group( "Settings" )] public string TweenID { get; set; } = "";

	[Property, Group( "Settings" )]
	public bool UseCurve { get; set; } = false;

	[Property, Group( "Settings" ), ShowIf( nameof( UseCurve ), true )]
	public Curve TweenCurve { get; set; } = Curve.Linear;


	// EVENTS
	[Property, Group( "Events" )] public Action _OnStart { get; set; }
	[Property, Group( "Events" )] public Action _OnComplete { get; set; }
	[Property, Group( "Debug" )] public bool GizmoDebug { get; set; } = true;


	// Editor stuff.
	[Hide] public bool IsCircularOrSpiral => Type == TweenType.InCircle || Type == TweenType.Spiral;
	[Hide] public bool IsShake => Type == TweenType.ShakeScale || Type == TweenType.ShakeLocation || Type == TweenType.ShakeRotation;
	[Hide] public bool IsVector => Type == TweenType.Move || Type == TweenType.MoveLocal || Type == TweenType.Rotate || Type == TweenType.RotateLocal || Type == TweenType.Scale;

	private BaseTween _currentTween;
	private TweenSnapshot _initialState;
	private int _completedLoops = 0;
	private bool _isReversing = false;
	private Vector3 _incrementalFrom;
	private Vector3 _incrementalTo;
	private bool _isIncrementalStarted = false;
	protected override void OnStart()
	{
		if ( Game.IsPlaying && AutoPlay )
		{
			Play();
		}
	}

	protected override void OnUpdate()
	{
		if ( Game.IsPlaying ) return;

		if ( _currentTween == null ) return;

		if ( !_currentTween.IsFinished && !_currentTween.IsPaused )
		{
			GameObject.Transform.Local = GameObject.Transform.Local;
		}
	}

	protected override void DrawGizmos()
	{
		if ( !GizmoDebug || !Gizmo.IsSelected ) return;
		if ( Type != TweenType.Move && Type != TweenType.MoveLocal ) return;

		using ( Gizmo.Scope() )
		{
			// GIZMO TRANSFORM.
			Gizmo.Transform = new Transform().WithPosition( 0 ).WithRotation( Rotation.Identity ).WithScale( 1 );

			float sphereRadius = 6f;

			Gizmo.Draw.Color = Color.Green;
			Gizmo.Draw.SolidSphere( From, sphereRadius );
			Gizmo.Draw.Text( "FROM", new Transform( From + Vector3.Up * 10f ) );

			Gizmo.Draw.Color = Color.Red;
			Gizmo.Draw.SolidSphere( To, sphereRadius );
			Gizmo.Draw.Text( "TO", new Transform( To + Vector3.Up * 10f ) );

			Gizmo.Draw.Color = Color.Yellow.WithAlpha( 0.4f );
			Gizmo.Draw.Line( From, To );

			var mid = (From + To) / 2f;
			var dir = (To - From).Normal;
			if ( dir.Length > 0.1f )
			{
				Gizmo.Draw.Arrow( mid, mid + dir * 15f, 10f, 5f );
			}
		}
	}

	public void Play()
	{
		StopInternal();

		if ( _initialState == null )
		{
			_initialState = new TweenSnapshot();
			_initialState.Capture( GameObject );
		}

		if ( LoopMode == LoopType.Incremental && !IsCircularOrSpiral )
		{
			if ( !_isIncrementalStarted )
			{
				_incrementalFrom = From;
				_incrementalTo = To;
				_isIncrementalStarted = true;
			}
		}
		else
		{
			_isIncrementalStarted = false;
		}

		if ( !IsCircularOrSpiral && LoopMode != LoopType.Incremental )
		{
			ResetToState( !_isReversing );
		}

		GameObject.Transform.ClearInterpolation();

		_currentTween = CreateTweenInstance( _isReversing );
		if ( _currentTween == null ) return;

		_currentTween
			.SetDelay( Delay )
			.SetEase( Easing )
			.OnStart( () => _OnStart?.Invoke() );

		if ( LoopMode == LoopType.Incremental && !IsCircularOrSpiral )
		{
			_currentTween.OnComplete( () => HandleCompletion() );
		}
		else
		{

			_currentTween.SetLoops( Loops, LoopMode );
			_currentTween.OnComplete( () => _OnComplete?.Invoke() );
		}

		_currentTween.Play();
	}

	public void Stop()
	{
		StopInternal();
		_completedLoops = 0;
		_isReversing = false;
		_isIncrementalStarted = false;

		if ( _initialState != null )
		{
			_initialState.Restore();
		}

		_initialState = null;
	}

	private void StopInternal()
	{
		_currentTween?.Stop();
		_currentTween = null;
	}

	private void HandleCompletion()
	{
		_OnComplete?.Invoke();

		bool hasMoreLoops = Loops == -1 || _completedLoops < Loops;

		switch ( LoopMode )
		{
			case LoopType.YoYo:
				_isReversing = !_isReversing;

				if ( !_isReversing && Loops != -1 )
					_completedLoops++;

				if ( hasMoreLoops || _isReversing )
				{
					Play();
				}
				break;

			case LoopType.Incremental:
				if ( hasMoreLoops )
				{
					if ( Loops != -1 ) _completedLoops++;
					if ( IsVector )
					{
						var delta = _incrementalTo - _incrementalFrom;
						_incrementalFrom = _incrementalTo;
						_incrementalTo += delta;
					}

					_isReversing = false;
					Play();
				}
				break;

			case LoopType.Restart:
				if ( hasMoreLoops )
				{
					if ( Loops != -1 ) _completedLoops++;
					_isReversing = false;
					Play();
				}
				break;
		}
	}

	private void ResetToState( bool useFrom )
	{
		if ( Type == TweenType.Tint )
		{
			var mr = Components.Get<ModelRenderer>();
			if ( mr.IsValid() ) mr.Tint = useFrom ? ColorFrom : ColorTo;
		}
		else
		{
			var val = useFrom ? From : To;
			switch ( Type )
			{
				case TweenType.Move: GameObject.WorldPosition = val; break;
				case TweenType.MoveLocal: GameObject.LocalPosition = val; break;
				case TweenType.Scale: GameObject.LocalScale = val; break;
				case TweenType.Rotate: GameObject.WorldRotation = Rotation.From( val.x, val.y, val.z ); break;
				case TweenType.RotateLocal: GameObject.LocalRotation = Rotation.From( val.x, val.y, val.z ); break;
			}
		}
	}

	private BaseTween CreateTweenInstance( bool reverse )
	{
		Vector3 startVal = (LoopMode == LoopType.Incremental && _isIncrementalStarted) ? _incrementalFrom : From;
		Vector3 endVal = (LoopMode == LoopType.Incremental && _isIncrementalStarted) ? _incrementalTo : To;
		Vector3 targetVal = reverse ? startVal : endVal;

		Color targetColor = reverse ? ColorFrom : ColorTo;
		float effectiveSpeed = reverse ? -Speed : Speed;

		var t = Type switch
		{
			TweenType.Move => GameObject.TweenMove( targetVal, Duration ),
			TweenType.MoveLocal => GameObject.TweenMoveLocal( targetVal, Duration ),
			TweenType.Rotate => GameObject.TweenRotate( Rotation.From( targetVal.x, targetVal.y, targetVal.z ), Duration ),
			TweenType.RotateLocal => GameObject.TweenRotateLocal( Rotation.From( targetVal.x, targetVal.y, targetVal.z ), Duration ),
			TweenType.Scale => GameObject.TweenScale( targetVal, Duration ),
			TweenType.Tint => Components.Get<ModelRenderer>()?.TweenTint( targetColor, Duration ),

			TweenType.ShakeLocation => GameObject.TweenShakeLocation( Duration, Strength ),
			TweenType.ShakeRotation => GameObject.TweenShakeRotation( Duration, Strength ),
			TweenType.ShakeScale => GameObject.TweenShakeScale( Duration, Strength ),

			TweenType.InCircle => GameObject.TweenInCircle( Duration, Axis, Range, Speed, Snapping ),
			TweenType.Spiral => GameObject.TweenSpiral( Duration, Axis, Speed, Frequency ),

			_ => null
		};

		if ( t == null ) return null;

		if ( UseCurve ) t.WithCurve( TweenCurve );
		else t.SetEase( Easing );

		t.Target = this.GameObject;
		t.SetDelay( Delay );
		t.LoopMode = LoopMode;

		if ( !string.IsNullOrEmpty( TweenID ) )
		{
			t.SetId( TweenID );
		}

		return t;
	}
}

public enum TweenType
{
	Move,
	MoveLocal,
	Rotate,
	RotateLocal,
	Scale,
	Tint,
	ShakeLocation,
	ShakeRotation, 
	ShakeScale,
	Spiral,
	InCircle
}