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 );

        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 );
            editorPanel.Parent = _windowStack;

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

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

    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 );
            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();
        }
    }

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

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

    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 == null ) 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;

    List<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 (!_canvas.HasHovered) go = default;
        if (!CanSelect(go)) go = null;

        UpdateHovered(go);
    }

    bool CanSelect( GameObject o )
    {
        if (o == null) 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 isPlayer = target.Tags.Has( "player" );
        var prop = target.GetComponent<Prop>();
        var isGibbable = prop.IsValid() && prop.Health > 0;

        var menu = MenuPanel.Open( this );
        if ( !isPlayer )
        {
            menu.AddOption( "🗑️", Game.Language.GetPhrase( "spawnmenu.inspect.delete" ), () => GameManager.DeleteInspectedObject( target ) );
        }

        if ( isGibbable )
        {
            menu.AddOption( "💥", Game.Language.GetPhrase( "spawnmenu.inspect.break" ), () => GameManager.BreakInspectedProp( prop ) );
        }
    }

    public void WorldMouseUp(MousePanelEvent e)
    {
        // nothing.
    }

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

            _selected.Remove(o);
            _selected.Add(o);
        }

        _selected = _selected.Distinct().ToList();
    }

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