Editor/PackageFolderNode.cs
using Editor;
using Sandbox;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;

namespace GeneralGame.Editor;

/// <summary>
/// Tree node representing a cloud package folder (parent project loaded from cloud)
/// </summary>
public class PackageFolderNode : TreeNode
{
    public Package Package { get; }
    public string DisplayName { get; }
    public string IconName { get; set; }

    /// <summary>
    /// Tracks whether this folder is currently expanded in the tree view.
    /// Updated during OnPaint.
    /// </summary>
    public bool IsExpanded { get; private set; }

    private List<string> _cachedFiles;

    public override bool HasChildren => GetPackageFiles().Any();
    public override string Name => DisplayName;
    public override bool CanEdit => false;

    public PackageFolderNode(Package package, string icon = "cloud") : base()
    {
        Package = package;
        DisplayName = package.Title;
        IconName = icon;
        Value = this;
    }

    private List<string> GetPackageFiles()
    {
        if (_cachedFiles == null)
        {
            try
            {
                _cachedFiles = AssetSystem.GetPackageFiles(Package).ToList();
            }
            catch
            {
                _cachedFiles = new List<string>();
            }
        }
        return _cachedFiles;
    }

    protected override void BuildChildren()
    {
        Clear();

        var files = GetPackageFiles();
        if (!files.Any())
            return;

        try
        {
            // Group files by directory structure
            var directories = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
            var rootFiles = new List<string>();

            foreach (var file in files)
            {
                var parts = file.Split('/', '\\');
                if (parts.Length > 1)
                {
                    var topDir = parts[0];
                    if (!directories.ContainsKey(topDir))
                        directories[topDir] = new List<string>();
                    directories[topDir].Add(file);
                }
                else
                {
                    rootFiles.Add(file);
                }
            }

            // Add subdirectories
            foreach (var dir in directories.OrderBy(d => d.Key, StringComparer.OrdinalIgnoreCase))
            {
                AddItem(new PackageSubFolderNode(Package, dir.Key, dir.Value));
            }

            // Add root files
            foreach (var file in rootFiles.OrderBy(f => f, StringComparer.OrdinalIgnoreCase))
            {
                var fullPath = global::Editor.FileSystem.Cloud.GetFullPath(file);
                if (!string.IsNullOrEmpty(fullPath))
                {
                    AddItem(new AssetFileNode(fullPath));
                }
            }
        }
        catch (Exception ex)
        {
            Log.Warning($"Error building package tree: {ex.Message}");
        }
    }

    public override void OnPaint(VirtualWidget item)
    {
        // Track expanded state for external access
        IsExpanded = item.IsOpen;

        PaintSelection(item);

        var rect = item.Rect;

        // Package icon
        Paint.SetPen(Theme.Blue);
        Paint.DrawIcon(rect, item.IsOpen ? "folder_open" : IconName, 16, TextFlag.LeftCenter);

        rect.Left += 22;

        // Package name
        Paint.SetPen(Theme.Text);
        Paint.SetDefaultFont();
        Paint.DrawText(rect, DisplayName, TextFlag.LeftCenter);
    }

    public override bool OnContextMenu()
    {
        var menu = new ContextMenu(null);

        menu.AddOption("Refresh", "refresh", () =>
        {
            _cachedFiles = null;
            Dirty();
        });

        menu.AddOption("View on asset.party", "open_in_new", () =>
        {
            System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
            {
                FileName = $"https://asset.party/{Package.FullIdent}",
                UseShellExecute = true
            });
        });

        menu.OpenAtCursor();
        return true;
    }

    /// <summary>
    /// Check if this folder or any of its contents matches the search filter.
    /// </summary>
    public bool MatchesFilter(string filter)
    {
        if (string.IsNullOrEmpty(filter))
            return true;

        // Check package name
        if (DisplayName.ToLowerInvariant().Contains(filter))
            return true;

        // Check files
        foreach (var file in GetPackageFiles())
        {
            if (file.ToLowerInvariant().Contains(filter))
                return true;
        }

        return false;
    }
}

/// <summary>
/// Subfolder within a package
/// </summary>
public class PackageSubFolderNode : TreeNode
{
    public Package Package { get; }
    public string FolderName { get; }
    public List<string> Files { get; }

    /// <summary>
    /// Tracks whether this folder is currently expanded in the tree view.
    /// Updated during OnPaint.
    /// </summary>
    public bool IsExpanded { get; private set; }

    public override bool HasChildren => Files.Any();
    public override string Name => FolderName;
    public override bool CanEdit => false;

    public PackageSubFolderNode(Package package, string folderName, List<string> files) : base()
    {
        Package = package;
        FolderName = folderName;
        Files = files;
        Value = this;
    }

    protected override void BuildChildren()
    {
        Clear();

        // Group by next level directory
        var subDirs = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
        var currentFiles = new List<string>();

        foreach (var file in Files)
        {
            // Remove the current folder prefix
            var relativePath = file;
            if (file.StartsWith(FolderName + "/", StringComparison.OrdinalIgnoreCase) ||
                file.StartsWith(FolderName + "\\", StringComparison.OrdinalIgnoreCase))
            {
                relativePath = file.Substring(FolderName.Length + 1);
            }

            var parts = relativePath.Split('/', '\\');
            if (parts.Length > 1)
            {
                var subDir = parts[0];
                if (!subDirs.ContainsKey(subDir))
                    subDirs[subDir] = new List<string>();
                subDirs[subDir].Add(file);
            }
            else
            {
                currentFiles.Add(file);
            }
        }

        // Add subdirectories
        foreach (var dir in subDirs.OrderBy(d => d.Key, StringComparer.OrdinalIgnoreCase))
        {
            AddItem(new PackageSubFolderNode(Package, FolderName + "/" + dir.Key, dir.Value));
        }

        // Add files
        foreach (var file in currentFiles.OrderBy(f => f, StringComparer.OrdinalIgnoreCase))
        {
            var fullPath = global::Editor.FileSystem.Cloud.GetFullPath(file);
            if (!string.IsNullOrEmpty(fullPath))
            {
                AddItem(new AssetFileNode(fullPath));
            }
        }
    }

    public override void OnPaint(VirtualWidget item)
    {
        // Track expanded state for external access
        IsExpanded = item.IsOpen;

        PaintSelection(item);

        var rect = item.Rect;

        Paint.SetPen(Theme.Yellow);
        Paint.DrawIcon(rect, item.IsOpen ? "folder_open" : "folder", 16, TextFlag.LeftCenter);

        rect.Left += 22;

        Paint.SetPen(Theme.Text);
        Paint.SetDefaultFont();

        // Show just the folder name, not full path
        var displayName = FolderName.Contains('/') ? FolderName.Split('/').Last() : FolderName;
        Paint.DrawText(rect, displayName, TextFlag.LeftCenter);
    }

    public bool MatchesFilter(string filter)
    {
        if (string.IsNullOrEmpty(filter))
            return true;

        if (FolderName.ToLowerInvariant().Contains(filter))
            return true;

        foreach (var file in Files)
        {
            if (file.ToLowerInvariant().Contains(filter))
                return true;
        }

        return false;
    }
}