UI/ContextMenu/Inspector.razor
@using Sandbox;
@using Sandbox.UI;
@namespace Sandbox
@inherits Panel

<root>

	<div class="canvas" @ref=_canvas></div>

	<div class="inspector-panel @(_editors?.Any( e => e.WasVisible ) == true ? "" : "hidden")" @onmousedown="@WindowPress">
		<div class="tab-bar">
			@foreach ( var entry in _editors?.Where( e => e.WasVisible ) ?? [] )
			{
				var e = entry;
				<div class="tab @( _active == e ? "active" : "" )" @onclick="@(() => SetActive( e ))">
					@e.Editor.Title
				</div>
			}
		</div>
		<div class="window-stack" @ref=_windowStack></div>
	</div>

</root>

@code
{
    public GameObject Hovered => hovered;

    Panel _canvas = default;
    Panel _windowStack = default;

    record EditorEntry( IInspectorEditor Editor )
    {
        public bool WasVisible { get; set; }
    }
    List<EditorEntry> _editors;
    EditorEntry _active;

    void InitEditors()
    {
        if ( _editors != null || _windowStack == null ) return;
        _editors = new();

        var types = TypeLibrary.GetTypesWithAttribute<InspectorEditorAttribute>()
            .OrderByDescending( x => x.Type.GetAttribute<OrderAttribute>()?.Value ?? 0 )
            .ThenBy( t => t.Attribute.Type is null ? 1 : 0 )
            .ToList();

        foreach ( var (typeDesc, attr) in types )
        {
            var editor = typeDesc.Create<IInspectorEditor>();
            if ( editor is not Panel editorPanel ) continue;

            editorPanel.AddClass( "window" );
            editorPanel.SetClass( "hidden", true );
            _windowStack.AddChild( editorPanel );

            _editors.Add( new EditorEntry( editor ) );
        }
        StateHasChanged();
    }

    void AttachEditors()
    {
        if ( _windowStack == null ) return;

        foreach ( var entry in _editors ?? [] )
        {
            if ( entry.Editor is Panel editorPanel )
                _windowStack.AddChild( editorPanel );
        }
    }

    void SetActive( EditorEntry entry )
    {
        _active = entry;
        ApplyVisibility();
        StateHasChanged();
    }

    void ApplyVisibility()
    {
        foreach ( var e in _editors )
            (e.Editor as Panel)?.SetClass( "hidden", !( e.WasVisible && e == _active ) );
    }

    HashSet<GameObject> _lastSelected = new();

    void UpdateEditors()
    {
        if ( _editors == null ) return;

        bool changed = false;

        foreach ( var entry in _editors )
        {
            bool visible = entry.Editor.TrySetTarget( _selected.ToList() );
            if ( visible != entry.WasVisible ) changed = true;
            entry.WasVisible = visible;
        }

        if ( !_lastSelected.SetEquals( _selected ) )
        {
            changed = true;
            _lastSelected = _selected.ToHashSet();
        }

        if ( changed )
        {
            var visible = _editors.Where( e => e.WasVisible ).ToList();
            if ( _active == null || !_active.WasVisible )
                _active = visible.LastOrDefault();

            ApplyVisibility();
            StateHasChanged();
        }
    }

    protected override int BuildHash() => HashCode.Combine( hovered, _selected.Count, _active );

    public override void Tick()
    {
        _selected.RemoveWhere( x => !x.IsValid() );
        InitEditors();
        UpdateEditors();
        UpdateHighlights();
        UpdateCursor();
    }

    protected override void OnAfterTreeRender( bool firstTime )
    {
        base.OnAfterTreeRender( firstTime );

        if ( _editors == null ) return;

        AttachEditors();
        ApplyVisibility();
    }

    protected override void OnVisibilityChanged()
    {
        UpdateHighlights();
    }

    void UpdateHighlights()
    {
        var host = Ancestors.OfType<ContextMenuHost>().FirstOrDefault();
        if ( host is null ) return;

        if ( host.SelectedOutline is null || host.HoveredOutline is null )
            return;

        host.SelectedOutline.Targets ??= new();
        host.SelectedOutline.Targets.Clear();
        host.SelectedOutline.Color = new Color( 4.7f, 10.1f, 30.6f, 1 );
        host.SelectedOutline.ObscuredColor = new Color( 2.2f, 2.3f, 2.9f, 0.1f );
        host.SelectedOutline.Width = 0.2f;

        host.HoveredOutline.Targets ??= new();
        host.HoveredOutline.Targets.Clear();
        host.HoveredOutline.Color = new Color( 2.6f, 2.0f, 0.2f, 1 );
        host.HoveredOutline.ObscuredColor = new Color( 2.6f, 2.0f, 0.2f, 0.1f );
        host.HoveredOutline.Width = 0.2f;

        if ( !IsVisible ) return;

        host.SelectedOutline.Targets.AddRange( _selected.SelectMany( x => GetRenderers( x ) ) ?? [] );

        if ( !_selected.Contains( hovered ) )
        {
            host.HoveredOutline.Targets = GetRenderers( hovered ).ToList();
        }
        else
        {
            host.HoveredOutline.Targets = default;
        }
    }

    IEnumerable<Renderer> GetRenderers( GameObject o )
    {
        if ( !o.IsValid() ) yield break;

        foreach ( var r in o.GetComponents<Renderer>() )
        {
            yield return r;
        }

        foreach ( var c in o.Children )
        {
            if ( c.NetworkMode == NetworkMode.Object ) continue;

            foreach ( var rr in GetRenderers( c ) )
            {
                yield return rr;
            }
        }
    }

    GameObject hovered;

    HashSet<GameObject> _selected = new();

    void UpdateCursor()
    {
        var cursorPos = Mouse.Position;
        var screenRay = Scene.Camera.ScreenPixelToRay( cursorPos );
        var tr = Scene.Trace.Ray( screenRay, 4096 )
                            .IgnoreGameObjectHierarchy( Player.FindLocalPlayer()?.GameObject )
                            .Run();

        var go = tr.Collider?.GameObject ?? tr.GameObject;
        go = go.FindNetworkRoot();

        if ( !CanSelect( go ) ) go = null;

        UpdateHovered( go );
    }

    bool CanSelect( GameObject o )
    {
        if ( !o.IsValid() ) return false;
        if ( o.Tags.Has( "world" ) ) return false;
        if ( o.NetworkMode == NetworkMode.Never ) return false;

        o = o?.FindNetworkRoot();

        return true;
    }

    void UpdateHovered( GameObject o )
    {
        o = o?.FindNetworkRoot();

        if ( hovered == o ) return;

        hovered = o;
        PlaySound( "ui.button.over" );
    }


    public void WorldMouseDown(MousePanelEvent e)
    {
        SelectObject( hovered );
    }

    public void WorldMouseRightDown(MousePanelEvent e)
    {
        if ( !hovered.IsValid() ) return;

        SelectObject( hovered );

        var target = hovered;
        var ev = new IContextMenuEvent.Event( target );
        Scene.RunEvent<IContextMenuEvent>( listener => listener.PopulateContextMenu( ev ) );
        if ( !ev.Options.Any() ) return;

        var menu = new Sandbox.UI.Menu();
        ev.Populate( menu );

        menu.Open( this, Popup.PositionMode.UnderMouse );
    }

    public void SelectObject( GameObject o )
    {
        if ( !o.IsValid() )
        {
            _selected.Clear();
        }
        else
        {
            if ( !Input.Down( "run" ) )
            {
                _selected.Clear();
            }

            _selected.Add( o );
        }
    }

	void WindowPress( PanelEvent panelEvent )
	{
		panelEvent.StopPropagation();
	}
}