Extensions/PanelExtensions.cs
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace BetterUI.Extensions;

/// <summary>
/// Extensions for <see cref="Panel"/>.
/// </summary>
public static class PanelExtensions
{
	/// <summary>
	/// Adds a CSS variable to the panel.
	/// </summary>
	/// <param name="panel">The panel.</param>
	/// <param name="name">The name of the CSS variable.</param>
	/// <param name="value">The value of the CSS variable.</param>
	/// <returns>The panel.</returns>
	public static void AddCssVariable( this Panel panel, string name, string value )
	{
		if ( string.IsNullOrWhiteSpace( name ) )
			throw new ArgumentException( "CSS variable name cannot be null or empty.", nameof(name) );

		var styleSheet = new StyleSheet { Variables = new Dictionary<string, string> { { name, value } } };
		panel.StyleSheet.Add( styleSheet );
	}

	/// <summary>
	/// Adds a CSS variable to the panel if the condition is true.
	/// </summary>
	/// <param name="panel">The panel.</param>
	/// <param name="name">The name of the CSS variable.</param>
	/// <param name="value">The value of the CSS variable.</param>
	/// <param name="condition">The condition to check.</param>
	/// <returns>The panel.</returns>
	public static void BindCssVariable( this Panel panel, string name, string value, Func<bool> condition )
	{
		if ( string.IsNullOrWhiteSpace( name ) )
			throw new ArgumentException( "CSS class name cannot be null or empty.", nameof(name) );

		if ( !condition() ) return;
		panel.AddCssVariable( name, value );
	}

	/// <summary>
	/// Binds a CSS class to the panel when the task completes successfully.
	/// </summary>
	/// <param name="panel">The panel.</param>
	/// <param name="name">The name of the CSS class.</param>
	/// <param name="task">The task.</param>
	/// <returns>The panel.</returns>
	public static Task AddClassOnTaskCompletion( this Panel panel, string name, Task task )
	{
		if ( string.IsNullOrWhiteSpace( name ) )
			throw new ArgumentException( "CSS class name cannot be null or empty.", nameof(name) );

		return panel.Task.WhenAll( task )
			.ContinueWith( _ => panel.BindClass( name, () => task.IsCompletedSuccessfully ) );
	}

	/// <summary>
	/// Binds a CSS class to the panel when the task completes successfully, and unbinds it when the task completes.
	/// </summary>
	/// <param name="panel">The panel.</param>
	/// <param name="name">The name of the CSS class.</param>
	/// <param name="task">The task.</param>
	/// <returns>The panel.</returns>
	public static Task RemoveClassOnTaskCompletion( this Panel panel, string name, Task task )
	{
		if ( string.IsNullOrWhiteSpace( name ) )
			throw new ArgumentException( "CSS class name cannot be null or empty.", nameof(name) );

		return panel.Task.WhenAll( task )
			.ContinueWith( _ => panel.BindClass( name, () => !task.IsCompletedSuccessfully ) );
	}

	/// <summary>
	/// Switches the panel's CSS class between two classes based on the task's completion status.
	/// </summary>
	/// <param name="panel">The panel.</param>
	/// <param name="runningClass">The CSS class to apply while the task is running.</param>
	/// <param name="task">The task.</param>
	/// <param name="completedClass">The CSS class to apply when the task is completed. Defaults to <c>null</c>. If <c>null</c>, the class will be removed.</param>
	/// <returns>The panel.</returns>
	public static Task SwitchClassOnTaskCompletion( this Panel panel, string runningClass, Task task,
		string? completedClass = null )
	{
		if ( string.IsNullOrWhiteSpace( runningClass ) )
			throw new ArgumentException( "CSS class name cannot be null or empty.", nameof(runningClass) );

		if ( string.IsNullOrWhiteSpace( completedClass ) )
			throw new ArgumentException( "CSS class name cannot be null or empty.", nameof(completedClass) );

		panel.AddClass( runningClass );

		return panel.Task.WhenAll( task ).ContinueWith( _ =>
		{
			panel.RemoveClass( runningClass );
			panel.BindClass( completedClass, () => task.IsCompletedSuccessfully );
		} );
	}

	/// <summary>
	/// Adds a CSS class to the panel after a specified delay.
	/// </summary>
	/// <param name="panel">The panel.</param>
	/// <param name="name">The name of the CSS class.</param>
	/// <param name="delay">The delay in milliseconds.</param>
	/// <returns>A task representing the asynchronous operation.</returns>
	public static Task AddClassAfterDelay( this Panel panel, string name, int delay )
	{
		if ( string.IsNullOrWhiteSpace( name ) )
			throw new ArgumentException( "CSS class name cannot be null or empty.", nameof(name) );

		return panel.Task.RunInThreadAsync( async () =>
		{
			await Task.Delay( delay );
			panel.AddClass( name );
		} );
	}

	/// <summary>
	/// Removes a CSS class from the panel after a specified delay.
	/// </summary>
	/// <param name="panel">The panel.</param>
	/// <param name="name">The name of the CSS class.</param>
	/// <param name="delay">The delay in milliseconds.</param>
	/// <returns>A task representing the asynchronous operation.</returns>
	public static Task RemoveClassAfterDelay( this Panel panel, string name, int delay )
	{
		if ( string.IsNullOrWhiteSpace( name ) )
			throw new ArgumentException( "CSS class name cannot be null or empty.", nameof(name) );

		return panel.Task.RunInThreadAsync( async () =>
		{
			await Task.Delay( delay );
			panel.RemoveClass( name );
		} );
	}

	/// <summary>
	/// Cycles the panel's CSS classes between the classes provided.
	/// </summary>
	/// <param name="panel">The panel.</param>
	/// <param name="interval">The interval between each class cycle.</param>
	/// <param name="delay">The delay before the first cycle.</param>
	/// <param name="classList">The list of CSS classes to cycle through.</param>
	public static Task CycleClasses( this Panel panel, int interval, int delay = 0, params string[] classList )
	{
		return panel.Task.RunInThreadAsync( async () =>
		{
			while ( panel is { IsValid: true, DeletionToken.IsCancellationRequested: false } )
			{
				for ( var i = 0; i < classList.Length; i++ )
				{
					var previous = i > 0 ? classList[i - 1] : classList[^1];
					var next = classList[i];

					await Task.Delay( interval, panel.DeletionToken );

					panel.RemoveClass( previous );
					panel.AddClass( next );

					await Task.Delay( delay );
				}
			}
		} );
	}

	/// <summary>
	/// Flashes a CSS class on the panel for a specified duration.
	/// </summary>
	/// <param name="panel">The panel.</param>
	/// <param name="className">The name of the CSS class to flash.</param>
	/// <param name="duration">The duration of the flash in milliseconds.</param>
	public static void FlashClass( this Panel panel, string className, int duration )
	{
		panel.Task.RunInThreadAsync( async () =>
		{
			panel.AddClass( className );
			await Task.Delay( duration, panel.DeletionToken );
			panel.RemoveClass( className );
		} );
	}

	/// <summary>
	/// Flashes a CSS class on the panel for a specified duration if a condition is true.
	/// </summary>
	/// <param name="panel">The panel.</param>
	/// <param name="className">The name of the CSS class to flash.</param>
	/// <param name="duration">The duration of the flash in milliseconds.</param>
	/// <param name="condition">The condition to check before flashing the class.</param>
	public static void FlashClass( this Panel panel, string className, int duration, Func<bool> condition )
	{
		if ( !condition() ) return;
		panel.FlashClass( className, duration );
	}

	/// <summary>
	/// Gets a dictionary of user data associated with the panel.
	/// </summary>
	/// <returns>The user data dictionary.</returns>
	private static Dictionary<string, object> GetBag( this Panel panel )
	{
		if ( panel.UserData is Dictionary<string, object> bag ) return bag;

		bag = new Dictionary<string, object>();
		panel.UserData = bag;

		return bag;
	}

	/// <summary>
	/// Gets a value from the panel's user data dictionary.
	/// </summary>
	/// <typeparam name="T">The type of the value to retrieve.</typeparam>
	/// <param name="panel">The panel.</param>
	/// <param name="name">The key to retrieve.</param>
	/// <returns>The value associated with the key, or <c>null</c> if the key does not exist.</returns>
	public static T? GetBag<T>( this Panel panel, string name ) where T : class
	{
		if ( panel.UserData is Dictionary<string, object> bag )
			return (T?)bag[name];

		bag = new Dictionary<string, object>();
		panel.UserData = bag;

		return null;
	}

	/// <summary>
	/// Sets the value of a key in the panel's user data dictionary.
	/// </summary>
	/// <param name="panel">The panel.</param>
	/// <param name="name">The key to set.</param>
	/// <param name="value">The value to set.</param>
	public static void SetBag( this Panel panel, string name, object value )
	{
		panel.GetBag()[name] = value;
	}

	/// <summary>
	/// Sets a state on the panel, which can be used to change styles
	/// or behaviors. If the state already exists, it is removed.
	/// </summary>
	/// <param name="panel">The panel.</param>
	/// <param name="className">The name of the state.</param>
	public static void SetState( this Panel panel, string className )
	{
		if ( string.IsNullOrWhiteSpace( className ) )
			throw new ArgumentException( "State name cannot be null or empty.", nameof(className) );

		if ( panel.GetBag<string>( "state" ) is var state )
			panel.RemoveClass( state );

		panel.GetBag()["state"] = className;
		panel.AddClass( className );
	}

	/// <summary>
	/// Sets a state on the panel, which can be used to change styles
	/// or behaviors. If the state already exists, it is removed.
	/// </summary>
	/// <param name="panel">The panel.</param>
	/// <param name="className">The name of the state.</param>
	/// <param name="condition">A condition that determines if the state should be set.</param>
	public static void SetState( this Panel panel, string className, Func<bool> condition )
	{
		if ( !condition() ) return;
		panel.SetState( className );
	}

	/// <summary>
	/// Hides the panel by adding the "hidden" CSS class.
	/// </summary>
	/// <param name="panel">The panel.</param>
	/// <param name="delay">The delay in milliseconds.</param>
	public static Task Hide( this Panel panel, int delay = 0 )
	{
		return panel.Task.RunInThreadAsync( async () =>
		{
			await Task.Delay( delay );
			panel.AddClass( "hidden" );
		} );
	}

	/// <summary>
	/// Sets the display mode of the panel after a specified delay.
	/// </summary>
	/// <param name="panel">The panel.</param>
	/// <param name="displayMode">The display mode.</param>
	/// <param name="delay">The delay in milliseconds.</param>
	public static Task Display( this Panel panel, DisplayMode displayMode, int delay = 0 )
	{
		return GameTask.RunInThreadAsync( async () =>
		{
			await Task.Delay( delay );
			panel.Style.Display = displayMode;
		} );
	}

	/// <summary>
	/// Shows the panel by removing the "hidden" CSS class.
	/// </summary>
	/// <param name="panel">The panel.</param>
	/// <param name="delay">The delay in milliseconds.</param>
	public static Task Show( this Panel panel, int delay = 0 )
	{
		return panel.Task.RunInThreadAsync( async () =>
		{
			panel.RemoveClass( "hidden" );
			await Task.Delay( delay );
		} );
	}

	/// <summary>
	/// Shows the panel if the condition is true, hides it otherwise.
	/// </summary>
	/// <param name="panel">The panel.</param>
	/// <param name="condition">The condition to check.</param>
	public static void ShowIf( this Panel panel, Func<bool> condition )
	{
		if ( condition() ) panel.Show();
		else panel.Hide();
	}

	/// <summary>
	/// Hides the panel if the condition is true, shows it otherwise.
	/// </summary>
	/// <param name="panel">The panel.</param>
	/// <param name="condition">The condition to check.</param>
	public static void HideIf( this Panel panel, Func<bool> condition )
	{
		if ( condition() ) panel.Hide();
		else panel.Show();
	}

	/// <summary>
	/// Checks if the panel is visible.
	/// </summary>
	/// <param name="panel">The panel.</param>
	/// <returns>True if the panel is visible, false otherwise.</returns>
	public static bool IsVisible( this Panel panel ) => !panel.IsHidden();

	/// <summary>
	/// Checks if the panel is hidden.
	/// </summary>
	/// <param name="panel">The panel.</param>
	/// <returns>True if the panel is hidden, false otherwise.</returns>
	public static bool IsHidden( this Panel panel ) => panel.HasClass( "hidden" );

	/// <summary>
	/// Binds the visibility of the panel to a condition.
	/// </summary>
	/// <param name="panel">The panel.</param>
	/// <param name="condition">The condition to check.</param>
	public static void BindVisibility( this Panel panel, Func<bool> condition ) =>
		panel.BindClass( "hidden", () => !condition() );
}