Router.razor.cs
using System;
using System.Collections.Generic;
using System.Linq;
using BetterUI.Extensions;
using Sandbox.Diagnostics;

namespace BetterUI;

/// <summary>
/// A panel that manages child RouterPages. It will only show one page at a time,
/// and automatically adds the page to the DOM.
/// </summary>
public sealed partial class Router : Panel
{
	private RouterPage? _currentPage;
	private readonly List<RouterPage> _instances = new();

	/// <summary>
	/// Gets or sets the name of the router. This is used to identify the router.
	/// </summary>
	public new string Id { get; set; } = null!;

	/// <summary>
	/// Gets or sets the default page of the router. This is the page that will be shown
	/// if no page is specified in the navigation.
	/// </summary>
	public Type? DefaultPage { get; set; }

	protected override void OnAfterTreeRender( bool firstTime )
	{
		Assert.NotNull( Id, $"{nameof(Router)} must have an {nameof(Id)}" );

		foreach ( var page in GetDescendants( this ) )
		{
			if ( _instances.Exists( x => x.GetType().Name == page.GetType().Name ) )
				continue;

			page.Style.Position = PositionMode.Absolute;
			page.Style.Top = 0;
			page.Style.Left = 0;
			page.Hide();

			_instances.Add( page );
		}

		if ( !firstTime ) return;
		if ( DefaultPage is null ) return;

		_currentPage = GetPage( DefaultPage );
		_currentPage?.Show();

		Scene.RunSceneEvent<IRouterEvent>( x => x.OnRouterReady() );
	}

	/// <summary>
	/// Navigates to a new page.
	/// </summary>
	/// <param name="type">The type of page to navigate to.</param>
	/// <param name="args">Arguments to pass to the page.</param>
	/// <returns>The page that was navigated to.</returns>
	private RouterPage? Navigate( Type type, params object[] args )
	{
		var page = GetPage( type );
		if ( page is null ) return null;

		foreach ( var p in _instances )
			p.Style.ZIndex = 0;

		if ( _currentPage is not null )
		{
			Scene.RunSceneEvent<IRouterEvent>( x => x.OnRouterPageClose( _currentPage ) );

			_currentPage.Style.ZIndex = 0;
			_currentPage.Hide();
		}

		_currentPage = page;
		_currentPage.Show();
		_currentPage.Style.ZIndex = 10;

		Scene.RunSceneEvent<IRouterEvent>( x => x.OnRouterPageOpen( _currentPage, args ) );
		return _currentPage;
	}

	/// <summary>
	/// Navigates to a new page.
	/// </summary>
	/// <param name="args">Arguments to pass to the page.</param>
	/// <returns>The page that was navigated to.</returns>
	public T Navigate<T>( params object[] args ) where T : RouterPage, new() => (T)Navigate( typeof(T), args )!;

	/// <summary>
	/// Checks if the current page is of the specified type.
	/// </summary>
	/// <returns>True if the current page is of the specified type, false otherwise.</returns>
	public bool IsOpen<T>() where T : RouterPage, new() => _currentPage is T;

	/// <summary>
	/// Retrieves a page of the specified type from the existing instances.
	/// </summary>
	/// <param name="type">The type of the page to retrieve.</param>
	/// <returns>The page of the specified type, or null if not found.</returns>
	private RouterPage? GetPage( Type type ) => _instances.FirstOrDefault( x => x.GetType() == type );

	/// <summary>
	/// Gets a list of all RouterPage descendants within a panel.
	/// </summary>
	/// <param name="panel">The panel to search within.</param>
	/// <returns>A list of RouterPage descendants.</returns>
	private static List<RouterPage> GetDescendants( Panel panel ) =>
		panel.Descendants.OfType<RouterPage>().ToList();

	protected override int BuildHash() => HashCode.Combine( _instances.Count, _currentPage );
}