Animator/CrosshairAnimator.cs
using CrosshairMaker.Interfaces;
using Sandbox;
using Sandbox.Diagnostics;
using System;
using System.Collections.Generic;
using System.Linq;

using SandEase = Sandbox.Utility.Easing;
#nullable enable
namespace CrosshairMaker.Animator
{
	[Icon( "auto_awesome_motion" )]
	public sealed partial class CrosshairAnimator : Component
	{
		//static
		//-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-//
		private const string GroupAnim = "Animations";
		private const string GroupProperties = "Properties";
		private const string GroupCrossInfo = "Crosshair info";
		//-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-//
		//static

		//Events
		//-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-//
		public delegate void CrosshairAnimatorEvent(object sender);
		/// <summary>
		/// Event fired when animation starts
		/// </summary>
		public event CrosshairAnimatorEvent? OnAnimationStarting;
		/// <summary>
		/// Event fired when animation ends
		/// </summary>
		public event CrosshairAnimatorEvent? OnAnimationEnd;
		/// <summary>
		/// Event fired when animation starts to reset (when ResetAnimation is called)
		/// </summary>
		public event CrosshairAnimatorEvent? OnAnimationReseting;
		/// <summary>
		/// Event fired when animation is done reseting
		/// </summary>
		public event CrosshairAnimatorEvent? OnAnimationReset;
		/// <summary>
		/// Event fired when animation or PlaybackSpeed is changed before completing, the old Progress will still be used to complete the new animation
		/// </summary>
		public event CrosshairAnimatorEvent? OnAnimationInterupted;
		/// <summary>
		/// Event fired when the animation is interrupted and both Progress and Playbackspeed are reset
		/// </summary>
		public event CrosshairAnimatorEvent? OnAnimationCancelled;
		//-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-//
		//Events

		//Actions
		//-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-//
		/// <summary>
		/// Scans the current GameObject for a IAnimatableCrosshair component
		/// </summary>
		[Button( "Scan GO. for Crosshair", "my_location" ), Group( GroupCrossInfo )]
		public void ScanForCrosshair()
		{
			IEnumerable<IAnimatableCrosshair> crosshairs = this.GameObject.GetComponents<IAnimatableCrosshair>();
			int count = crosshairs.Count();
			if ( count == 0 )
			{
				Target = null;
				return;
			}
			_scanIndex = (_scanIndex + 1) % count;
			Target = crosshairs.ElementAt( _scanIndex );
		}
		private int _scanIndex = 0;
		/// <summary>
		/// Adds a new configurable animation to the crosshair
		/// </summary>
		[Button("Add CrosshairAnimation", "library_add" ),Group(GroupAnim)]
		[HideIf(nameof(Target),null)]
		private void AddAnimation() => AnimationIndex = AnimationData?.AddNewAnimation() ?? 0;
		[Button("Delete CrosshairAnimation","delete"),Group(GroupAnim)]
		[HideIf(nameof(_hideDelButton),true)]
		private void DelAnimation()
		{
			if ( AnimationData == null ) return;
			AnimationData.Remove( AnimationIndex );
			if ( AnimationData.Count == 0 ) AnimationIndex = 0;
			AnimationIndex = AnimationData.First().Key;
		}
		private bool _hideDelButton => (AnimationData == null) ? true : AnimationData.Count <= 1;
		[Button( "Play anim", "my_location" ), Group( GroupAnim )]
		[ShowIf( nameof( _gameIsRunning ), true )]
		public void PlayAnimation()
		{
			if ( ProgressMultiplier == 0 ) Log.Warning( "Progress cap is 0, this prevents the animation from playing" );
			else if ( float.IsNaN(ProgressMultiplier) ) Log.Warning( "Progress cap is NaN, this prevents the animation from playing" );
			if ( float.IsNaN( _progress ) ) _progress = 0;
			if ( _progress == 0 ) UpdateStartingProperties();
			if(AnimationDuration == 0) PlaybackSpeed = float.MaxValue;
			else PlaybackSpeed = 1 / AnimationDuration;
			OnAnimationStarting?.Invoke( this );
		}
		public void ResetAnimation()
		{
			if ( AnimationDuration == 0 ) PlaybackSpeed = float.MinValue;
			if ( float.IsNaN( _progress ) ) _progress = 1;
			PlaybackSpeed = 1 / - AnimationResetDuration;
			OnAnimationReseting?.Invoke( this );
		}
		public void CancelAnimation()
		{
			PlaybackSpeed = 0;
			_progress = 0;
			OnAnimationCancelled?.Invoke( this );
		}
		private bool _gameIsRunning => Game.IsPlaying;
		//-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-//
		//Actions

		//Properties
		//-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-//
		/// <summary>
		/// Current animation Index
		/// </summary>
		[Property, Group( GroupAnim )]
		public int AnimationIndex
		{
			get => _animationIndex;
			set
			{
				if ( Target == null )
				{
					_animationIndex = 0;
					return;
				}
				_animationIndex = value;
			}
		}
		private int _animationIndex = 0;
		/// <summary>
		/// Animation progress from 0 to 1
		/// </summary>
		[Property, Group( GroupAnim )]
		[HideIf(nameof(_gameIsRunning),false)]
		[ReadOnly]
		public float Progress
		{
			get
			{
				switch ( Easing )
				{
					default:
					case EasingMode.Linear: return _progress * ProgressMultiplier;
					case EasingMode.QuadOut: return SandEase.EaseOut( _progress ) * ProgressMultiplier;
					case EasingMode.QuadIn: return SandEase.EaseIn( _progress ) * ProgressMultiplier;
					case EasingMode.QuadInOut: return SandEase.EaseInOut( _progress ) * ProgressMultiplier;
					case EasingMode.BounceIn: return SandEase.BounceIn( _progress ) * ProgressMultiplier;
					case EasingMode.BounceOut: return SandEase.BounceOut( _progress ) * ProgressMultiplier;
					case EasingMode.BounceInOut: return SandEase.BounceInOut( _progress ) * ProgressMultiplier;
					case EasingMode.ExpIn: return SandEase.ExpoIn( _progress ) * ProgressMultiplier;
					case EasingMode.ExpOut: return SandEase.ExpoOut( _progress ) * ProgressMultiplier;
					case EasingMode.ExpInOut: return SandEase.ExpoInOut( _progress ) * ProgressMultiplier;
					case EasingMode.SinIn: return SandEase.SineEaseIn( _progress ) * ProgressMultiplier;
					case EasingMode.SinOut: return SandEase.SineEaseOut( _progress ) * ProgressMultiplier;
					case EasingMode.SinInOut: return SandEase.SineEaseInOut( _progress ) * ProgressMultiplier;
				}
			}
		}
		private float _progress = 0f;
		/// <summary>
		/// Sets the progress multiplier to change the animation amplitude, default is 1
		/// </summary>
		[Property, Group( GroupAnim )]
		[Range( 0f, 2f, 0.05f )]
		public float ProgressMultiplier
		{
			get => _progressCap;
			set => _progressCap = ( float.IsNegative(value) )? (value *= -1) : value;
		}
		private float _progressCap = 1f;
		/// <summary>
		/// Easing of animation, used to calculate Progress
		/// </summary>
		[Property, Group( GroupAnim )]
		public EasingMode Easing { get => _easing; set => _easing = value; }
		private EasingMode _easing = EasingMode.Linear;
		public enum EasingMode : byte
		{
			Linear,
			QuadIn,
			QuadOut,
			QuadInOut,
			BounceIn,
			BounceOut,
			BounceInOut,
			ExpIn,
			ExpOut,
			ExpInOut,
			SinIn,
			SinOut,
			SinInOut,
		}
		/// <summary>
		/// Duration in seconds for progress to go from 0 to 1 when PlayAnimation is called
		/// </summary>
		[Property, Group( GroupAnim )]
		public float AnimationDuration = 0.075f;
		/// <summary>
		/// Duration in seconds for the between the end of the animation and calling ResetAnimation, if set to -1 ResetAnimation is never called automatically
		/// </summary>
		[Property, Group( GroupAnim )]
		public float AnimationWait = 0.025f;
		/// <summary>
		/// Duration in seconds for progress to go from 1 to 0 when ResetAnimation is called
		/// </summary>
		[Property, Group( GroupAnim )]
		public float AnimationResetDuration = 0.5f;
		/// <summary>
		/// Current playback speed
		/// </summary>
		[Property, Group( GroupAnim )]
		[ReadOnly]
		[HideIf(nameof(_gameIsRunning),false)]
		public float PlaybackSpeed
		{
			get => _playbackSpeed;
			private set
			{
				if ( _playbackSpeed != 0 && _playbackSpeed != value )
				{
					OnAnimationInterupted?.Invoke( this );
				}
				_playbackSpeed = value;
			}
		}
		private float _playbackSpeed = 0;
		/// <summary>
		/// The crosshair animated by this component
		/// </summary>
		[Property, Group( GroupCrossInfo )]
		public IAnimatableCrosshair? Target 
		{
			get => _target; 
			set
			{
				if ( _target == value ) return;
				if( value == null )
				{
					if ( _target is Component chComp )
						chComp.OnComponentDestroy -= _OnDelCallback;
					_target = null;
					_animationData = null;
				}
				else
				{
					if ( _target is Component oldComp )
						oldComp.OnComponentDestroy -= _OnDelCallback;
					if ( value is Component newComp )
						newComp.OnComponentDestroy += _OnDelCallback;

					_target = value;
					_animationData = value.GetAnimationDictionary();
				}
				UpdateStartingProperties();
				
				void _OnDelCallback() => this.Target = null;
				
			} 
		}
		public IAnimatableCrosshair? _target = null;

		/// <summary>
		/// Target as string
		/// </summary>
		[Property, Group( GroupCrossInfo )]
		[ReadOnly]
		public string CurrentCrosshair => Target?.ToString() ?? "No crosshair found";

		/// <summary>
		/// Target id
		/// </summary>
		[Property, Group( GroupCrossInfo )]
		[ReadOnly]
		public string CurrentCrosshairID => (Target is not Component tc) ? "null" : tc.Id.ToString();
		/// <summary>
		/// AnimationDictionary
		/// </summary>
		[Property, Group( GroupCrossInfo ),Title("Animation data")]
		[HideIf(nameof(AnimationData),null)]
		[ReadOnly]
		public string _AnimationDataStr => AnimationData?.ToString() ?? "null";
		public AnimationDictionary? AnimationData => _animationData;
		public AnimationDictionary? _animationData = null;
		//-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-//
		//Properties

		protected override void OnUpdate()
		{
			base.OnUpdate();
			if(PlaybackSpeed != 0 )
			{
				_progress += Time.Delta * PlaybackSpeed;
				//Progress is between 0 and 1
				// (PlaybackSpeed = 1) => _progress reaches 1 in 1 second
				if ( _progress >= 1 )
				{
					_progress = 1;
					PlaybackSpeed = 0;
					OnAnimationEnd?.Invoke( this );
					PauseTimer = (AnimationWait,(AnimationWait >= 0)); //Activates PauseTimer, unless wait time in negative
				}
				if(_progress <= 0 )
				{
					_progress = 0;
					PlaybackSpeed = 0;
					OnAnimationReset?.Invoke( this );
				}
				UpdateTargetValues();
			}
			else if(PauseTimer.active && PauseTimer.timer) //If PauseTimer is complete
			{
				PauseTimer = (0,false);
				ResetAnimation();
			}
		}

		protected override void OnStart()
		{
			base.OnStart();
			ScanForCrosshair();
		}

		
		private void UpdateTargetValues()
		{
			if ( Target == null ) _StartingProperties = null;
			if ( _StartingProperties == null ) return;
			if ( AnimationData == null ) return;
			CrosshairAnimation current = AnimationData[AnimationIndex];
			AnimationValueSetter setters = AnimationData.ValueSetters!;

			for(int f = 0; f < setters.FloatSetters.Count; f++ )
			{
				float val = (current.Floats[f] * Progress) + _StartingProperties.Floats[f];
				Action<IAnimatableCrosshair, float> action = setters.FloatSetters[f];
				action.Invoke( Target! , val );
			}
			for(int i = 0; i < setters.IntSetters.Count; i++ )
			{
				int val = (int)((float)current.Ints[i] * Progress) + _StartingProperties.Ints[i];
				setters.IntSetters[i].Invoke( Target! , val );
			}
			for(int c = 0; c < setters.ColorSetters.Count; c++ )
			{
				InterpolatableColor currentColor = current.Colors[c];
				Color val = ColorInterpolator.Interpolate( _StartingProperties.Colors[c].Color, currentColor.Color, Progress, currentColor.Interp );
				setters.ColorSetters[c].Invoke( Target! , val );
			}
		}
		private void UpdateStartingProperties()
		{
			if(Target == null )
			{
				_StartingProperties = null;
				return;
			}
			if ( AnimationData == null ) return;
			CrosshairAnimation ca = CrosshairAnimation.Empty;
			AnimationValueSetter avs = AnimationData.ValueSetters!;
			for(int f= 0; f < avs.FloatGetters.Count; f++ )
			{
				ca.Floats.Add(avs.FloatGetters[f].Invoke(Target));
			}
			for(int i= 0; i < avs.IntGetters.Count; i++ )
			{
				ca.Ints.Add(avs.IntGetters[i].Invoke(Target));
			}
			for(int c= 0; c < avs.ColorGetters.Count; c++ )
			{
				ca.Colors.Add(new(avs.ColorGetters[c].Invoke(Target)));
			}
			
			_StartingProperties = ca;
		}
		private CrosshairAnimation? _StartingProperties;
		private (TimeUntil timer,bool active) PauseTimer = (0,false);

		
	}
}