XGUI/Elements/ExtraElements/FileBrowserTree.cs
using Sandbox;
using Sandbox.UI;
using System;
using System.Collections.Generic;
using System.Linq;

namespace XGUI;

/// <summary>
/// A tree view that shows the directory structure for navigation, similar to Windows 95 Explorer.
/// </summary>
public class FileBrowserTree : Panel
{
	public BaseFileSystem CurrentFileSystem;
	public string RootPath = "/";
	public event Action<string> OnDirectorySelected;

	private TreeNode RootNode;
	private string SelectedPath;

	public FileBrowserTree()
	{
		AddClass( "file-browser-tree" );
	}

	public void Initialize( string rootPath )
	{
		RootPath = rootPath;
		SelectedPath = rootPath;
		Refresh();
	}

	public void Refresh()
	{
		DeleteChildren();

		if ( string.IsNullOrEmpty( RootPath ) || CurrentFileSystem == null )
			return;

		if ( !CurrentFileSystem.DirectoryExists( RootPath ) )
			return;

		try
		{
			string rootName = System.IO.Path.GetFileName( RootPath );
			// Use "Root" if at the file system's root
			if ( string.IsNullOrEmpty( rootName ) || rootName == "/" )
				rootName = "Root";

			RootNode = new TreeNode( this, rootName, RootPath, true );
			AddChild( RootNode );
			RootNode.Expand(); // Auto-expand root
			RootNode.SetSelected( true ); // Select root by default
		}
		catch ( Exception ex )
		{
			Log.Error( $"Error creating file tree: {ex.Message}" );
		}
	}

	/// <summary>
	/// Select a directory in the tree and expand the path to it.
	/// </summary>
	public void SelectDirectory( string path )
	{
		if ( RootNode == null || string.IsNullOrEmpty( path ) )
			return;

		// Don't do anything if it's already selected
		if ( SelectedPath == path )
			return;

		SelectedPath = path;

		// If path is root, just select the root node
		if ( path == RootPath )
		{
			UnselectAll();
			RootNode.SetSelected( true );
			return;
		}

		// Expand the path to the directory
		ExpandPathTo( path );
	}

	/// <summary>
	/// Expand the tree path to the specified directory.
	/// </summary>
	private void ExpandPathTo( string targetPath )
	{
		// First unselect everything
		UnselectAll();

		// Start with the root
		string currentPath = RootPath;
		TreeNode currentNode = RootNode;

		// Get the relative path parts
		string relativePath = GetRelativePath( RootPath, targetPath );
		if ( string.IsNullOrEmpty( relativePath ) )
		{
			currentNode.SetSelected( true );
			return;
		}

		string[] pathParts = relativePath.Split( '/' ).Where( p => !string.IsNullOrEmpty( p ) ).ToArray();
		//string[] pathParts = relativePath.Split( '/', '\\' ).Where( p => !string.IsNullOrEmpty( p ) ).ToArray();

		// Expand each part of the path
		foreach ( string part in pathParts )
		{
			// Expand current node to see its children
			currentNode.Expand();

			// Move to the next directory in the path
			currentPath = System.IO.Path.Combine( currentPath, part ).Replace( "\\", "/" );

			// Find the child node for this path part
			TreeNode nextNode = null;
			foreach ( var child in currentNode.ChildNodes )
			{
				if ( child.FullPath.Equals( currentPath, StringComparison.OrdinalIgnoreCase ) )
				{
					nextNode = child;
					break;
				}
			}

			// If we couldn't find a matching child, break out
			if ( nextNode == null )
				break;

			currentNode = nextNode;
		}

		// Select the final node if we reached it
		if ( currentNode.FullPath.Equals( targetPath, StringComparison.OrdinalIgnoreCase ) )
		{
			currentNode.SetSelected( true );
		}
	}

	private string GetRelativePath( string basePath, string fullPath )
	{
		// Normalize paths with forward slashes
		basePath = basePath.Replace( "\\", "/" ).TrimEnd( '/' ) + "/";
		fullPath = fullPath.Replace( "\\", "/" );

		if ( fullPath.StartsWith( basePath, StringComparison.OrdinalIgnoreCase ) )
		{
			return fullPath.Substring( basePath.Length );
		}

		return fullPath;
	}

	private void UnselectAll()
	{
		// Recursively unselect all nodes starting from root
		if ( RootNode != null )
			UnselectNode( RootNode );
	}

	private void UnselectNode( TreeNode node )
	{
		node.SetSelected( false );
		foreach ( var child in node.ChildNodes )
		{
			UnselectNode( child );
		}
	}

	private class TreeNode : Panel
	{
		public FileBrowserTree ParentTree;
		public string NodeName;
		public string FullPath;
		public bool IsDirectory;
		public bool IsExpanded;
		public List<TreeNode> ChildNodes = new();
		private Panel ExpandIcon;
		private Panel ContentPanel;
		private Panel ChildrenPanel;
		private bool IsSelected;

		public TreeNode( FileBrowserTree parent, string name, string path, bool isDirectory )
		{
			ParentTree = parent;
			NodeName = name;
			FullPath = path;
			IsDirectory = isDirectory;
			IsExpanded = false;
			IsSelected = false;

			AddClass( "tree-node" );

			// Create the row with icon and label
			ContentPanel = new Panel();
			ContentPanel.AddClass( "tree-node-content" );

			if ( isDirectory )
			{
				// Create expand/collapse icon
				ExpandIcon = new Panel();
				ExpandIcon.AddClass( "expand-icon" );
				ExpandIcon.AddEventListener( "onclick", ( e ) =>
				{
					e.StopPropagation();
					ToggleExpand();
				} );
				ContentPanel.AddChild( ExpandIcon );

				// Create folder icon
				var folderIcon = new Panel();
				folderIcon.AddClass( "folder-icon" );
				ContentPanel.AddChild( folderIcon );
			}
			else
			{
				// Create file icon
				var fileIcon = new Panel();
				fileIcon.AddClass( "file-icon" );
				ContentPanel.AddChild( fileIcon );
			}

			// Create label
			var label = new Label();
			label.Text = name;
			label.AddClass( "node-label" );
			ContentPanel.AddChild( label );

			AddChild( ContentPanel );

			// Create container for children
			ChildrenPanel = new Panel();
			ChildrenPanel.AddClass( "tree-node-children" );
			ChildrenPanel.Style.Display = DisplayMode.None;
			AddChild( ChildrenPanel );

			// Handle click event
			ContentPanel.AddEventListener( "onclick", () =>
			{
				if ( IsDirectory )
				{
					// Unselect all nodes first
					ParentTree.UnselectAll();

					// Select this node
					SetSelected( true );

					// Notify parent of directory selection
					ParentTree.OnDirectorySelected?.Invoke( FullPath );
					ParentTree.SelectedPath = FullPath;
				}
			} );
		}

		public void ToggleExpand()
		{
			if ( IsExpanded )
				Collapse();
			else
				Expand();
		}

		public void Expand()
		{
			if ( !IsDirectory || IsExpanded )
				return;

			IsExpanded = true;
			ExpandIcon.AddClass( "expanded" );
			ChildrenPanel.Style.Display = DisplayMode.Flex;

			// Only load children once
			if ( ChildNodes.Count == 0 )
				LoadChildren();
		}

		public void Collapse()
		{
			if ( !IsDirectory || !IsExpanded )
				return;

			IsExpanded = false;
			ExpandIcon.RemoveClass( "expanded" );
			ChildrenPanel.Style.Display = DisplayMode.None;
		}

		public void SetSelected( bool selected )
		{
			IsSelected = selected;
			ContentPanel.SetClass( "selected", selected );
		}

		private void LoadChildren()
		{
			try
			{
				// Get directories
				IEnumerable<string> directories = ParentTree.CurrentFileSystem.FindDirectory( FullPath );

				// Create a node for each subdirectory
				foreach ( var dir in directories )
				{
					var dirName = System.IO.Path.GetFileName( dir );
					var node = new TreeNode( ParentTree, dirName, dir, true );
					ChildNodes.Add( node );
					ChildrenPanel.AddChild( node );

					// Check if this directory has subdirectories (to show +/- icons)
					bool hasSubdirs = HasSubdirectories( dir );
					if ( !hasSubdirs )
					{
						node.ExpandIcon.Style.Opacity = 0; // Hide expand icon if no subdirs
					}
				}
			}
			catch ( Exception ex )
			{
				Log.Error( $"Error loading directory children: {ex.Message}" );
			}
		}

		private bool HasSubdirectories( string path )
		{
			try
			{
				var directories = ParentTree.CurrentFileSystem.FindDirectory( path );
				return directories.Any();
			}
			catch
			{
				return false;
			}
		}
	}
}