Editor/ConnecterBrowserDock.cs
using Sandbox;
using Sandbox.UI;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;

namespace Editor;

[Dock( "Editor", "Connecter Browser", "perm_media" )]
public sealed class ConnecterBrowserDock : Widget
{
	private ConnecterWorkspace Workspace;
	private ConnecterRepository CurrentRepository;
	private string CurrentFolder;
	private ConnecterBrowserMode CurrentMode = ConnecterBrowserMode.Files;
	private ConnecterBrowserFilter CurrentFilter = ConnecterBrowserFilter.All;
	private AssetListViewMode CurrentViewMode = AssetListViewMode.List;

	private TreeView LocationTree;
	private ListView AssetList;
	private LineEdit WorkspaceText;
	private LineEdit SearchText;
	private Label StatusLabel;
	private Button ImportButton;
	private ToolButton ModeButton;
	private ToolButton ViewModeButton;
	private ToolButton FilterButton;

	private CancellationTokenSource ScanCancellation;
	private IReadOnlyList<ConnecterAssetRecord> CurrentItems = [];
	private readonly Dictionary<string, Pixmap> ImagePreviewCache = new( StringComparer.OrdinalIgnoreCase );
	private readonly HashSet<string> FailedImagePreviews = new( StringComparer.OrdinalIgnoreCase );

	public ConnecterBrowserDock( Widget parent ) : base( parent )
	{
		WindowTitle = "Connecter Browser";
		SetWindowIcon( "perm_media" );
		MinimumSize = new Vector2( 300, 180 );

		Layout = Layout.Column();
		Layout.Spacing = 4;

		BuildToolbar();
		BuildWorkspaceBar();
		BuildBody();

		StatusLabel = Layout.Add( new Label( this ) { Text = "Loading Connecter workspace..." } );
		StatusLabel.MinimumHeight = Theme.RowHeight;

		LoadWorkspace();
	}

	private void BuildToolbar()
	{
		var toolbar = Layout.AddRow();
		toolbar.Spacing = 4;

		ModeButton = toolbar.Add( new ToolButton( "Browser Mode", "folder_open", this ) );
		ModeButton.MouseLeftPress = OpenModeMenu;

		SearchText = toolbar.Add( new LineEdit(), 1 );
		SearchText.PlaceholderText = "Search Connecter assets";
		SearchText.TextChanged += _ => RefreshAssetList();

		toolbar.Add( new ToolButton( "Clear Search", "clear", this )
		{
			MouseLeftPress = () =>
			{
				SearchText.Text = string.Empty;
				RefreshAssetList();
			}
		} );

		toolbar.Add( new ToolButton( "Refresh", "refresh", this )
		{
			MouseLeftPress = LoadWorkspace
		} );

		FilterButton = toolbar.Add( new ToolButton( "Filter", "filter_list", this ) );
		FilterButton.MouseLeftPress = OpenFilterMenu;

		ViewModeButton = toolbar.Add( new ToolButton( "View Mode", "view_headline", this ) );
		ViewModeButton.MouseLeftPress = OpenViewModeMenu;

		ImportButton = toolbar.Add( new Button.Primary( "Import Selected", "download", this ) );
		ImportButton.Clicked = () => ImportSelected();
	}

	private void BuildWorkspaceBar()
	{
		var workspaceRow = Layout.AddRow();
		workspaceRow.Spacing = 4;

		workspaceRow.Add( new Label( "Workspace", this ) { MinimumWidth = 74 } );

		WorkspaceText = workspaceRow.Add( new LineEdit(), 1 );
		WorkspaceText.PlaceholderText = "Auto-detect Connecter workspace";

		var savedWorkspacePath = ConnecterWorkspaceReader.GetSavedWorkspacePath();
		if ( ConnecterWorkspaceReader.IsConnecterWorkspacePath( savedWorkspacePath ) )
		{
			WorkspaceText.Text = savedWorkspacePath;
		}

		workspaceRow.Add( new ToolButton( "Use This Workspace Path", "done", this )
		{
			MouseLeftPress = ApplyManualWorkspacePath
		} );

		workspaceRow.Add( new ToolButton( "Auto Detect Workspace", "travel_explore", this )
		{
			MouseLeftPress = AutoDetectWorkspace
		} );

		workspaceRow.Add( new ToolButton( "Workspace Options", "more_vert", this )
		{
			MouseLeftPress = OpenWorkspaceMenu
		} );
	}

	private void BuildBody()
	{
		var splitter = new Splitter( this )
		{
			IsHorizontal = true
		};

		LocationTree = new TreeView( splitter );
		LocationTree.ItemSelected += OnLocationSelected;

		AssetList = new ListView( splitter );
		AssetList.MultiSelect = true;
		AssetList.ItemSelected += _ => UpdateImportButton();
		AssetList.OnSelectionChanged += _ => UpdateImportButton();
		AssetList.ItemActivated += OnAssetActivated;
		AssetList.ItemContextMenu = OpenAssetContextMenu;
		AssetList.ItemDrag = StartAssetDrag;
		AssetList.Margin = new Margin( 4 );

		splitter.AddWidget( LocationTree );
		splitter.SetStretch( 0, 1 );
		splitter.AddWidget( AssetList );
		splitter.SetStretch( 1, 4 );

		Layout.Add( splitter, 1 );

		ApplyViewMode( AssetListViewMode.List );
		ApplyModeLayout();
	}

	private void LoadWorkspace()
	{
		ScanCancellation?.Cancel();

		Workspace = ConnecterWorkspaceReader.Read( GetManualWorkspacePath() );
		WorkspaceText.Text = Workspace.WorkspacePath;
		LocationTree.Clear();

		if ( !Workspace.HasRepositories )
		{
			StatusLabel.Text = string.IsNullOrWhiteSpace( Workspace.WorkspacePath )
				? "No Connecter workspace found. Enter the folder containing default.dcdb or settings.xml."
				: $"No Connecter roots found at {Workspace.WorkspacePath}";
			CurrentRepository = null;
			CurrentFolder = null;
			AssetList.SetItems( [] );
			UpdateImportButton();
			return;
		}

		foreach ( var repository in Workspace.Repositories )
		{
			LocationTree.AddItem( new ConnecterLocationNode( repository, repository.FullPath, NavigateTo ) );
		}

		var first = Workspace.Repositories.First();
		CurrentRepository = first;
		CurrentFolder = first.FullPath;
		if ( CurrentMode == ConnecterBrowserMode.Files )
		{
			LocationTree.Open( first );
			LocationTree.SelectItem( first, skipEvents: true );
		}

		ApplyModeLayout();
		RefreshAssetList();
	}

	private string GetManualWorkspacePath()
	{
		var text = WorkspaceText?.Text;
		return string.IsNullOrWhiteSpace( text ) ? null : text;
	}

	private void ApplyManualWorkspacePath()
	{
		var path = GetManualWorkspacePath();
		if ( string.IsNullOrWhiteSpace( path ) )
		{
			AutoDetectWorkspace();
			return;
		}

		ConnecterWorkspaceReader.SetSavedWorkspacePath( path );
		LoadWorkspace();
	}

	private void AutoDetectWorkspace()
	{
		ConnecterWorkspaceReader.ClearSavedWorkspacePath();
		WorkspaceText.Text = string.Empty;

		var discovered = ConnecterWorkspaceReader.DiscoverWorkspacePaths().FirstOrDefault();
		if ( !string.IsNullOrWhiteSpace( discovered ) )
		{
			WorkspaceText.Text = discovered;
			ConnecterWorkspaceReader.SetSavedWorkspacePath( discovered );
		}

		LoadWorkspace();
	}

	private void OpenWorkspaceMenu()
	{
		var menu = new ContextMenu( this );

		menu.AddOption( "Apply Workspace Path", "done", ApplyManualWorkspacePath );
		menu.AddOption( "Auto Detect Workspace", "travel_explore", AutoDetectWorkspace );
		menu.AddSeparator();
		menu.AddOption( "Reveal Workspace", "folder_open", () => EditorUtility.OpenFolder( Workspace?.WorkspacePath ?? WorkspaceText.Text ) )
			.Enabled = Workspace is not null && Directory.Exists( Workspace.WorkspacePath );
		menu.AddOption( "Copy Workspace Path", "content_copy", () => EditorUtility.Clipboard.Copy( Workspace?.WorkspacePath ?? WorkspaceText.Text ) )
			.Enabled = !string.IsNullOrWhiteSpace( Workspace?.WorkspacePath ?? WorkspaceText.Text );
		menu.AddSeparator();
		menu.AddOption( "Clear Saved Workspace", "delete", () =>
		{
			ConnecterWorkspaceReader.ClearSavedWorkspacePath();
			WorkspaceText.Text = string.Empty;
			LoadWorkspace();
		} );

		menu.OpenAtCursor();
	}

	private void OnLocationSelected( object item )
	{
		switch ( item )
		{
			case ConnecterRepository repository:
				NavigateTo( repository, repository.FullPath );
				break;
			case ConnecterLocation location:
				NavigateTo( location.Repository, location.FullPath );
				break;
		}
	}

	private void NavigateTo( ConnecterRepository repository, string folderPath )
	{
		if ( repository is null || string.IsNullOrWhiteSpace( folderPath ) || !Directory.Exists( folderPath ) )
			return;

		CurrentRepository = repository;
		CurrentFolder = folderPath;
		RefreshAssetList();
	}

	private async void RefreshAssetList()
	{
		if ( Workspace is null || !Workspace.HasRepositories )
			return;

		if ( CurrentMode == ConnecterBrowserMode.Files && (CurrentRepository is null || string.IsNullOrWhiteSpace( CurrentFolder )) )
			return;

		ScanCancellation?.Cancel();
		ScanCancellation = new CancellationTokenSource();
		var token = ScanCancellation.Token;

		StatusLabel.Text = CurrentMode == ConnecterBrowserMode.Assets
			? "Indexing Connecter assets..."
			: $"Scanning {CurrentRepository.Name}...";
		AssetList.SetItems( [] );

		try
		{
			var result = CurrentMode == ConnecterBrowserMode.Assets
				? await ConnecterAssetScanner.ScanAssetsAsync( Workspace.Repositories, SearchText.Text, CurrentFilter, token )
				: await ConnecterAssetScanner.ScanAsync( CurrentRepository, CurrentFolder, SearchText.Text, CurrentFilter, token );

			MainThread.Queue( () =>
			{
				if ( token.IsCancellationRequested || !IsValid )
					return;

				CurrentItems = result.Items;
				AssetList.SetItems( CurrentItems.Cast<object>() );
				StatusLabel.Text = BuildStatusText( result );
				UpdateImportButton();
			} );
		}
		catch ( OperationCanceledException )
		{
		}
		catch ( Exception exception )
		{
			StatusLabel.Text = $"Connecter scan failed: {exception.Message}";
		}
	}

	private string BuildStatusText( ConnecterScanResult result )
	{
		if ( CurrentMode == ConnecterBrowserMode.Assets )
		{
			var truncationText = result.Truncated ? " (showing first 1,000 results)" : "";
			return $"Assets {GetFilterLabel( CurrentFilter )}: {result.Items.Count:n0} item{(result.Items.Count == 1 ? "" : "s")}{truncationText}";
		}

		var location = CurrentFolder is null ? "" : ConnecterPathUtility.GetRelativePath( CurrentRepository.FullPath, CurrentFolder );
		if ( string.IsNullOrWhiteSpace( location ) || location == "." )
			location = CurrentRepository.Name;

		var suffix = result.Truncated ? " (showing first 1,000 results)" : "";
		return $"{location}: {result.Items.Count:n0} item{(result.Items.Count == 1 ? "" : "s")}{suffix}";
	}

	private void UpdateImportButton()
	{
		ImportButton.Enabled = GetSelectedRecords().Any( x => x.CanImport );
	}

	private void OpenModeMenu()
	{
		var menu = new ContextMenu( this );

		AddModeOption( menu, "Files Mode", "folder_open", ConnecterBrowserMode.Files );
		AddModeOption( menu, "Assets Mode", "category", ConnecterBrowserMode.Assets );

		menu.OpenAt( ModeButton.ScreenRect.BottomLeft, false );
	}

	private void AddModeOption( ContextMenu menu, string title, string icon, ConnecterBrowserMode mode )
	{
		var option = menu.AddOption( title, icon, () => SetBrowserMode( mode ) );
		option.Checkable = true;
		option.Checked = mode == CurrentMode;
	}

	private void SetBrowserMode( ConnecterBrowserMode mode )
	{
		if ( mode == CurrentMode )
			return;

		CurrentMode = mode;
		ApplyModeLayout();

		if ( mode == ConnecterBrowserMode.Assets )
			ApplyViewMode( AssetListViewMode.MediumIcons );
		else
			ApplyViewMode( AssetListViewMode.List );

		RefreshAssetList();
	}

	private void ApplyModeLayout()
	{
		var assetMode = CurrentMode == ConnecterBrowserMode.Assets;

		LocationTree.Visible = !assetMode;
		ModeButton.Icon = assetMode ? "category" : "folder_open";
		ModeButton.ToolTip = assetMode ? "Assets Mode" : "Files Mode";
		SearchText.PlaceholderText = assetMode ? "Search all Connecter assets" : "Search current Connecter folder";
	}

	private void OpenFilterMenu()
	{
		var menu = new ContextMenu( this );

		foreach ( var filter in Enum.GetValues<ConnecterBrowserFilter>() )
		{
			var option = menu.AddOption( filter.ToString(), GetFilterIcon( filter ), () =>
			{
				CurrentFilter = filter;
				FilterButton.Icon = GetFilterIcon( filter );
				RefreshAssetList();
			} );

			option.Checkable = true;
			option.Checked = filter == CurrentFilter;
		}

		menu.OpenAt( FilterButton.ScreenRect.BottomLeft, false );
	}

	private void OpenViewModeMenu()
	{
		var menu = new ContextMenu( this );

		AddViewOption( menu, "List View", "view_headline", AssetListViewMode.List );
		AddViewOption( menu, "Small Icons", "apps", AssetListViewMode.SmallIcons );
		AddViewOption( menu, "Medium Icons", "grid_on", AssetListViewMode.MediumIcons );
		AddViewOption( menu, "Large Icons", "grid_view", AssetListViewMode.LargeIcons );

		menu.OpenAt( ViewModeButton.ScreenRect.BottomLeft, false );
	}

	private void AddViewOption( ContextMenu menu, string title, string icon, AssetListViewMode viewMode )
	{
		var option = menu.AddOption( title, icon, () => ApplyViewMode( viewMode ) );
		option.Checkable = true;
		option.Checked = viewMode == CurrentViewMode;
	}

	private void ApplyViewMode( AssetListViewMode viewMode )
	{
		CurrentViewMode = viewMode;
		ViewModeButton.Icon = viewMode switch
		{
			AssetListViewMode.SmallIcons => "apps",
			AssetListViewMode.MediumIcons => "grid_on",
			AssetListViewMode.LargeIcons => "grid_view",
			_ => "view_headline"
		};

		switch ( viewMode )
		{
			case AssetListViewMode.SmallIcons:
				AssetList.ItemSize = CurrentMode == ConnecterBrowserMode.Assets ? new Vector2( 96, 132 ) : new Vector2( 72, 104 );
				AssetList.ItemSpacing = 4;
				AssetList.ItemPaint = PaintIconItem;
				break;
			case AssetListViewMode.MediumIcons:
				AssetList.ItemSize = CurrentMode == ConnecterBrowserMode.Assets ? new Vector2( 132, 176 ) : new Vector2( 104, 144 );
				AssetList.ItemSpacing = 4;
				AssetList.ItemPaint = PaintIconItem;
				break;
			case AssetListViewMode.LargeIcons:
				AssetList.ItemSize = CurrentMode == ConnecterBrowserMode.Assets ? new Vector2( 168, 220 ) : new Vector2( 136, 184 );
				AssetList.ItemSpacing = 6;
				AssetList.ItemPaint = PaintIconItem;
				break;
			default:
				AssetList.ItemSize = new Vector2( 0, Theme.RowHeight );
				AssetList.ItemSpacing = 0;
				AssetList.ItemPaint = PaintListItem;
				break;
		}

		AssetList.Update();
	}

	private void PaintListItem( VirtualWidget item )
	{
		if ( item.Object is not ConnecterAssetRecord record )
			return;

		DrawItemBackground( item );

		var rect = item.Rect;
		var iconRect = rect.Shrink( 6, 4 );
		iconRect.Width = iconRect.Height = 18;

		Paint.SetPen( GetKindColor( record.Kind ) );
		Paint.DrawIcon( iconRect, GetKindIcon( record ), 18, TextFlag.Center );

		var nameRect = rect.Shrink( 32, 0, 260, 0 );
		Paint.SetPen( item.Selected ? Color.White : Theme.Text );
		Paint.DrawText( nameRect, record.Name, TextFlag.LeftCenter | TextFlag.SingleLine );

		var typeRect = rect;
		typeRect.Left = rect.Right - 250;
		typeRect.Width = 90;
		Paint.SetPen( Theme.TextLight );
		Paint.DrawText( typeRect, record.Kind.ToString(), TextFlag.LeftCenter | TextFlag.SingleLine );

		var pathRect = rect;
		pathRect.Left = rect.Right - 155;
		pathRect.Right -= 8;
		Paint.SetPen( record.Warning is null ? Theme.TextLight : Theme.Yellow );
		Paint.DrawText( pathRect, record.Warning ?? record.RelativePath, TextFlag.LeftCenter | TextFlag.SingleLine );
	}

	private void PaintIconItem( VirtualWidget item )
	{
		if ( item.Object is not ConnecterAssetRecord record )
			return;

		if ( CurrentMode == ConnecterBrowserMode.Assets )
		{
			PaintAssetTile( item, record );
			return;
		}

		DrawItemBackground( item );

		var rect = item.Rect.Shrink( 4 );
		var iconRect = rect;
		iconRect.Height = iconRect.Width;

		Paint.SetBrush( Theme.ControlBackground );
		Paint.ClearPen();
		Paint.DrawRect( iconRect, Theme.ControlRadius );

		DrawPreviewContent( record, iconRect );

		var textRect = rect;
		textRect.Top = iconRect.Bottom + 4;
		textRect.Height = 36;
		Paint.SetDefaultFont( 7 );
		Paint.SetPen( item.Selected ? Color.White : Theme.Text );
		Paint.DrawText( textRect, Paint.GetElidedText( record.Name, textRect.Width, ElideMode.Middle ), TextFlag.LeftTop );

		if ( record.Warning is not null )
		{
			Paint.SetPen( Theme.Yellow );
			Paint.DrawIcon( rect.Shrink( 4 ), "warning", 16, TextFlag.RightTop );
		}
	}

	private void PaintAssetTile( VirtualWidget item, ConnecterAssetRecord record )
	{
		DrawItemBackground( item );

		var rect = item.Rect.Shrink( 4 );
		var previewRect = rect;
		previewRect.Height = previewRect.Width;

		Paint.SetBrush( Theme.ControlBackground );
		Paint.ClearPen();
		Paint.DrawRect( previewRect, Theme.ControlRadius );

		DrawPreviewContent( record, previewRect );

		var nameRect = rect.Shrink( 2, 0 );
		nameRect.Top = previewRect.Bottom + 5;
		nameRect.Height = 28;

		Paint.SetDefaultFont( 7 );
		Paint.SetPen( item.Selected ? Color.White : Theme.Text );
		Paint.DrawText( nameRect, Paint.GetElidedText( record.Name, nameRect.Width, ElideMode.Middle ), TextFlag.LeftTop );

		var metaRect = nameRect;
		metaRect.Top += 28;
		metaRect.Height = 16;

		Paint.SetPen( Theme.TextLight );
		Paint.DrawText( metaRect, Paint.GetElidedText( $"{record.Kind} - {record.RepositoryName}", metaRect.Width, ElideMode.Right ), TextFlag.LeftTop );

		Paint.SetPen( GetKindColor( record.Kind ) );
		Paint.DrawIcon( previewRect.Shrink( 5 ), GetKindIcon( record ), 16, TextFlag.LeftTop );

		if ( record.Warning is not null )
		{
			Paint.SetPen( Theme.Yellow );
			Paint.DrawIcon( previewRect.Shrink( 5 ), "warning", 16, TextFlag.RightTop );
		}
	}

	private void DrawPreviewContent( ConnecterAssetRecord record, Rect previewRect )
	{
		if ( record.Kind == ConnecterAssetKind.Image && TryGetImagePreview( record.FullPath, out var pixmap ) )
		{
			Paint.BilinearFiltering = true;
			Paint.Draw( previewRect.Shrink( 2 ), pixmap );
			Paint.BilinearFiltering = false;
			return;
		}

		var color = GetKindColor( record.Kind );
		Paint.ClearPen();
		Paint.SetBrush( color.WithAlpha( 0.12f ) );
		Paint.DrawRect( previewRect.Shrink( 2 ), Theme.ControlRadius );

		Paint.SetPen( color );
		Paint.DrawIcon( previewRect.Shrink( 18 ), GetKindIcon( record ), Math.Min( 56, previewRect.Width - 24 ), TextFlag.Center );
	}

	private bool TryGetImagePreview( string path, out Pixmap pixmap )
	{
		if ( ImagePreviewCache.TryGetValue( path, out pixmap ) )
			return true;

		if ( FailedImagePreviews.Contains( path ) || !File.Exists( path ) )
		{
			pixmap = null;
			return false;
		}

		try
		{
			pixmap = Pixmap.FromFile( path );
			ImagePreviewCache[path] = pixmap;
			return pixmap is not null;
		}
		catch
		{
			FailedImagePreviews.Add( path );
			pixmap = null;
			return false;
		}
	}

	private void DrawItemBackground( VirtualWidget item )
	{
		if ( item.Selected || item.Pressed )
		{
			Paint.SetBrush( Theme.Primary.WithAlpha( 0.38f ) );
			Paint.ClearPen();
			Paint.DrawRect( item.Rect.Shrink( 1 ), Theme.ControlRadius );
		}
		else if ( item.Hovered )
		{
			Paint.SetBrush( Theme.SurfaceLightBackground.WithAlpha( 0.35f ) );
			Paint.ClearPen();
			Paint.DrawRect( item.Rect.Shrink( 1 ), Theme.ControlRadius );
		}
	}

	private void OnAssetActivated( object item )
	{
		if ( item is not ConnecterAssetRecord record )
			return;

		if ( record.IsDirectory )
		{
			NavigateTo( CurrentRepository, record.FullPath );
			return;
		}

		ImportRecords( [record] );
	}

	private void OpenAssetContextMenu( object item )
	{
		if ( item is not ConnecterAssetRecord record )
			return;

		var selected = GetSelectedRecords().ToList();
		if ( selected.Count == 0 || !selected.Contains( record ) )
			selected = [record];

		var menu = new ContextMenu( this ) { Searchable = true };

		menu.AddOption( selected.Count == 1 ? "Import" : $"Import {selected.Count} Items", "download", () => ImportRecords( selected ) )
			.Enabled = selected.Any( x => x.CanImport );

		menu.AddOption( "Reveal Source", "folder_open", () => EditorUtility.OpenFolder( record.FullPath ) );
		menu.AddOption( "Copy Source Path", "content_copy", () => EditorUtility.Clipboard.Copy( record.FullPath ) );

		if ( record.Warning is not null )
		{
			menu.AddSeparator();
			menu.AddOption( record.Warning, "warning" ).Enabled = false;
		}

		menu.OpenAtCursor();
	}

	private bool StartAssetDrag( object item )
	{
		if ( item is not ConnecterAssetRecord record || record.IsDirectory )
			return false;

		if ( !record.CanImport )
		{
			ShowWarning( record.Warning );
			return false;
		}

		try
		{
			StatusLabel.Text = $"Importing {record.Name}...";
			var imported = ImportRecord( record );
			if ( imported.PrimaryAsset is null )
				return false;

			var drag = new Drag( AssetList );
			drag.Data.Text = imported.PrimaryAsset.RelativePath;
			drag.Data.Url = new Uri( "file:///" + imported.PrimaryAsset.AbsolutePath );
			drag.Execute();

			StatusLabel.Text = imported.Result.Message;
			return true;
		}
		catch ( Exception exception )
		{
			StatusLabel.Text = $"Import failed: {exception.Message}";
			return false;
		}
	}

	private void ImportSelected()
	{
		ImportRecords( GetSelectedRecords().ToList() );
	}

	private void ImportRecords( IReadOnlyList<ConnecterAssetRecord> records )
	{
		records = records.Where( x => x.CanImport ).ToList();
		if ( records.Count == 0 )
			return;

		var folder = records.FirstOrDefault( x => x.IsDirectory );
		if ( folder is not null )
		{
			var stats = ConnecterImporter.GetDirectoryStats( folder.FullPath );
			var confirm = new PopupWindow(
				"Import folder?",
				$"This will copy {stats.Count:n0} files ({stats.Bytes.SizeFormat()}) into the current project's Assets/ConnecterImports folder.",
				"Cancel",
				new Dictionary<string, Action>
				{
					["Import"] = () => ImportRecordsConfirmed( records )
				} );

			confirm.Show();
			return;
		}

		ImportRecordsConfirmed( records );
	}

	private void ImportRecordsConfirmed( IReadOnlyList<ConnecterAssetRecord> records )
	{
		Asset lastAsset = null;

		foreach ( var record in records )
		{
			try
			{
				StatusLabel.Text = $"Importing {record.Name}...";
				var imported = ImportRecord( record );
				lastAsset = imported.PrimaryAsset ?? lastAsset;
				StatusLabel.Text = imported.Result.Message;
			}
			catch ( Exception exception )
			{
				StatusLabel.Text = $"Import failed: {exception.Message}";
				ShowWarning( exception.Message );
				break;
			}
		}

		MainAssetBrowser.Instance?.Local.UpdateAssetList();

		if ( lastAsset is not null )
		{
			MainAssetBrowser.Instance?.Local.FocusOnAsset( lastAsset, true );
			EditorUtility.InspectorObject = lastAsset;
		}
	}

	private ConnecterEditorImportResult ImportRecord( ConnecterAssetRecord record )
	{
		var options = new ConnecterImportOptions( Sandbox.Project.Current.GetAssetsPath() );
		return ConnecterImporter.Import( record, options );
	}

	private IEnumerable<ConnecterAssetRecord> GetSelectedRecords()
	{
		return AssetList.SelectedItems.OfType<ConnecterAssetRecord>();
	}

	private void ShowWarning( string text )
	{
		var popup = new PopupWindow( "Connecter Browser", text ?? "Unsupported asset.", "OK" );
		popup.Show();
	}

	private static string GetKindIcon( ConnecterAssetRecord record )
	{
		if ( record.IsDirectory )
			return "folder";

		return record.Kind switch
		{
			ConnecterAssetKind.ModelSource => "view_in_ar",
			ConnecterAssetKind.SboxModel => "deployed_code",
			ConnecterAssetKind.Image => "image",
			ConnecterAssetKind.Audio => "volume_up",
			ConnecterAssetKind.Material => "texture",
			ConnecterAssetKind.Unsupported => "warning",
			_ => "insert_drive_file"
		};
	}

	private static string GetFilterIcon( ConnecterBrowserFilter filter )
	{
		return filter switch
		{
			ConnecterBrowserFilter.Models => "view_in_ar",
			ConnecterBrowserFilter.Materials => "texture",
			ConnecterBrowserFilter.Images => "image",
			ConnecterBrowserFilter.Audio => "volume_up",
			ConnecterBrowserFilter.Sbox => "deployed_code",
			ConnecterBrowserFilter.Unsupported => "warning",
			_ => "filter_list"
		};
	}

	private static Color GetKindColor( ConnecterAssetKind kind )
	{
		return kind switch
		{
			ConnecterAssetKind.ModelSource or ConnecterAssetKind.SboxModel => Color.Cyan,
			ConnecterAssetKind.Image or ConnecterAssetKind.Material => Color.Green,
			ConnecterAssetKind.Audio => Color.Orange,
			ConnecterAssetKind.Unsupported => Theme.Yellow,
			ConnecterAssetKind.Unknown => Color.Gray,
			_ => Theme.Text
		};
	}

	private static string GetFilterLabel( ConnecterBrowserFilter filter )
	{
		return filter == ConnecterBrowserFilter.All ? "" : $"({filter})";
	}
}

public sealed record ConnecterLocation( ConnecterRepository Repository, string FullPath );

file sealed class ConnecterLocationNode : TreeNode
{
	private readonly ConnecterRepository Repository;
	private readonly string FullPath;
	private readonly Action<ConnecterRepository, string> Navigate;

	public ConnecterLocationNode( ConnecterRepository repository, string fullPath, Action<ConnecterRepository, string> navigate )
	{
		Repository = repository;
		FullPath = fullPath;
		Navigate = navigate;
		Value = string.Equals( repository.FullPath, fullPath, StringComparison.OrdinalIgnoreCase )
			? repository
			: new ConnecterLocation( repository, fullPath );
	}

	public override bool HasChildren => SafeEnumerateDirectories( FullPath ).Any();

	protected override void BuildChildren()
	{
		if ( !Directory.Exists( FullPath ) )
		{
			ClearChildren();
			return;
		}

		var directories = SafeEnumerateDirectories( FullPath )
			.Where( x => !Path.GetFileName( x ).StartsWith( "." ) )
			.OrderBy( Path.GetFileName, StringComparer.OrdinalIgnoreCase )
			.Select( x => new ConnecterLocation( Repository, x ) );

		SetChildren( directories, x => new ConnecterLocationNode( x.Repository, x.FullPath, Navigate ) );
	}

	public override void OnSelectionChanged( bool state )
	{
		if ( state )
			Navigate( Repository, FullPath );
	}

	public override void OnActivated()
	{
		Navigate( Repository, FullPath );
	}

	public override string GetTooltip()
	{
		return FullPath;
	}

	public override void OnPaint( VirtualWidget item )
	{
		PaintSelection( item );

		var rect = item.Rect;
		var name = Value is ConnecterRepository repository ? repository.Name : Path.GetFileName( FullPath );

		Paint.SetPen( item.Selected ? Color.White : Theme.Text );
		Paint.DrawIcon( rect.Shrink( 4, 2 ), Value is ConnecterRepository ? "perm_media" : "folder", 18, TextFlag.LeftCenter );

		rect.Left += 26;
		Paint.DrawText( rect, name, TextFlag.LeftCenter | TextFlag.SingleLine );
	}

	private static IEnumerable<string> SafeEnumerateDirectories( string folderPath )
	{
		if ( !Directory.Exists( folderPath ) )
			return [];

		try
		{
			return Directory.EnumerateDirectories( folderPath );
		}
		catch
		{
			return [];
		}
	}
}