Editor/AssetFolderNode.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 folder in the asset browser
/// </summary>
public class AssetFolderNode : TreeNode
{
    public string FullPath { 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; }

    /// <summary>
    /// Set to true to auto-expand this folder on first paint.
    /// </summary>
    public bool ShouldAutoExpand { get; set; }

    /// <summary>
    /// Dictionary of paths that should be auto-expanded, keyed by panel identifier.
    /// Used to persist across node rebuilds while keeping panels separate.
    /// </summary>
    public static Dictionary<object, HashSet<string>> PathsToAutoExpandByPanel { get; } = new();

    /// <summary>
    /// Static event fired when any folder's expanded state changes.
    /// Parameters: (panelKey, fullPath, isExpanded)
    /// panelKey is TreeView.Parent which identifies the panel
    /// </summary>
    public static event Action<object, string, bool> OnExpandedStateChanged;

    private FileSystemWatcher _watcher;
    private bool _isRoot;

    public override bool HasChildren => Directory.Exists(FullPath) &&
        (Directory.EnumerateDirectories(FullPath).Any() || Directory.EnumerateFiles(FullPath).Any());

    public override string Name => DisplayName;
    public override bool CanEdit => !_isRoot;

    public AssetFolderNode(string path, string displayName = null, string icon = "folder") : base()
    {
        FullPath = Path.GetFullPath(path);
        DisplayName = displayName ?? Path.GetFileName(path);
        IconName = icon;
        _isRoot = displayName != null;
        Value = this;

        // Watch for changes
        if (Directory.Exists(FullPath))
        {
            try
            {
                _watcher = new FileSystemWatcher(FullPath);
                _watcher.EnableRaisingEvents = true;
                _watcher.Created += OnFileSystemChanged;
                _watcher.Deleted += OnFileSystemChanged;
                _watcher.Renamed += OnFileSystemChanged;
            }
            catch
            {
                // Ignore watcher errors
            }
        }
    }

    ~AssetFolderNode()
    {
        _watcher?.Dispose();
    }

    private void OnFileSystemChanged(object sender, FileSystemEventArgs e)
    {
        MainThread.Queue(Dirty);
    }

    /// <summary>
    /// Public method to force building children
    /// </summary>
    public void EnsureChildrenBuilt()
    {
        if (Children == null || !Children.Any())
        {
            BuildChildren();
        }
    }


    protected override void BuildChildren()
    {
        Clear();

        if (!Directory.Exists(FullPath))
            return;

        try
        {
            // Add subdirectories first
            var directories = Directory.GetDirectories(FullPath)
                .OrderBy(d => Path.GetFileName(d), StringComparer.OrdinalIgnoreCase);

            foreach (var dir in directories)
            {
                var dirInfo = new DirectoryInfo(dir);

                // Skip hidden folders
                if (dirInfo.Attributes.HasFlag(FileAttributes.Hidden))
                    continue;

                // Skip obj folder in code directories
                if (dirInfo.Name.Equals("obj", StringComparison.OrdinalIgnoreCase))
                    continue;

                // Skip .sbox folder
                if (dirInfo.Name.StartsWith("."))
                    continue;

                AddItem(new AssetFolderNode(dir));
            }

            // Add files
            var files = Directory.GetFiles(FullPath)
                .OrderBy(f => Path.GetFileName(f), StringComparer.OrdinalIgnoreCase);

            foreach (var file in files)
            {
                var fileInfo = new FileInfo(file);

                // Skip hidden files
                if (fileInfo.Attributes.HasFlag(FileAttributes.Hidden))
                    continue;

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

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

                AddItem(new AssetFileNode(file));
            }
        }
        catch (Exception ex)
        {
            Log.Warning($"Error building folder tree: {ex.Message}");
        }
    }

    public override void OnPaint(VirtualWidget item)
    {
        // Auto-expand if requested (for restoring saved state)
        // Check both instance flag and panel-specific set
        var panelKey = TreeView?.Parent;
        HashSet<string> panelPaths = null;
        bool inPanelSet = panelKey != null &&
                         PathsToAutoExpandByPanel.TryGetValue(panelKey, out panelPaths) &&
                         panelPaths.Contains(FullPath);

        bool shouldExpand = ShouldAutoExpand || inPanelSet;
        if (shouldExpand)
        {
            ShouldAutoExpand = false;
            if (inPanelSet && panelPaths != null)
            {
                panelPaths.Remove(FullPath);
            }

            if (!item.IsOpen)
            {
                EnsureChildrenBuilt();
                var tv = TreeView;
                MainThread.Queue(() => tv?.Toggle(this));
            }
        }

        // Track expanded state and notify if changed
        if (IsExpanded != item.IsOpen)
        {
            IsExpanded = item.IsOpen;
            OnExpandedStateChanged?.Invoke(panelKey, FullPath, IsExpanded);
        }

        PaintSelection(item);

        var rect = item.Rect;

        // Folder icon
        Paint.SetPen(Theme.Yellow);
        var iconRect = Paint.DrawIcon(rect, item.IsOpen ? "folder_open" : IconName, 16, TextFlag.LeftCenter);

        rect.Left += 22;

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

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

        menu.AddOption("Open in Explorer", "folder_open", () =>
        {
            EditorUtility.OpenFolder(FullPath);
        });

        menu.AddSeparator();

        // Create submenu
        var createMenu = menu.AddMenu("Create", "add");
        AssetCreator.AddOptions(createMenu, FullPath);
        menu.AddSeparator();

        if (!_isRoot)
        {
            menu.AddOption("Rename", "edit", () =>
            {
                ShowRenameDialog();
            });
        }

        menu.AddOption("Copy Path", "content_copy", () =>
        {
            EditorUtility.Clipboard.Copy(FullPath);
        });

        menu.AddOption("Copy Relative Path", "content_copy", () =>
        {
            var relativePath = Path.GetRelativePath(Project.Current?.GetRootPath() ?? "", FullPath);
            EditorUtility.Clipboard.Copy(relativePath);
        });

        menu.AddSeparator();

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

        if (!_isRoot)
        {
            menu.AddSeparator();

            menu.AddOption("Delete", "delete", () =>
            {
                var confirm = new PopupWindow(
                    "Delete Folder",
                    $"Are you sure you want to delete '{DisplayName}'?\nAll contents will be deleted.",
                    "Cancel",
                    new Dictionary<string, Action>()
                    {
                        { "Delete", () =>
                            {
                                try
                                {
                                    Directory.Delete(FullPath, recursive: true);
                                    Parent?.Dirty();
                                }
                                catch (Exception ex)
                                {
                                    Log.Error($"Failed to delete folder: {ex.Message}");
                                }
                            }
                        }
                    }
                );
                confirm.Show();
            });
        }

        menu.OpenAtCursor();
        return true;
    }

    private void ShowRenameDialog()
    {
        var dialog = new RenameDialog("Rename Folder", DisplayName);
        dialog.OnConfirm = (newName) =>
        {
            if (string.IsNullOrWhiteSpace(newName) || newName == DisplayName)
                return;

            var newPath = Path.Combine(Path.GetDirectoryName(FullPath), newName);
            try
            {
                Directory.Move(FullPath, newPath);
                Parent?.Dirty();
            }
            catch (Exception ex)
            {
                Log.Error($"Failed to rename folder: {ex.Message}");
            }
        };
        dialog.Show();
    }


    public override void OnRename(VirtualWidget item, string text, List<TreeNode> selection = null)
    {
        if (string.IsNullOrWhiteSpace(text) || text == DisplayName)
            return;

        var newPath = Path.Combine(Path.GetDirectoryName(FullPath), text);
        try
        {
            Directory.Move(FullPath, newPath);
            Parent?.Dirty();
        }
        catch (Exception ex)
        {
            Log.Error($"Failed to rename folder: {ex.Message}");
        }
    }

    public override bool OnDragStart()
    {
        if (_isRoot)
            return false;

        var drag = new Drag(TreeView);
        drag.Data.Text = FullPath;
        drag.Data.Url = new Uri("file:///" + FullPath);
        drag.Execute();
        return true;
    }

    public override DropAction OnDragDrop(BaseItemWidget.ItemDragEvent e)
    {
        var dropAction = e.HasCtrl ? DropAction.Copy : DropAction.Move;

        // If not actually dropping, just return the action (for hover feedback)
        if (!e.IsDrop)
            return dropAction;

        // Check if any directories are being moved (not copied)
        var foldersToMove = new List<string>();
        var filesToProcess = new List<string>();

        foreach (var file in e.Data.Files)
        {
            if (file.ToLowerInvariant() == FullPath.ToLowerInvariant())
                continue;

            var fileName = Path.GetFileName(file);
            var destPath = Path.Combine(FullPath, fileName);

            if (Path.GetFullPath(file).Equals(Path.GetFullPath(destPath), StringComparison.OrdinalIgnoreCase))
                continue;

            if (Directory.Exists(file) && dropAction == DropAction.Move)
            {
                foldersToMove.Add(file);
            }
            else
            {
                filesToProcess.Add(file);
            }
        }

        // Process files immediately (no confirmation needed)
        ProcessFiles(filesToProcess, dropAction);

        // Show confirmation for folder moves
        if (foldersToMove.Count > 0)
        {
            var folderNames = string.Join(", ", foldersToMove.Select(Path.GetFileName));
            var message = foldersToMove.Count == 1
                ? $"Move folder \"{Path.GetFileName(foldersToMove[0])}\" to \"{DisplayName}\"?"
                : $"Move {foldersToMove.Count} folders to \"{DisplayName}\"?";

            var details = foldersToMove.Count == 1
                ? $"From: {Path.GetDirectoryName(foldersToMove[0])}\nTo: {FullPath}"
                : $"Folders: {folderNames}";

            var targetPath = FullPath;
            var folderNode = this;

            ConfirmationDialog.Show(
                "Move Folder",
                message,
                details,
                onConfirm: () =>
                {
                    foreach (var folder in foldersToMove)
                    {
                        try
                        {
                            var fileName = Path.GetFileName(folder);
                            var destPath = Path.Combine(targetPath, fileName);
                            Directory.Move(folder, destPath);
                        }
                        catch (Exception ex)
                        {
                            Log.Error($"Failed to move folder: {ex.Message}");
                        }
                    }
                    folderNode.Dirty();
                }
            );
        }
        else
        {
            // Refresh immediately if no folders to move
            Dirty();
        }

        return dropAction;
    }

    /// <summary>
    /// Process files and folders (copy operations or file moves)
    /// </summary>
    private void ProcessFiles(List<string> files, DropAction action)
    {
        foreach (var file in files)
        {
            try
            {
                var fileName = Path.GetFileName(file);
                var destPath = Path.Combine(FullPath, fileName);

                // Skip if same path
                if (Path.GetFullPath(file).Equals(Path.GetFullPath(destPath), StringComparison.OrdinalIgnoreCase))
                    continue;

                if (Directory.Exists(file))
                {
                    // It's a directory
                    if (action == DropAction.Copy)
                        CopyDirectory(file, destPath);
                    else
                    {
                        EditorUtility.RenameDirectory(file, destPath);
                    }
                }
                else if (File.Exists(file))
                {
                    // Check if it's a registered asset
                    var asset = AssetSystem.FindByPath(file);

                    if (asset != null && !asset.IsDeleted)
                    {
                        // Use EditorUtility for proper asset handling
                        if (action == DropAction.Copy)
                            EditorUtility.CopyAssetToDirectory(asset, FullPath);
                        else
                            EditorUtility.MoveAssetToDirectory(asset, FullPath);
                    }
                    else
                    {
                        // Regular file, not an asset
                        if (action == DropAction.Copy)
                            File.Copy(file, destPath);
                        else
                            File.Move(file, destPath);
                    }
                }
            }
            catch (Exception ex)
            {
                Log.Error($"Failed to {(action == DropAction.Copy ? "copy" : "move")}: {ex.Message}");
            }
        }
    }

    private static void CopyDirectory(string sourceDir, string destDir)
    {
        Directory.CreateDirectory(destDir);

        foreach (var file in Directory.GetFiles(sourceDir))
        {
            File.Copy(file, Path.Combine(destDir, Path.GetFileName(file)));
        }

        foreach (var dir in Directory.GetDirectories(sourceDir))
        {
            CopyDirectory(dir, Path.Combine(destDir, Path.GetFileName(dir)));
        }
    }

    /// <summary>
    /// Check if this folder or any of its contents matches the search filter.
    /// Scans filesystem directly to find matches in unopened folders.
    /// </summary>
    public bool MatchesFilter(string filter)
    {
        if (string.IsNullOrEmpty(filter))
            return true;

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

        // If children are already built, check them
        if (Children != null && Children.Any())
        {
            foreach (var child in Children)
            {
                if (child is AssetFolderNode folder && folder.MatchesFilter(filter))
                    return true;
                if (child is AssetFileNode file && file.MatchesFilter(filter))
                    return true;
            }
            return false;
        }

        // Otherwise scan filesystem directly (without creating node objects)
        return ScanDirectoryForFilter(FullPath, filter, maxDepth: 5);
    }

    /// <summary>
    /// Scan directory for filter match without creating node objects.
    /// Limited depth to prevent excessive recursion.
    /// </summary>
    private static bool ScanDirectoryForFilter(string path, string filter, int maxDepth)
    {
        if (maxDepth <= 0 || !Directory.Exists(path))
            return false;

        try
        {
            // Check files in this folder
            foreach (var file in Directory.EnumerateFiles(path))
            {
                var fileName = Path.GetFileName(file).ToLowerInvariant();
                if (fileName.Contains(filter))
                    return true;
            }

            // Check subfolders recursively
            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 folder name
                if (dirName.ToLowerInvariant().Contains(filter))
                    return true;

                // Recursively check contents (with depth limit)
                if (ScanDirectoryForFilter(dir, filter, maxDepth - 1))
                    return true;
            }
        }
        catch
        {
            // Ignore filesystem errors
        }

        return false;
    }

    /// <summary>
    /// Find a node by its full path
    /// </summary>
    public TreeNode FindNode(string path)
    {
        if (Path.GetFullPath(FullPath).Equals(path, StringComparison.OrdinalIgnoreCase))
            return this;

        // Build children if not built yet
        if (Children == null || !Children.Any())
            BuildChildren();

        foreach (var child in Children)
        {
            if (child is AssetFolderNode folder)
            {
                if (path.StartsWith(folder.FullPath, StringComparison.OrdinalIgnoreCase))
                {
                    var result = folder.FindNode(path);
                    if (result != null)
                        return result;
                }
            }
            else if (child is AssetFileNode file)
            {
                if (Path.GetFullPath(file.FullPath).Equals(path, StringComparison.OrdinalIgnoreCase))
                    return file;
            }
        }

        return null;
    }

    protected override bool HasDescendant(object obj)
    {
        if (obj is AssetFileNode fileNode)
        {
            return fileNode.FullPath.StartsWith(FullPath, StringComparison.OrdinalIgnoreCase);
        }
        if (obj is AssetFolderNode folderNode)
        {
            return folderNode.FullPath.StartsWith(FullPath, StringComparison.OrdinalIgnoreCase);
        }
        return false;
    }
}

/// <summary>
/// Simple popup for renaming files/folders
/// </summary>
internal class RenameDialog : PopupWidget
{
    private LineEdit _lineEdit;
    public Action<string> OnConfirm;

    public RenameDialog(string title, string initialText) : base(null)
    {
        Layout = Layout.Row();
        Layout.Margin = 4;
        Layout.Spacing = 4;

        _lineEdit = Layout.Add(new LineEdit(this));
        _lineEdit.Text = initialText;
        _lineEdit.MinimumWidth = 200;
        _lineEdit.SelectAll();
        _lineEdit.ReturnPressed += () =>
        {
            OnConfirm?.Invoke(_lineEdit.Text);
            Close();
        };

        var okBtn = Layout.Add(new Button("OK", this));
        okBtn.Clicked = () =>
        {
            OnConfirm?.Invoke(_lineEdit.Text);
            Close();
        };

        _lineEdit.Focus();
    }

    public new void Show()
    {
        OpenAtCursor();
        _lineEdit.Focus();
    }
}

/// <summary>
/// Confirmation dialog for critical actions
/// </summary>
internal class ConfirmationDialog : Dialog
{
    public Action OnConfirm;
    public Action OnCancel;

    private Label _messageLabel;
    private Label _detailsLabel;

    public ConfirmationDialog(string title, string message, string details = null, string confirmText = "Confirm", string cancelText = "Cancel") : base(null)
    {
        Window.WindowTitle = title;
        Window.Size = new Vector2(400, 180);
        Window.MinimumSize = new Vector2(350, 150);

        Layout = Layout.Column();
        Layout.Margin = 16;
        Layout.Spacing = 12;

        // Warning icon and message
        var headerRow = Layout.AddRow();
        headerRow.Spacing = 12;

        var iconLabel = headerRow.Add(new Label("⚠️", this));
        iconLabel.SetStyles("font-size: 24px;");

        var textColumn = headerRow.AddColumn();
        textColumn.Spacing = 4;

        _messageLabel = textColumn.Add(new Label(message, this));
        _messageLabel.SetStyles("font-size: 13px; font-weight: 600;");
        _messageLabel.WordWrap = true;

        if (!string.IsNullOrEmpty(details))
        {
            _detailsLabel = textColumn.Add(new Label(details, this));
            _detailsLabel.SetStyles("font-size: 11px; color: #aaa;");
            _detailsLabel.WordWrap = true;
        }

        headerRow.AddStretchCell();

        Layout.AddStretchCell();

        // Buttons
        var buttonRow = Layout.AddRow();
        buttonRow.Spacing = 8;
        buttonRow.AddStretchCell();

        var cancelBtn = buttonRow.Add(new Button(cancelText, this));
        cancelBtn.MinimumWidth = 80;
        cancelBtn.Clicked = () =>
        {
            OnCancel?.Invoke();
            Close();
        };

        var confirmBtn = buttonRow.Add(new Button.Primary(confirmText, this));
        confirmBtn.MinimumWidth = 80;
        confirmBtn.Clicked = () =>
        {
            OnConfirm?.Invoke();
            Close();
        };
    }

    /// <summary>
    /// Show confirmation dialog and execute action if confirmed
    /// </summary>
    public static void Show(string title, string message, string details, Action onConfirm, Action onCancel = null)
    {
        var dialog = new ConfirmationDialog(title, message, details);
        dialog.OnConfirm = onConfirm;
        dialog.OnCancel = onCancel;
        dialog.Show();
    }
}