Editor/Effects/SFXREffectTypeSelector.cs
using Sandbox;
using Editor;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace SFXR.Editor;

/// <summary>
/// A popup dialog to select an entity type
/// </summary>
internal partial class SFXREffectTypeSelector : PopupWidget
{
    public Action<TypeDescription> OnSelect { get; set; }
    List<EffectSelection> Panels { get; set; } = new();
    int CurrentPanelId { get; set; } = 0;
    Widget Main { get; set; }

    string searchString;
    const string NoCategoryName = "Uncategorized";

    internal LineEdit Search { get; init; }

    public SFXREffectTypeSelector( Widget parent ) : base( parent )
    {
        Layout = Layout.Column();

        var head = Layout.Row();
        head.Margin = 6;

        Layout.Add( head );

        Main = new Widget( this );
        Main.Layout = Layout.Row();
        Layout.Add( Main );

        FixedWidth = 200;
        MaximumHeight = 300;
        DeleteOnClose = true;

        Search = new LineEdit( this );
        Search.MinimumHeight = 22;
        Search.PlaceholderText = "Search..";
        Search.TextEdited += ( t ) =>
        {
            searchString = t;
            ResetSelection();
        };

        head.Add( Search );

        ResetSelection();

        Search.Focus();
    }

    /// <summary>
    /// Pushes a new selection to the selector
    /// </summary>
    /// <param name="selection"></param>
    void PushSelection( EffectSelection selection )
    {
        CurrentPanelId++;

        // Do we have something at our new index, if so, kill it
        if ( Panels.Count > CurrentPanelId && Panels.ElementAt( CurrentPanelId ) is var existingObj ) existingObj.Destroy();

        Panels.Insert( CurrentPanelId, selection );
        Main.Layout.Add( selection, 1 );

        if ( !selection.IsManual )
        {
            UpdateSelection( selection );
        }

        AnimateSelection( true, Panels[CurrentPanelId - 1], selection );

        selection.Focus();
    }

    /// <summary>
    /// Pops the current selection off
    /// </summary>
    internal void PopSelection()
    {
        // Don't pop while empty
        if ( CurrentPanelId == 0 ) return;

        var currentIdx = Panels[CurrentPanelId];
        CurrentPanelId--;

        AnimateSelection( false, currentIdx, Panels[CurrentPanelId] );

        Panels[CurrentPanelId].Focus();
    }

    /// <summary>
    /// Runs an animation on the last selection, and the current selection.
    /// I kinda hate this. A lot. But it's pretty.
    /// </summary>
    /// <param name="forward"></param>
    /// <param name="prev"></param>
    /// <param name="selection"></param>
    void AnimateSelection( bool forward, EffectSelection prev, EffectSelection selection )
    {
        const string easing = "ease-out";
        const float speed = 0.3f;

        var distance = Width;

        var prevFrom = prev.Position.x;
        var prevTo = forward ? prev.Position.x - distance : prev.Position.x + distance;

        var selectionFrom = forward ? selection.Position.x + distance : selection.Position.x;
        var selectionTo = forward ? selection.Position.x : selection.Position.x + distance;

        var func = ( EffectSelection a, float x ) =>
        {
            a.Position = a.Position.WithX( x );
            OnMoved();
        };

        Animate.Add( prev, speed, prevFrom, prevTo, x => func( prev, x ), easing );
        Animate.Add( selection, speed, selectionFrom, selectionTo, x => func( selection, x ), easing );
    }

    /// <summary>
    /// Resets the current selection, useful when setting up / searching
    /// </summary>
    protected void ResetSelection()
    {
        Main.Layout.Clear( true );
        Panels.Clear();

        var selection = new EffectSelection( this, this );

        CurrentPanelId = 0;

        UpdateSelection( selection );

        Panels.Add( selection );
        Main.Layout.Add( selection );
    }

    protected override void OnPaint()
    {
        Paint.Antialiasing = true;
        Paint.SetPen( Theme.WidgetBackground.Darken( 0.4f ), 1 );
        Paint.SetBrush( Theme.WidgetBackground );
        Paint.DrawRect( LocalRect.Shrink( 1 ), 3 );
    }

    /// <summary>
    /// Called when a category is selected
    /// </summary>
    /// <param name="category"></param>
    void OnCategorySelected( string category )
    {
        // Push this as a new selection
        PushSelection( new EffectSelection( this, this, category ) );
    }

    /// <summary>
    /// Called when an individual effect is selected
    /// </summary>
    /// <param name="type"></param>
    void OnEffectSelected( TypeDescription type )
    {
        OnSelect( type );
        Destroy();
    }

    protected override void OnKeyRelease( KeyEvent e )
    {
        if ( e.Key == KeyCode.Down )
        {
            var selection = Panels[CurrentPanelId];
            if ( selection.ItemList.FirstOrDefault() != null )
            {
                selection.Focus();
                selection.PostKeyEvent( KeyCode.Down );
                e.Accepted = true;
            }
        }
    }

    /// <summary>
    /// Updates any selection
    /// </summary>
    /// <param name="selection"></param>
    void UpdateSelection( EffectSelection selection )
    {
        selection.Clear();

        selection.ItemList.Add( selection.CategoryHeader );

        // entity components
        var types = EditorTypeLibrary.GetTypes<SFXREffect>().Where( x => !x.IsAbstract );

        if ( !string.IsNullOrWhiteSpace( searchString ) )
        {
            var searchWords = searchString.Split( ' ', StringSplitOptions.RemoveEmptyEntries );
            var query = types.Where( x =>
                searchWords.All( word => x.Title.Contains( word, StringComparison.OrdinalIgnoreCase ) )
            );

            foreach ( var type in query )
            {
                selection.AddEntry( new EffectEntry( selection, type ) { MouseClick = () => OnEffectSelected( type ) } );
            }

            selection.AddStretchCell();
            return;
        }

        if ( selection.Category == null )
        {
            var categories = types.Select( x => string.IsNullOrWhiteSpace( x.Group ) ? NoCategoryName : x.Group ).Distinct().OrderBy( x => x ).ToArray();
            if ( categories.Length > 1 )
            {
                foreach ( var category in categories )
                {
                    selection.AddEntry( new EffectCategory( selection )
                    {
                        Category = category,
                        MouseClick = () => OnCategorySelected( category ),
                    } );
                }

                selection.AddStretchCell();

                return;
            }
        }
        else
        {
            types = types.Where( x => selection.Category == NoCategoryName ? x.Group == null : x.Group == selection.Category ).OrderBy( x => x.Title );

            foreach ( var type in types )
            {
                selection.AddEntry( new EffectEntry( selection, type ) { MouseClick = () => OnEffectSelected( type ) } );
            }
            selection.AddStretchCell();
        }
    }

    /// <summary>
    /// A widget that contains a given selection - we hold this in a class because more than one can exist.
    /// </summary>
    partial class EffectSelection : Widget
    {
        internal string Category { get; init; }
        internal Widget CategoryHeader { get; init; }
        ScrollArea Scroller { get; init; }
        SFXREffectTypeSelector Selector { get; set; }

        internal List<Widget> ItemList { get; private set; } = new();
        internal int CurrentItemId { get; private set; } = 0;
        internal Widget CurrentItem { get; private set; }

        internal bool IsManual { get; set; }

        internal EffectSelection( Widget parent, SFXREffectTypeSelector selector, string categoryName = null ) : base( parent )
        {
            Selector = selector;
            Category = categoryName;
            FixedWidth = 200;
            MinimumHeight = 220;

            Layout = Layout.Column();

            CategoryHeader = new Widget( this );
            CategoryHeader.FixedHeight = 24;
            CategoryHeader.OnPaintOverride = PaintHeader;
            CategoryHeader.MouseClick = Selector.PopSelection;
            Layout.Add( CategoryHeader );

            Scroller = new ScrollArea( this );
            Scroller.Layout = Layout.Column();
            Scroller.FocusMode = FocusMode.None;
            Layout.Add( Scroller, 1 );

            Scroller.Canvas = new Widget( Scroller );
            Scroller.Canvas.Layout = Layout.Column();
        }

        protected bool SelectMoveRow( int delta )
        {
            var selection = Selector.Panels[Selector.CurrentPanelId];
            if ( delta == 1 && selection.ItemList.Count - 1 > selection.CurrentItemId )
            {
                selection.CurrentItem = selection.ItemList[++selection.CurrentItemId];
                selection.Update();

                if ( selection.CurrentItem != null )
                {
                    Scroller.MakeVisible( selection.CurrentItem );
                }

                return true;
            }
            else if ( delta == -1 )
            {
                if ( selection.CurrentItemId > 0 )
                {
                    selection.CurrentItem = selection.ItemList[--selection.CurrentItemId];
                    selection.Update();

                    if ( selection.CurrentItem != null )
                    {
                        Scroller.MakeVisible( selection.CurrentItem );
                    }

                    return true;
                }
                else
                {
                    selection.Selector.Search.Focus();
                    selection.CurrentItem = null;
                    selection.Update();
                    return true;
                }
            }

            return false;
        }

        protected bool Enter()
        {
            var selection = Selector.Panels[Selector.CurrentPanelId];
            if ( selection.ItemList[selection.CurrentItemId] is Widget entry )
            {
                entry.MouseClick?.Invoke();
                return true;
            }

            return false;
        }

        protected override void OnKeyRelease( KeyEvent e )
        {
            // Move down
            if ( e.Key == KeyCode.Down )
            {
                e.Accepted = true;
                SelectMoveRow( 1 );
                return;
            }

            // Move up 
            if ( e.Key == KeyCode.Up )
            {
                e.Accepted = true;
                SelectMoveRow( -1 );
                return;
            }

            // Back button while in any selection, goes to previous selction.
            if ( e.Key == KeyCode.Left )
            {
                e.Accepted = true;
                Selector.PopSelection();
                return;
            }

            // Moving right, or hitting the enter key assumes you're trying to select something
            if ( (e.Key == KeyCode.Return || e.Key == KeyCode.Right) && Enter() )
            {
                e.Accepted = true;
                return;
            }
        }

        internal bool PaintHeader()
        {
            var c = CategoryHeader;
            var selected = c.IsUnderMouse || CurrentItem == c;

            Paint.ClearPen();
            Paint.SetBrush( selected ? Theme.Selection : Theme.WidgetBackground.WithAlpha( selected ? 0.7f : 0.4f ) );
            Paint.DrawRect( c.LocalRect );

            var r = c.LocalRect.Shrink( 12, 2 );
            Paint.SetPen( Theme.ControlText );

            if ( Selector.CurrentPanelId > 0 )
            {
                Paint.DrawIcon( r, "arrow_back", 14, TextFlag.LeftCenter );
            }

            Paint.SetDefaultFont( 8 );
            Paint.DrawText( r, string.IsNullOrEmpty( Category ) ? "Effect" : Category, TextFlag.Center );

            return true;
        }

        /// <summary>
        /// Adds a new entry to the current selection.
        /// </summary>
        /// <param name="entry"></param>
        internal Widget AddEntry( Widget entry )
        {
            var layoutWidget = Scroller.Canvas.Layout.Add( entry );
            ItemList.Add( entry );

            if ( entry is EffectEntry e ) e.Selector = this;

            return layoutWidget;
        }

        /// <summary>
        /// Adds a stretch cell to the bottom of the selection - good to call this when you know you're done adding entries.
        /// </summary>
        internal void AddStretchCell()
        {
            Scroller.Canvas.Layout.AddStretchCell( 1 );
            Update();
        }

        /// <summary>
        /// Adds a separator cell.
        /// </summary>
        internal void AddSeparator()
        {
            Scroller.Canvas.Layout.AddSeparator( true );
            Update();
        }

        /// <summary>
        /// Clears the current selection
        /// </summary>
        internal void Clear()
        {
            Scroller.Canvas.Layout.Clear( true );
            ItemList.Clear();
        }

        protected override void OnPaint()
        {
            Paint.Antialiasing = true;
            Paint.SetPen( Theme.WidgetBackground.Darken( 0.8f ), 1 );
            Paint.SetBrush( Theme.WidgetBackground.Darken( 0.2f ) );
            Paint.DrawRect( LocalRect.Shrink( 0 ), 3 );
        }
    }

    /// <summary>
    /// An effect entry
    /// </summary>
    class EffectEntry : Widget
    {
        public string Text { get; set; } = "My Effect";
        public string Icon { get; set; } = "note_add";
        public bool IsSelected { get; set; } = false;

        internal EffectSelection Selector { get; set; }

        public TypeDescription Type { get; init; }

        internal EffectEntry( Widget parent, TypeDescription type = null ) : base( parent )
        {
            FixedHeight = 24;
            Type = type;

            if ( type is not null )
            {
                Text = type.Title;
                Icon = type.Icon;
            }
        }

        protected override void OnPaint()
        {
            var r = LocalRect.Shrink( 12, 2 );
            var selected = IsUnderMouse || Selector.CurrentItem == this;
            var opacity = selected ? 1.0f : 0.7f;

            if ( selected )
            {
                Paint.ClearPen();
                Paint.SetBrush( Theme.Selection );
                Paint.DrawRect( LocalRect );
            }

            if ( Type is not null && !string.IsNullOrEmpty( Type.Icon ) )
            {
                Type.PaintComponentIcon( new Rect( r.Position, r.Height ).Shrink( 2 ), opacity );
            }
            else
            {
                Paint.SetPen( Theme.Green.WithAlpha( opacity ) );

                var icon = !string.IsNullOrEmpty( Icon ) ? Icon : "note_add";
                Paint.DrawIcon( new Rect( r.Position, r.Height ).Shrink( 2 ), icon, r.Height, TextFlag.Center );
            }

            r.Left += r.Height + 6;

            Paint.SetDefaultFont( 8 );
            Paint.SetPen( Theme.ControlText.WithAlpha( selected ? 1.0f : 0.5f ) );
            Paint.DrawText( r, Text, TextFlag.LeftCenter );
        }
    }

    /// <summary>
    /// A category effect entry
    /// </summary>
    class EffectCategory : EffectEntry
    {
        public string Category { get; set; }
        public EffectCategory( Widget parent ) : base( parent ) { }

        protected override void OnPaint()
        {
            var selected = IsUnderMouse || Selector.CurrentItem == this;

            if ( selected )
            {
                Paint.ClearPen();
                Paint.SetBrush( Theme.Selection );
                Paint.DrawRect( LocalRect );
            }

            var r = LocalRect.Shrink( 12, 2 );

            Paint.SetPen( Theme.ControlText.WithAlpha( selected ? 1.0f : 0.5f ) );

            Paint.SetDefaultFont( 8 );
            Paint.DrawText( r, Category, TextFlag.LeftCenter );
            Paint.DrawIcon( r, "arrow_forward", 14, TextFlag.RightCenter );
        }
    }
}