Editor/AssetBrowserAddon/AssetTypeSelector.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Editor;
using Sandbox;

namespace Sandbox.AssetBrowserAddon;

/// <summary>
/// Searchable list of asset types that supports multi-selection with icons.
/// </summary>
internal sealed class AssetTypeSelector : Widget
{
    private readonly LineEdit _searchInput;
    private readonly ScrollArea _scrollArea;
    private readonly Widget _list;
    private readonly Action<LineEdit> _styleInput;

    private readonly List<OptionData> _options = new();
    private readonly List<AssetTypeRow> _rows = new();
    private readonly HashSet<string> _selected = new(StringComparer.OrdinalIgnoreCase);

    public event Action<IReadOnlyCollection<string>> SelectionChanged;

    public AssetTypeSelector(Widget parent, Action<LineEdit> styleInput) : base(parent)
    {
        _styleInput = styleInput;

        Layout = Layout.Column();
        Layout.Spacing = 6f;

        _searchInput = Layout.Add(new LineEdit(this));
        _searchInput.MinimumHeight = Theme.RowHeight;
        _searchInput.PlaceholderText = "Search asset types...";
        _styleInput?.Invoke(_searchInput);
        _searchInput.TextEdited += _ => RefreshList();

        _scrollArea = Layout.Add(new ScrollArea(this));
        _scrollArea.MinimumHeight = 220f;

        _list = new Widget(_scrollArea);
        _scrollArea.Canvas = _list;
        _list.Layout = Layout.Column();
        _list.Layout.Spacing = 2f;
        _list.VerticalSizeMode = SizeMode.CanShrink;

        BuildOptions();
        RefreshList();
    }

    public IReadOnlyCollection<string> SelectedTags => _selected;

    public void SetSelected(IEnumerable<string> tags)
    {
        _selected.Clear();

        if (tags != null)
        {
            foreach (var tag in tags)
            {
                var normalized = NormalizeExtension(tag);
                if (!string.IsNullOrWhiteSpace(normalized))
                    _selected.Add(normalized);
            }
        }

        RefreshList();
    }

    private void BuildOptions()
    {
        _options.Clear();

        foreach (var assetType in AssetType.All.OrderBy(x => x.FriendlyName ?? x.FileExtension))
        {
            var extensions = (assetType.FileExtensions?.Count ?? 0) > 0
                ? assetType.FileExtensions
                : new[] { assetType.FileExtension };

            foreach (var extension in extensions ?? Array.Empty<string>())
            {
                var normalized = NormalizeExtension(extension);
                if (string.IsNullOrWhiteSpace(normalized))
                    continue;

                if (_options.Any(x => x.Tag.Equals(normalized, StringComparison.OrdinalIgnoreCase)))
                    continue;

                var title = assetType.FriendlyName ?? normalized.ToUpperInvariant();
                var subtitle = string.IsNullOrWhiteSpace(extension) ? null : $".{normalized}";

                _options.Add(new OptionData(normalized, title, subtitle, assetType.Icon16));
            }
        }
    }

    private void RefreshList()
    {
        var filter = _searchInput.Text?.Trim() ?? string.Empty;

        _list.DestroyChildren();
        _rows.Clear();

        IEnumerable<OptionData> data = _options;
        if (!string.IsNullOrWhiteSpace(filter))
        {
            data = data.Where(option =>
                Contains(option.Tag, filter) ||
                Contains(option.Title, filter) ||
                Contains(option.Subtitle, filter));
        }

        foreach (var option in data)
        {
            var row = new AssetTypeRow(_list, option, _selected.Contains(option.Tag), ToggleSelection);
            _list.Layout.Add(row);
            _rows.Add(row);
        }

        if (!_rows.Any())
        {
            var empty = _list.Layout.Add(new Label(_list)
            {
                Text = "No asset types match your search.",
                Alignment = TextFlag.Center
            });
            empty.SetStyles($"color: {Theme.Text.WithAlpha(0.6f).Hex}; padding: 12px;");
        }
    }

    private void ToggleSelection(string tag, bool isSelected)
    {
        if (isSelected)
            _selected.Add(tag);
        else
            _selected.Remove(tag);

        SelectionChanged?.Invoke(_selected);
    }

    private static bool Contains(string value, string filter)
    {
        if (string.IsNullOrWhiteSpace(value))
            return false;

        return value.IndexOf(filter, StringComparison.OrdinalIgnoreCase) >= 0;
    }

    private static string NormalizeExtension(string value)
    {
        if (string.IsNullOrWhiteSpace(value))
            return string.Empty;

        return value.Trim().TrimStart('.').ToLowerInvariant();
    }

    private sealed record OptionData(string Tag, string Title, string Subtitle, Pixmap Icon);

    private sealed class AssetTypeRow : Widget
    {
        private readonly OptionData _option;
        private readonly Action<string, bool> _onToggle;
        private bool _isSelected;

        public AssetTypeRow(Widget parent, OptionData option, bool selected, Action<string, bool> onToggle) : base(parent)
        {
            _option = option;
            _onToggle = onToggle;
            _isSelected = selected;
            MinimumHeight = 26f;
            MaximumHeight = 26f;
            MinimumWidth = 50f;
            MaximumWidth = 10000f;
            VerticalSizeMode = SizeMode.CanShrink;
            HorizontalSizeMode = SizeMode.CanGrow;
        }

        protected override void OnMousePress(MouseEvent e)
        {
            if ( e.LeftMouseButton )
            {
                Toggle();
                e.Accepted = true;
            }

            base.OnMousePress( e );
        }

        private void Toggle()
        {
            _isSelected = !_isSelected;
            _onToggle?.Invoke( _option.Tag, _isSelected );
            Update();
        }

        protected override void OnPaint()
        {
            var rect = LocalRect;

            if ( Paint.HasMouseOver || _isSelected )
            {
                var color = _isSelected ? Theme.Primary.WithAlpha( 0.15f ) : Theme.ControlBackground.WithAlpha( 0.5f );
                Paint.SetBrush( color );
                Paint.ClearPen();
                Paint.DrawRect( rect );
            }

            var iconRect = new Rect( rect.Left, rect.Top, Theme.RowHeight, Theme.RowHeight );
            if ( _option.Icon != null )
            {
                Paint.Draw( iconRect.Shrink( 4 ), _option.Icon, 1f );
            }

            var textRect = rect;
            textRect.Left = iconRect.Right + 8f;
            textRect.Right -= 20f;

            Rect? extensionRect = null;
            if ( !string.IsNullOrWhiteSpace( _option.Subtitle ) )
            {
                var width = Math.Clamp( textRect.Width * 0.3f, 60f, 140f );
                extensionRect = new Rect( textRect.Right - width, rect.Top, width, rect.Height );
                textRect.Right = extensionRect.Value.Left - 8f;
            }

            Paint.SetPen( Theme.Text );
            Paint.DrawText( textRect, _option.Title, TextFlag.LeftCenter );

            if ( extensionRect.HasValue )
            {
                Paint.SetPen( Theme.Text.WithAlpha( 0.8f ) );
                Paint.DrawText( extensionRect.Value, _option.Subtitle, TextFlag.RightCenter );
            }
        }
    }
}