Editor/HierarchySelection.cs
using System.Collections.Generic;
using System.Linq;

namespace HierarchyExtension;

/// <summary>
/// The Hierarchy's selection, so its rows are right while a drag selection is going on in the scene view.
/// <para>
/// The stock drag selection puts whatever its box touches into the scene's selection on every frame of the drag, and
/// the Hierarchy highlights exactly that: parents, maps and unselectable objects that the fully-inside rule
/// (<see cref="ScenePicking"/>) only takes out when the mouse comes up. Taking them out every frame would have the stock
/// one put them back every frame. So the Hierarchy gets a selection of its own instead: its tree's
/// <c>TreeView.Selection</c> is a public setting, and the tree paints rows by asking it, repaints when its hash changes,
/// and scrolls to what's added to it (tools source, <c>BaseItemWidget</c>, 26.09.29).
/// </para>
/// <para>
/// It holds a copy of what the Hierarchy should show: the scene's selection, or during a drag selection what that will
/// select (<see cref="ScenePicking.DragResult"/>). Selecting in the Hierarchy goes straight to the scene's selection,
/// as before. The inspector and the move handles follow the scene's selection, so during a drag they still show the
/// stock one's picks.
/// </para>
/// </summary>
public sealed class HierarchySelection : SelectionSystem
{
	static HierarchySelection _current;

	/// <summary>The one on the Hierarchy in use, if any.</summary>
	internal static HierarchySelection Current => _current;

	readonly SelectionSystem _selection;

	HierarchySelection( SelectionSystem selection )
	{
		_selection = selection;
	}

	/// <summary>
	/// Puts one on the Hierarchy's tree, and again whenever the tree is given the scene's selection back: it does that
	/// each time it rebuilds, and on a new scene. Then brings its copy up to date.
	/// </summary>
	[EditorEvent.Frame]
	static void Frame()
	{
		var tree = SceneTreeWidget.Current?.TreeView;
		var selection = SceneEditorSession.Active?.Selection;
		if ( !tree.IsValid() || selection is null ) return;

		if ( tree.Selection == _current && _current?._selection == selection )
		{
			_current.Sync();
			return;
		}

		// the tree has the scene's own, or one of these from before a code reload that wasn't carried over
		var ours = tree.Selection?.GetType().FullName == typeof( HierarchySelection ).FullName;
		if ( tree.Selection != selection && !ours ) return;

		var view = new HierarchySelection( selection );

		// filled before the tree has it, so filling it doesn't scroll the tree
		view.Sync();
		tree.Selection = view;
		_current = view;
	}

	/// <summary>Makes the copy what the Hierarchy should show now, adding and removing only what differs.</summary>
	internal void Sync()
	{
		IEnumerable<object> target = ScenePicking.DragResult ?? (IEnumerable<object>)_selection;
		var wanted = target.ToHashSet();

		foreach ( var obj in this.ToArray() )
		{
			if ( !wanted.Contains( obj ) ) base.Remove( obj );
		}

		foreach ( var obj in target )
		{
			if ( !base.Contains( obj ) ) base.Add( obj );
		}
	}

	public override bool Contains( object obj ) => ScenePicking.DragResult is null ? _selection.Contains( obj ) : base.Contains( obj );

	public override bool Any() => ScenePicking.DragResult is null ? _selection.Any() : base.Any();

	public override bool Add( object obj )
	{
		var added = _selection.Add( obj );
		Sync();
		return added;
	}

	public override bool Remove( object obj )
	{
		var removed = _selection.Remove( obj );
		Sync();
		return removed;
	}

	public override void Clear()
	{
		_selection.Clear();
		Sync();
	}

	public override bool Set( object obj )
	{
		var changed = _selection.Set( obj );
		Sync();
		return changed;
	}
}