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

namespace GeneralGame.Editor;

/// <summary>
/// Individual asset browser panel with its own toolbar and tree view.
/// Multiple panels can be displayed side by side in TreeAssetBrowser.
/// </summary>
public class AssetBrowserPanel : Widget, IBrowserPanel
{
    private AssetTreeView _treeView;
    private LineEdit _searchEdit;
    private IconButton _searchBtn;
    private IconButton _clearSearchBtn;
    private string _searchFilter = "";
    private Widget _toolbar;
    private IconButton _closeBtn;
    private IconButton _moveLeftBtn;
    private IconButton _moveRightBtn;
    private IconButton _expandAllBtn;
    private IconButton _collapseAllBtn;

    private string _assetsPath;
    private string _codePath;
    private string _editorPath;

    // Unique panel ID for this panel instance
    private string _panelId;

    // Store expanded folder paths to restore state after refresh/search
    private HashSet<string> _expandedPaths = new();
    private HashSet<string> _preSearchExpandedPaths = new();
    private bool _isSearchActive = false;

    // Cache of paths that match the current search filter (computed once)
    private HashSet<string> _matchingPaths = new();

    // Cookie key for saving expanded state between sessions (unique per panel)
    private string ExpandedPathsCookieKey => $"TreeAssetBrowser.{_panelId}.ExpandedPaths";

    /// <summary>
    /// Callback when user requests to close this panel
    /// </summary>
    public Action OnCloseRequested { get; set; }

    /// <summary>
    /// Callback when user requests to move this panel left
    /// </summary>
    public Action OnMoveLeftRequested { get; set; }

    /// <summary>
    /// Callback when user requests to move this panel right
    /// </summary>
    public Action OnMoveRightRequested { get; set; }

    /// <summary>
    /// Selected asset callback
    /// </summary>
    public Action<Asset> OnAssetSelected;
    public Action<string> OnFileSelected;
    public Action<string> OnFolderSelected;

    /// <summary>
    /// Called when a folder is clicked (for syncing with IconGridPanel)
    /// </summary>
    public Action<string> OnFolderClicked;

    /// <summary>
    /// Controls visibility of close button (hide if this is the only panel)
    /// </summary>
    public bool ShowCloseButton
    {
        get => _closeBtn?.Visible ?? false;
        set
        {
            if (_closeBtn != null)
                _closeBtn.Visible = value;
        }
    }

    /// <summary>
    /// Controls visibility of move left button
    /// </summary>
    public bool ShowMoveLeftButton
    {
        get => _moveLeftBtn?.Visible ?? false;
        set
        {
            if (_moveLeftBtn != null)
                _moveLeftBtn.Visible = value;
        }
    }

    /// <summary>
    /// Controls visibility of move right button
    /// </summary>
    public bool ShowMoveRightButton
    {
        get => _moveRightBtn?.Visible ?? false;
        set
        {
            if (_moveRightBtn != null)
                _moveRightBtn.Visible = value;
        }
    }

    public AssetBrowserPanel(Widget parent, string panelId = null) : base(parent)
    {
        // Use provided panel ID or generate a default one
        _panelId = panelId ?? $"Default_{Guid.NewGuid():N}";

        SetSizeMode(SizeMode.CanGrow, SizeMode.CanGrow);

        _assetsPath = Project.Current?.GetAssetsPath();
        _codePath = Project.Current?.GetCodePath();

        var rootPath = Project.Current?.GetRootPath();
        if (!string.IsNullOrEmpty(rootPath))
        {
            _editorPath = Path.Combine(rootPath, "Editor");
        }

        // Load saved expanded state from previous session
        LoadExpandedState();
        _lastSavedExpandedPaths = new HashSet<string>(_expandedPaths);

        // Subscribe to folder state changes
        AssetFolderNode.OnExpandedStateChanged += OnFolderExpandedStateChanged;

        CreateUI();
        RefreshTree();
    }

    public override void OnDestroyed()
    {
        base.OnDestroyed();
        AssetFolderNode.OnExpandedStateChanged -= OnFolderExpandedStateChanged;
        AssetFolderNode.PathsToAutoExpandByPanel.Remove(this);
    }

    private void OnFolderExpandedStateChanged(object panelKey, string fullPath, bool isExpanded)
    {
        // Only handle events from this panel's tree view
        if (panelKey != this)
            return;

        if (isExpanded)
        {
            _expandedPaths.Add(fullPath);
        }
        else
        {
            _expandedPaths.Remove(fullPath);
        }
    }

    private void CreateUI()
    {
        Layout = Layout.Column();
        Layout.Spacing = 4;
        Layout.Margin = 4;

        // Toolbar
        _toolbar = Layout.Add(new Widget(this));
        _toolbar.Layout = Layout.Row();
        _toolbar.Layout.Spacing = 4;
        _toolbar.FixedHeight = 28;

        // Search input
        _searchEdit = _toolbar.Layout.Add(new LineEdit(this));
        _searchEdit.PlaceholderText = "Search...";
        _searchEdit.TextEdited += OnSearchTextChanged;
        _searchEdit.ReturnPressed += () => DoSearch();

        // Search button
        _searchBtn = _toolbar.Layout.Add(new IconButton("search"));
        _searchBtn.ToolTip = "Search";
        _searchBtn.Background = Color.Transparent;
        _searchBtn.OnClick = () => DoSearch();

        // Clear search button
        _clearSearchBtn = _toolbar.Layout.Add(new IconButton("close"));
        _clearSearchBtn.ToolTip = "Clear Search";
        _clearSearchBtn.Background = Color.Transparent;
        _clearSearchBtn.OnClick = ClearSearch;
        _clearSearchBtn.Visible = false;

        // Refresh button
        var refreshBtn = _toolbar.Layout.Add(new IconButton("refresh"));
        refreshBtn.ToolTip = "Refresh";
        refreshBtn.Background = Color.Transparent;
        refreshBtn.OnClick = RefreshTree;

        // Expand all button (only visible during search)
        _expandAllBtn = _toolbar.Layout.Add(new IconButton("unfold_more"));
        _expandAllBtn.ToolTip = "Expand All";
        _expandAllBtn.Background = Color.Transparent;
        _expandAllBtn.OnClick = ExpandAll;
        _expandAllBtn.Visible = false;

        // Collapse all button (only visible during search)
        _collapseAllBtn = _toolbar.Layout.Add(new IconButton("unfold_less"));
        _collapseAllBtn.ToolTip = "Collapse All";
        _collapseAllBtn.Background = Color.Transparent;
        _collapseAllBtn.OnClick = CollapseAll;
        _collapseAllBtn.Visible = false;

        // Move left button
        _moveLeftBtn = _toolbar.Layout.Add(new IconButton("chevron_left"));
        _moveLeftBtn.ToolTip = "Move Panel Left";
        _moveLeftBtn.Background = Color.Transparent;
        _moveLeftBtn.OnClick = () => OnMoveLeftRequested?.Invoke();
        _moveLeftBtn.Visible = false;

        // Move right button
        _moveRightBtn = _toolbar.Layout.Add(new IconButton("chevron_right"));
        _moveRightBtn.ToolTip = "Move Panel Right";
        _moveRightBtn.Background = Color.Transparent;
        _moveRightBtn.OnClick = () => OnMoveRightRequested?.Invoke();
        _moveRightBtn.Visible = false;

        // Close button
        _closeBtn = _toolbar.Layout.Add(new IconButton("close"));
        _closeBtn.ToolTip = "Close Panel";
        _closeBtn.Background = Color.Transparent;
        _closeBtn.OnClick = () => OnCloseRequested?.Invoke();
        _closeBtn.Visible = false; // Hidden by default, shown when multiple panels exist

        // Tree view with drag-drop support
        _treeView = Layout.Add(new AssetTreeView(this));
        _treeView.SetSizeMode(SizeMode.CanGrow, SizeMode.CanGrow);
        _treeView.MultiSelect = false;
        _treeView.ItemSpacing = 1;
        _treeView.IndentWidth = 16;

        _treeView.ItemActivated += OnItemActivated;
        _treeView.OnSelectionChanged += OnSelectionChanged;
    }

    private void OnSearchTextChanged(string text)
    {
        // Save expanded state when user starts typing (before any search)
        if (!string.IsNullOrEmpty(text) && !_isSearchActive)
        {
            _preSearchExpandedPaths = GetExpandedPaths();
            _isSearchActive = true;
        }
    }

    private void DoSearch()
    {
        var text = _searchEdit.Text;
        _searchFilter = text?.ToLowerInvariant() ?? "";

        if (string.IsNullOrEmpty(_searchFilter))
        {
            ClearSearch();
            return;
        }

        // Ensure expanded state is saved (in case search was triggered without typing)
        if (!_isSearchActive)
        {
            _preSearchExpandedPaths = GetExpandedPaths();
            _isSearchActive = true;
        }

        _clearSearchBtn.Visible = true;
        _expandAllBtn.Visible = true;
        _collapseAllBtn.Visible = true;
        ApplySearchFilter();
    }

    private void ApplySearchFilter()
    {
        // Pre-compute matching paths by scanning filesystem (done ONCE)
        _matchingPaths.Clear();

        // Scan Assets folder
        if (!string.IsNullOrEmpty(_assetsPath) && Directory.Exists(_assetsPath))
        {
            ScanDirectoryForMatches(_assetsPath, _searchFilter);
        }

        // Scan Code folder
        if (!string.IsNullOrEmpty(_codePath) && Directory.Exists(_codePath))
        {
            ScanDirectoryForMatches(_codePath, _searchFilter);
        }

        // Scan Editor folder
        if (!string.IsNullOrEmpty(_editorPath) && Directory.Exists(_editorPath))
        {
            ScanDirectoryForMatches(_editorPath, _searchFilter);
        }

        // Scan Libraries (other projects in the workspace)
        foreach (var project in EditorUtility.Projects.GetAll())
        {
            if (project == Project.Current || project.Config.Ident == "menu")
                continue;

            var projectRoot = project.GetRootPath();
            if (!string.IsNullOrEmpty(projectRoot) && Directory.Exists(projectRoot))
            {
                ScanDirectoryForMatches(projectRoot, _searchFilter);
            }
        }

        // Scan Core folder
        var corePath = global::Editor.FileSystem.Root.GetFullPath("/core/");
        if (!string.IsNullOrEmpty(corePath) && Directory.Exists(corePath))
        {
            ScanDirectoryForMatches(corePath, _searchFilter);
        }

        // Scan Citizen folder
        var citizenPath = global::Editor.FileSystem.Root.GetFullPath("/addons/citizen/assets/");
        if (!string.IsNullOrEmpty(citizenPath) && Directory.Exists(citizenPath))
        {
            ScanDirectoryForMatches(citizenPath, _searchFilter);
        }

        // Set filter to check the pre-computed cache (fast O(1) lookup)
        _treeView.ShouldDisplayChild = (obj) =>
        {
            if (obj is AssetFolderNode folder)
                return _matchingPaths.Contains(folder.FullPath);
            if (obj is AssetFileNode file)
                return _matchingPaths.Contains(file.FullPath);
            // Headers and other nodes - always show
            return true;
        };

        // Clear and rebuild tree
        _treeView.Clear();
        BuildTreeItems();
        _treeView.Update();
    }

    /// <summary>
    /// Scan directory and add all matching file/folder paths to cache.
    /// Also adds parent folder paths so they remain visible.
    /// </summary>
    private void ScanDirectoryForMatches(string path, string filter, int maxDepth = 15)
    {
        if (maxDepth <= 0 || !Directory.Exists(path))
            return;

        try
        {
            bool hasMatchingChild = false;

            // Check files
            foreach (var file in Directory.EnumerateFiles(path))
            {
                var fileName = Path.GetFileName(file);

                // Skip hidden/generated files
                if (fileName.StartsWith(".") ||
                    fileName.Contains(".generated", StringComparison.OrdinalIgnoreCase) ||
                    fileName.EndsWith(".meta", StringComparison.OrdinalIgnoreCase))
                    continue;

                // Skip compiled assets only if source exists
                if (fileName.EndsWith("_c"))
                {
                    var sourcePath = file[..^2]; // Remove _c
                    if (File.Exists(sourcePath))
                        continue;
                }

                if (fileName.ToLowerInvariant().Contains(filter))
                {
                    _matchingPaths.Add(Path.GetFullPath(file));
                    hasMatchingChild = true;
                }
            }

            // Check subdirectories
            foreach (var dir in Directory.EnumerateDirectories(path))
            {
                var dirName = Path.GetFileName(dir);

                // Skip hidden/system folders
                if (dirName.StartsWith(".") || dirName.Equals("obj", StringComparison.OrdinalIgnoreCase))
                    continue;

                // Check if folder name matches
                if (dirName.ToLowerInvariant().Contains(filter))
                {
                    _matchingPaths.Add(Path.GetFullPath(dir));
                    hasMatchingChild = true;
                }

                // Recursively scan subdirectory
                int countBefore = _matchingPaths.Count;
                ScanDirectoryForMatches(dir, filter, maxDepth - 1);

                // If subdirectory had matches, add it to visible paths
                if (_matchingPaths.Count > countBefore)
                {
                    _matchingPaths.Add(Path.GetFullPath(dir));
                    hasMatchingChild = true;
                }
            }

            // If this folder has matching children, make sure it's visible
            if (hasMatchingChild)
            {
                _matchingPaths.Add(Path.GetFullPath(path));
            }
        }
        catch
        {
            // Ignore filesystem errors
        }
    }

    private void ClearSearch()
    {
        _searchEdit.Text = "";
        _searchFilter = "";
        _treeView.ShouldDisplayChild = null;
        _matchingPaths.Clear();
        _clearSearchBtn.Visible = false;
        _expandAllBtn.Visible = false;
        _collapseAllBtn.Visible = false;

        // Restore expanded state from before search
        if (_isSearchActive)
        {
            _isSearchActive = false;

            // Rebuild tree without filter, then restore expanded state
            _treeView.Clear();
            BuildTreeItems();
            RestoreExpandedPaths(_preSearchExpandedPaths);
        }

        _treeView.Update();
    }

    private HashSet<string> GetExpandedPaths()
    {
        // Return copy of current tracked expanded paths
        return new HashSet<string>(_expandedPaths);
    }

    private void SaveExpandedState()
    {
        // Save expanded paths to ProjectCookie for persistence between sessions
        var pathsList = _expandedPaths.ToList();
        ProjectCookie.Set(ExpandedPathsCookieKey, pathsList);
    }

    private void LoadExpandedState()
    {
        // Load expanded paths from ProjectCookie
        var pathsList = ProjectCookie.Get<List<string>>(ExpandedPathsCookieKey, new List<string>());
        _expandedPaths = new HashSet<string>(pathsList);
    }

    private void RestoreExpandedPaths(HashSet<string> paths)
    {
        _expandedPaths = new HashSet<string>(paths);

        // Create panel-specific auto-expand set (keyed by this panel instance)
        if (!AssetFolderNode.PathsToAutoExpandByPanel.ContainsKey(this))
        {
            AssetFolderNode.PathsToAutoExpandByPanel[this] = new HashSet<string>();
        }
        else
        {
            AssetFolderNode.PathsToAutoExpandByPanel[this].Clear();
        }

        RestoreExpandedPathsRecursive(_treeView.Items, paths, 0);
    }

    private void RestoreExpandedPathsRecursive(IEnumerable<object> items, HashSet<string> paths, int depth)
    {
        foreach (var item in items)
        {
            if (item is AssetFolderNode folder)
            {
                bool shouldExpand = paths.Contains(folder.FullPath);
                bool hasChildToExpand = !shouldExpand && paths.Any(p =>
                    p.StartsWith(folder.FullPath + Path.DirectorySeparatorChar) ||
                    p.StartsWith(folder.FullPath + "/"));

                if (shouldExpand || hasChildToExpand)
                {
                    // Add to panel-specific auto-expand set
                    if (AssetFolderNode.PathsToAutoExpandByPanel.TryGetValue(this, out var panelPaths))
                    {
                        panelPaths.Add(folder.FullPath);
                    }
                    folder.EnsureChildrenBuilt();
                    if (folder.Children != null)
                    {
                        RestoreExpandedPathsRecursive(folder.Children, paths, depth + 1);
                    }
                }
            }
            else if (item is PackageFolderNode packageFolder)
            {
                var packagePath = $"__package__{packageFolder.Package.FullIdent}";
                if (paths.Contains(packagePath))
                {
                    _treeView.Open(packageFolder);
                    if (packageFolder.Children != null)
                    {
                        RestoreExpandedPathsRecursive(packageFolder.Children, paths, depth + 1);
                    }
                }
            }
            else if (item is PackageSubFolderNode packageSubFolder)
            {
                var subPath = $"__packagesub__{packageSubFolder.Package.FullIdent}__{packageSubFolder.FolderName}";
                if (paths.Contains(subPath))
                {
                    _treeView.Open(packageSubFolder);
                    if (packageSubFolder.Children != null)
                    {
                        RestoreExpandedPathsRecursive(packageSubFolder.Children, paths, depth + 1);
                    }
                }
            }
            else if (item is TreeNode node)
            {
                var headerPath = $"__header__{node.Name}";
                if (paths.Contains(headerPath))
                {
                    _treeView.Open(node);
                }
                // Always check children of headers (they contain folders)
                if (node.Children != null)
                {
                    RestoreExpandedPathsRecursive(node.Children, paths, depth + 1);
                }
            }
        }
    }

    public void RefreshTree()
    {
        // Save expanded state before refresh
        var savedPaths = GetExpandedPaths();
        bool hadSavedPaths = savedPaths.Count > 0;

        _treeView.Clear();
        BuildTreeItems(hadSavedPaths);

        // Restore expanded state - folders will auto-expand on first paint
        if (hadSavedPaths)
        {
            RestoreExpandedPaths(savedPaths);
        }

        _treeView.Update();
    }

    private void BuildTreeItems(bool skipDefaultOpen = true)
    {
        // Add Assets folder
        if (!string.IsNullOrEmpty(_assetsPath) && Directory.Exists(_assetsPath))
        {
            var assetsNode = new AssetFolderNode(_assetsPath, "Assets", "folder_special");
            _treeView.AddItem(assetsNode);

            // Default open Assets on first load
            if (!skipDefaultOpen)
            {
                _treeView.Open(assetsNode);
                _expandedPaths.Add(assetsNode.FullPath);
            }
        }

        // Add Code folder
        if (!string.IsNullOrEmpty(_codePath) && Directory.Exists(_codePath))
        {
            var codeNode = new AssetFolderNode(_codePath, "Code", "code");
            _treeView.AddItem(codeNode);
        }

        // Add Editor folder
        if (!string.IsNullOrEmpty(_editorPath) && Directory.Exists(_editorPath))
        {
            var editorNode = new AssetFolderNode(_editorPath, "Editor", "edit");
            _treeView.AddItem(editorNode);
        }

        // Add Libraries section - shows all other projects in the workspace
        var libraryProjects = EditorUtility.Projects.GetAll()
            .Where(p => p != Project.Current && p.Config.Ident != "menu")
            .OrderBy(p => p.Config.Title)
            .ToList();

        {
            var librariesHeader = new TreeNode.SmallHeader("class", "Libraries");
            var addedCount = 0;

            foreach (var project in libraryProjects)
            {
                var projectRoot = project.GetRootPath();
                if (!string.IsNullOrEmpty(projectRoot) && Directory.Exists(projectRoot))
                {
                    var title = !string.IsNullOrEmpty(project.Config.Title) ? project.Config.Title : project.Config.Ident;
                    var projectNode = new AssetFolderNode(projectRoot, title, "inventory_2");
                    librariesHeader.AddItem(projectNode);
                    addedCount++;
                }
            }

            if (addedCount > 0)
            {
                _treeView.AddItem(librariesHeader);
            }
        }

        // Add Parent Package if this is an addon with a parent (e.g. sandbox gamemode)
        var parentPackageIdent = Project.Current?.Config?.GetMetaOrDefault("ParentPackage", "");
        if (Project.Current?.Config?.Type == "addon" &&
            !string.IsNullOrWhiteSpace(parentPackageIdent) &&
            Package.TryGetCached(parentPackageIdent, out Package parentPackage))
        {
            var parentHeader = new TreeNode.SmallHeader("cloud", "Parent");
            _treeView.AddItem(parentHeader);

            var packageNode = new PackageFolderNode(parentPackage, "supervisor_account");
            parentHeader.AddItem(packageNode);

            // Default open on first load
            if (!skipDefaultOpen)
            {
                _treeView.Open(parentHeader);
                _expandedPaths.Add("__header__Parent");
            }
        }

        // Add s&box section header with Core engine assets
        var sboxHeader = new TreeNode.SmallHeader("dns", "s&box");
        _treeView.AddItem(sboxHeader);

        // Add Core engine assets folder
        var corePath = global::Editor.FileSystem.Root.GetFullPath("/core/");
        if (!string.IsNullOrEmpty(corePath) && Directory.Exists(corePath))
        {
            var coreNode = new AssetFolderNode(corePath, "Core", "folder");
            sboxHeader.AddItem(coreNode);
        }

        // Add Citizen assets folder (optional DLC content)
        var citizenPath = global::Editor.FileSystem.Root.GetFullPath("/addons/citizen/assets/");
        if (!string.IsNullOrEmpty(citizenPath) && Directory.Exists(citizenPath))
        {
            var citizenNode = new AssetFolderNode(citizenPath, "Citizen", "accessibility_new");
            sboxHeader.AddItem(citizenNode);
        }

        // Default open s&box on first load
        if (!skipDefaultOpen)
        {
            _treeView.Open(sboxHeader);
            _expandedPaths.Add("__header__s&box");
        }
    }

    private void ExpandAll()
    {
        foreach (var item in _treeView.Items)
        {
            if (item is TreeNode node)
                ExpandNodeRecursive(node);
        }
        _treeView.Update();
    }

    private void ExpandNodeRecursive(TreeNode node)
    {
        // Handle different node types
        if (node is AssetFolderNode folder)
        {
            folder.EnsureChildrenBuilt();
            _treeView.Open(folder);

            foreach (var child in folder.Children)
            {
                if (child is TreeNode childNode)
                    ExpandNodeRecursive(childNode);
            }
        }
        else if (node is PackageFolderNode packageFolder)
        {
            _treeView.Open(packageFolder);

            if (packageFolder.Children != null)
            {
                foreach (var child in packageFolder.Children)
                {
                    if (child is TreeNode childNode)
                        ExpandNodeRecursive(childNode);
                }
            }
        }
        else if (node is PackageSubFolderNode packageSubFolder)
        {
            _treeView.Open(packageSubFolder);

            if (packageSubFolder.Children != null)
            {
                foreach (var child in packageSubFolder.Children)
                {
                    if (child is TreeNode childNode)
                        ExpandNodeRecursive(childNode);
                }
            }
        }
        else if (node.Children != null)
        {
            // For headers and other generic nodes with children
            _treeView.Open(node);

            foreach (var child in node.Children)
            {
                if (child is TreeNode childNode)
                    ExpandNodeRecursive(childNode);
            }
        }
    }

    private void CollapseAll()
    {
        foreach (var item in _treeView.Items)
        {
            if (item is TreeNode node)
                CollapseNodeRecursive(node);
        }
        _treeView.Update();
    }

    private void CollapseNodeRecursive(TreeNode node)
    {
        // Collapse children first (depth-first)
        if (node.Children != null)
        {
            foreach (var child in node.Children)
            {
                if (child is TreeNode childNode)
                    CollapseNodeRecursive(childNode);
            }
        }

        // Then collapse this node
        _treeView.Close(node);
    }

    private void OnItemActivated(object item)
    {
        if (item is AssetFileNode fileNode)
        {
            var asset = AssetSystem.FindByPath(fileNode.FullPath);
            if (asset != null)
            {
                OnAssetSelected?.Invoke(asset);
                asset.OpenInEditor();
            }
            else
            {
                OnFileSelected?.Invoke(fileNode.FullPath);
                EditorUtility.OpenFolder(fileNode.FullPath);
            }
        }
        // Folder and header toggle is handled automatically by TreeView
        // State tracking is done via OnFrame periodic sync
    }

    private void OnSelectionChanged(object[] items)
    {
        var item = items.FirstOrDefault();

        if (item is AssetFileNode fileNode)
        {
            var asset = AssetSystem.FindByPath(fileNode.FullPath);
            if (asset != null)
            {
                EditorUtility.InspectorObject = asset;
            }
        }
        else if (item is AssetFolderNode folderNode)
        {
            OnFolderSelected?.Invoke(folderNode.FullPath);
            OnFolderClicked?.Invoke(folderNode.FullPath);
        }
    }

    private HashSet<string> _lastSavedExpandedPaths = new();
    private float _saveTimer = 0;

    [EditorEvent.Frame]
    public void OnFrame()
    {
        // Periodically save if state changed
        _saveTimer += Time.Delta;
        if (_saveTimer >= 2.0f)
        {
            _saveTimer = 0;

            if (!_expandedPaths.SetEquals(_lastSavedExpandedPaths))
            {
                SaveExpandedState();
                _lastSavedExpandedPaths = new HashSet<string>(_expandedPaths);
            }
        }
    }


    /// <summary>
    /// Navigate to and select a specific file in the tree
    /// </summary>
    public void NavigateToFile(string path)
    {
        var normalizedPath = Path.GetFullPath(path);

        foreach (var rootItem in _treeView.Items)
        {
            if (rootItem is AssetFolderNode rootFolder)
            {
                var node = rootFolder.FindNode(normalizedPath);
                if (node != null)
                {
                    _treeView.ExpandPathTo(node);
                    _treeView.SelectItem(node);
                    _treeView.ScrollTo(node);
                    break;
                }
            }
        }
    }
}

/// <summary>
/// Custom TreeView with drag-drop support for asset folders
/// </summary>
internal class AssetTreeView : TreeView
{
    public AssetTreeView(Widget parent) : base(parent)
    {
        AcceptDrops = true;
    }
}