Editor/MovieMaker/Editor/ToolbarWidget.cs
using System;
using System.Collections.Immutable;
using System.Globalization;
using System.Linq;
using Sandbox.UI;
using Sandbox.Utility;

namespace Editor.MovieMaker;

#nullable enable

public sealed class ToolBarWidget : Widget
{
	private readonly List<ToolBarGroup> _groups = new();

	public ToolBarWidget( MovieEditorPanel parent ) : base( parent )
	{
		Parent = parent;

		Layout = Layout.Row();
		Layout.Spacing = 4f;
		Layout.Margin = new Margin( 0f, 4f );

		VerticalSizeMode = SizeMode.CanShrink;
	}

	public ToolBarGroup AddGroup( bool permanent = false, bool alignRight = false )
	{
		var group = new ToolBarGroup( this ) { IsPermanent = permanent, AlignRight = alignRight };

		_groups.Add( group );

		UpdateLayout();

		return group;
	}

	internal void RemoveGroup( ToolBarGroup group )
	{
		_groups.Remove( group );

		UpdateLayout();
	}

	public void Reset()
	{
		var toRemove = _groups.Where( x => !x.IsPermanent ).ToArray();

		foreach ( var group in toRemove )
		{
			group.Destroy();
		}
	}

	private void UpdateLayout()
	{
		Layout.Clear( false );

		_groups.RemoveAll( x => !x.IsValid );

		foreach ( var group in _groups.Where( x => !x.AlignRight ) )
		{
			Layout.Add( group );
		}

		if ( !_groups.Any( x => x.AlignRight ) ) return;

		Layout.AddStretchCell();

		foreach ( var group in _groups.Where( x => x.AlignRight ) )
		{
			Layout.Add( group );
		}
	}

	protected override void OnPaint()
	{
		Paint.SetBrushAndPen( Theme.TabBackground );
		Paint.DrawRect( LocalRect );
	}
}

public sealed record ToolBarItemDisplay( string Title, string Icon, string? Description, bool Background = true );

public sealed class ToolBarGroup : Widget
{
	public bool IsPermanent { get; init; }
	public bool AlignRight { get; init; }

	public ToolBarGroup( ToolBarWidget parent )
		: base( parent )
	{
		HorizontalSizeMode = SizeMode.CanGrow;

		Layout = Layout.Row();
		Layout.Spacing = 2f;
		Layout.Margin = 4f;
	}

	public override void OnDestroyed()
	{
		if ( !IsValid ) return;

		if ( Parent is ToolBarWidget { IsValid: true } toolbar )
		{
			toolbar.RemoveGroup( this );
		}
	}

	protected override void OnPaint()
	{
		Paint.ClearPen();
		Paint.SetBrush( Theme.SidebarBackground );
		Paint.DrawRect( LocalRect, 3f );
	}

	public Label AddLabel( string text )
	{
		var label = new Label( null )
		{
			Color = Color.White.Darken( 0.5f ),
			Margin = 4f,
			Alignment = TextFlag.Left,
			Text = text
		};

		Layout.Add( label );

		return label;
	}

	public IconButton AddAction( ToolBarItemDisplay display, Action action, Func<bool>? enabled = null )
	{
		var btn = new IconButton( display.Icon )
		{
			ToolTip = $"<h3>{display.Title}</h3>{display.Description}",
			IconSize = 16
		};

		if ( !display.Background )
		{
			btn.Background = Color.Transparent;
			btn.BackgroundActive = Color.Transparent;
			btn.ForegroundActive = Theme.Primary;
		}

		btn.OnClick += action;

		if ( enabled != null )
		{
			btn.Bind( nameof( IconButton.Enabled ) )
				.ReadOnly()
				.From( enabled, (Action<bool>?)null );
		}

		Layout.Add( btn );

		return btn;
	}

	public IconButton AddToggle( ToolBarItemDisplay display, Func<bool> getState, Action<bool> setState )
	{
		var btn = new IconButton( display.Icon )
		{
			ToolTip = $"<h3>{display.Title}</h3>{display.Description}",
			IconSize = 16,
			IsToggle = true
		};

		if ( !display.Background )
		{
			btn.Background = Color.Transparent;
			btn.BackgroundActive = Color.Transparent;
			btn.ForegroundActive = Theme.Primary;
		}

		btn.Bind( "IsActive" ).From( getState, setState );

		Layout.Add( btn );

		return btn;
	}

	public FunctionSelector<InterpolationMode> AddInterpolationSelector( Func<InterpolationMode> getValue, Action<InterpolationMode> setValue )
	{
		var selector = new FunctionSelector<InterpolationMode>( "Interpolation Mode", mode => t => mode.Apply( t ) );

		selector.Bind( "Value" ).From( getValue, setValue );

		Layout.Add( selector );

		return selector;
	}

	private static ImmutableArray<float> ExampleValues { get; } =
	[
		-0.5f,
		0f,
		0.67f,
		0.33f,
		1f,
		1.5f
	];

	private static float CubicInterpolationExample( float t )
	{
		const int margin = 1;

		var segments = ExampleValues.Length - 1;
		
		t *= segments - margin * 2;

		var index = (int)MathF.Floor( t ) + margin;

		var i1 = Math.Clamp( index, 0, segments );
		var i2 = Math.Clamp( index + 1, 0, segments );

		t -= i1 - margin;

		t = Math.Clamp( t, 0f, 1f );

		var v1 = ExampleValues[i1];
		var v2 = ExampleValues[i2];

		if ( t <= 0f ) return v1;
		if ( t >= 1f ) return v2;

		var i0 = Math.Clamp( index - 1, 0, segments );
		var i3 = Math.Clamp( index + 2, 0, segments );

		var v0 = ExampleValues[i0];
		var v3 = ExampleValues[i3];

		var t0 = (v2 - v0) * 0.5f;
		var t1 = (v3 - v1) * 0.5f;

		var c0 = v1 + t0 / 3f;
		var c1 = v2 - t1 / 3f;

		var a0 = MathX.Lerp( v1, c0, t );
		var a1 = MathX.Lerp( c0, c1, t );
		var a2 = MathX.Lerp( c1, v2, t );

		var b0 = MathX.Lerp( a0, a1, t );
		var b1 = MathX.Lerp( a1, a2, t );

		return MathX.Lerp( b0, b1, t );
	}

	public static Func<float, float>? GetInterpolationFunc( KeyframeInterpolation interpolation )
	{
		return interpolation switch
		{
			KeyframeInterpolation.Linear => Easing.Linear,
			KeyframeInterpolation.Quadratic => Easing.QuadraticInOut,
			KeyframeInterpolation.Cubic => CubicInterpolationExample,
			_ => null
		};
	}

	public FunctionSelector<KeyframeInterpolation> AddInterpolationSelector( Func<KeyframeInterpolation> getValue, Action<KeyframeInterpolation> setValue )
	{
		var selector = new FunctionSelector<KeyframeInterpolation>( "Keyframe Interpolation", GetInterpolationFunc );

		selector.Bind( "Value" ).From( getValue, setValue );

		Layout.Add( selector );

		return selector;
	}

	public (FloatSlider Slider, Label Label) AddSlider( string title, Func<float> getValue, Action<float> setValue, float minimum = 0f,
		float maximum = 1f, float step = 0.01f, Func<string>? getLabel = null )
	{
		var slider = new FloatSlider( null )
		{
			ToolTip = title,
			FixedWidth = 80f,
			Minimum = minimum,
			Maximum = maximum,
			Step = step
		};

		slider.Bind( nameof( FloatSlider.Value ) )
			.From( getValue, setValue );

		Layout.Add( slider );

		var label = new Label( null )
		{
			Color = Color.White.Darken( 0.5f ),
			Margin = 4f,
			Alignment = TextFlag.Left
		};

		label.Bind( nameof( Label.Text ) )
			.ReadOnly()
			.From( getLabel ?? (() => slider.Value.ToString( CultureInfo.InvariantCulture )), (Action<string>?)null );

		Layout.Add( label );

		return (slider, label);
	}

	public void AddSpacingCell() => Layout.AddSpacingCell( 8f );
}