Editor/WeaponImporter/Tool/UI/Steps/StepPanel.cs
using Editor;
using Sandbox;

namespace WeaponImporter.Tool.UI;

/// <summary>
/// One step of the workflow in the left column. Steps build their cards from the session and
/// rebuild only when their structure changes (a new weapon, a different part list...); value
/// changes just run the registered refreshers, so a slider being dragged is never destroyed.
/// Rebuilds are deferred to the next frame so a widget is never deleted inside its own callback.
/// </summary>
public abstract class StepPanel : Widget
{
	protected readonly ImporterController C;
	private readonly List<Action> _refreshers = new();
	private string _builtKey;
	private bool _dirty = true;

	protected StepPanel( Widget parent, ImporterController controller ) : base( parent )
	{
		C = controller;
		Layout = Layout.Column();
		Layout.Spacing = 10;
		controller.Changed += MarkDirty;
		controller.SessionReplaced += ForceRebuild;
	}

	public abstract string Title { get; }

	/// <summary>Anything that, when different, needs new widgets rather than new values.</summary>
	protected abstract string StructureKey();

	protected abstract void Build();

	public void MarkDirty() => _dirty = true;

	public void ForceRebuild()
	{
		_builtKey = null;
		_dirty = true;
	}

	/// <summary>Called every frame by the window while this step is visible.</summary>
	public void Flush()
	{
		if ( !_dirty )
			return;
		_dirty = false;
		string key;
		try
		{
			key = C.Session?.Setup is null ? "" : StructureKey();
		}
		catch ( Exception )
		{
			key = Guid.NewGuid().ToString();
		}
		if ( key != _builtKey )
		{
			_builtKey = key;
			Rebuild();
			return;
		}
		foreach ( var refresh in _refreshers.ToArray() )
		{
			try
			{
				refresh();
			}
			catch ( Exception e )
			{
				Log.Warning( $"[weapon importer] refresh failed: {e.Message}" );
			}
		}
	}

	private void Rebuild()
	{
		Layout.Clear( true );
		_refreshers.Clear();
		if ( C.Session?.Setup is null )
			return;
		try
		{
			Build();
		}
		catch ( Exception e )
		{
			Log.Warning( $"[weapon importer] {Title} panel failed: {e}" );
			Layout.Add( UiStyle.Colored( new Label( $"This panel could not be shown: {e.Message}", this ) { WordWrap = true }, Theme.Red ) );
		}
		Layout.AddStretchCell();
	}

	/// <summary>Registers a refresher and runs it once now.</summary>
	protected void Bind( Action refresh )
	{
		_refreshers.Add( refresh );
		refresh();
	}

	protected Card AddCard( string icon, string title, out Layout header, string tooltip = null )
	{
		var card = Layout.Add( new Card( this ) );
		header = card.Header( icon, title, tooltip );
		return card;
	}

	/// <summary>A labelled slider with a live value readout.</summary>
	protected FloatSlider Slider( Widget owner, Layout parent, string caption, string tooltip, float min, float max, float value, Func<float, string> format, Action<float> edited, float step = 0f )
	{
		var row = UiStyle.FieldRow( owner, parent, caption, tooltip );
		var slider = row.Add( new FloatSlider( owner ) { Minimum = min, Maximum = max, Value = value, ToolTip = tooltip, FixedHeight = UiStyle.ControlHeight }, 1 );
		if ( step > 0 )
			slider.Step = step;
		var readout = row.Add( UiStyle.Muted( new Label( format( value ), owner ) { FixedWidth = UiStyle.PillColumn, FixedHeight = UiStyle.ControlHeight, Alignment = TextFlag.RightCenter } ) );
		slider.OnValueEdited = () =>
		{
			readout.Text = format( slider.Value );
			try
			{
				edited( slider.Value );
			}
			catch ( Exception e )
			{
				C.SetStatus( e.Message, Theme.Red );
			}
		};
		return slider;
	}

	/// <summary>A framed dropdown (framed like mocap inputs).</summary>
	protected ComboBox Combo( Widget owner, Layout row, string tooltip, float width = 0 )
	{
		var combo = UiStyle.Framed( new ComboBox( owner ) { ToolTip = tooltip } );
		if ( width > 0 )
			combo.FixedWidth = width;
		else
			combo.MinimumWidth = 60; // long bone/clip names must not widen the column
		combo.MaxVisibleItems = 24;
		row.Add( combo, width > 0 ? 0 : 1 );
		return combo;
	}

	/// <summary>Runs a UI handler, turning exceptions into a red status line.</summary>
	protected void Safe( Action action )
	{
		try
		{
			action();
		}
		catch ( Exception e )
		{
			Log.Warning( $"[weapon importer] {e}" );
			C.SetStatus( e.Message, Theme.Red );
		}
	}

	public override void OnDestroyed()
	{
		C.Changed -= MarkDirty;
		C.SessionReplaced -= ForceRebuild;
		base.OnDestroyed();
	}
}