Editor/WeaponImporter/Tool/UI/Widgets/SearchPicker.cs
using Editor;
using Sandbox;

namespace WeaponImporter.Tool.UI;

/// <summary>One choice in a <see cref="SearchPicker"/>.</summary>
public sealed record PickerItem( string Value, string Label, string Detail = "", bool Suggested = false, bool Current = false );

/// <summary>
/// A popup list with a search box: type to filter (every word must match, anywhere in the
/// name), Enter picks the first match. Suggested items come first. Used for choosing clips.
/// </summary>
public sealed class SearchPicker : PopupWidget
{
	private readonly IReadOnlyList<PickerItem> _items;
	private readonly Action<string> _picked;
	private readonly LineEdit _search;
	private readonly Widget _list;
	private List<PickerItem> _shown = new();

	public SearchPicker( Widget parent, string title, IReadOnlyList<PickerItem> items, Action<string> picked ) : base( parent )
	{
		_items = items;
		_picked = picked;
		FixedWidth = 380;
		Layout = Layout.Column();
		Layout.Margin = 8;
		Layout.Spacing = 6;
		Layout.Add( UiStyle.Bold( new Label( title, this ) ) );
		_search = Layout.Add( new LineEdit( this ) { PlaceholderText = "Type to search…", FixedHeight = UiStyle.ControlHeight } );
		_search.TextEdited += _ => Fill();
		_search.ReturnPressed += () =>
		{
			if ( _shown.Count > 0 )
				Pick( _shown[0].Value );
		};
		var scroll = Layout.Add( new ScrollArea( this ), 1 );
		scroll.HorizontalScrollbarMode = ScrollbarMode.Off;
		scroll.MaximumHeight = 420;
		scroll.MinimumHeight = Math.Min( 420, 30 * (items.Count + 1) );
		_list = new Widget( scroll ) { Layout = Layout.Column() };
		_list.Layout.Spacing = 1;
		scroll.Canvas = _list;
		Fill();
	}

	/// <summary>Opens at the cursor with the search box focused.</summary>
	public void Open()
	{
		OpenAtCursor();
		_search.Focus();
	}

	private void Fill()
	{
		_list.Layout.Clear( true );
		_shown = Filter( _items, _search.Text );
		foreach ( var item in _shown )
		{
			var value = item.Value;
			var row = _list.Layout.Add( new ClickRow( _list, () => Pick( value ), item.Detail ) );
			row.Selected = item.Current;
			row.Layout.Add( new IconButton( item.Current ? "check" : item.Suggested ? "auto_awesome" : "movie" ) { Background = Color.Transparent, FixedSize = 22, TransparentForMouseEvents = true } );
			row.Layout.Add( new Label( UiStyle.Breakable( item.Label ), row ) { WordWrap = true, MinimumWidth = 20 }, 1 );
			if ( item.Detail.Length > 0 )
				row.Layout.Add( UiStyle.Muted( new Label( item.Detail, row ), small: true ) );
		}
		if ( _shown.Count == 0 )
			_list.Layout.Add( UiStyle.Muted( new Label( "Nothing matches.", _list ), small: true ) );
		_list.Layout.AddStretchCell();
	}

	/// <summary>Items matching a query (every word, anywhere in the name or detail), suggested first.</summary>
	public static List<PickerItem> Filter( IEnumerable<PickerItem> items, string query )
	{
		var words = (query ?? "").ToLowerInvariant().Split( new[] { ' ', '_', '-', '|' }, StringSplitOptions.RemoveEmptyEntries );
		return items
			.Where( i => words.All( w => i.Label.Contains( w, StringComparison.OrdinalIgnoreCase ) || i.Detail.Contains( w, StringComparison.OrdinalIgnoreCase ) ) )
			.OrderByDescending( i => i.Suggested )
			.ToList();
	}

	private void Pick( string value )
	{
		_picked( value );
		Close();
	}
}