Editor/ConnecterAssetScanner.cs
using Sandbox;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace Editor;

public enum ConnecterBrowserMode
{
	Files,
	Assets
}

public enum ConnecterBrowserFilter
{
	All,
	Models,
	Materials,
	Images,
	Audio,
	Sbox,
	Unsupported
}

public sealed record ConnecterScanResult( IReadOnlyList<ConnecterAssetRecord> Items, bool Truncated );

public static class ConnecterAssetScanner
{
	private const int MaxSearchResults = 1000;

	public static Task<ConnecterScanResult> ScanAsync( ConnecterRepository repository, string folderPath, string query, ConnecterBrowserFilter filter, CancellationToken token )
	{
		return Task.Run( () => Scan( repository, folderPath, query, filter, token ), token );
	}

	public static Task<ConnecterScanResult> ScanAssetsAsync( IReadOnlyList<ConnecterRepository> repositories, string query, ConnecterBrowserFilter filter, CancellationToken token )
	{
		return Task.Run( () => ScanAssets( repositories, query, filter, token ), token );
	}

	private static ConnecterScanResult Scan( ConnecterRepository repository, string folderPath, string query, ConnecterBrowserFilter filter, CancellationToken token )
	{
		var items = new List<ConnecterAssetRecord>();
		var recursive = !string.IsNullOrWhiteSpace( query );
		var truncated = false;

		if ( string.IsNullOrWhiteSpace( folderPath ) || !Directory.Exists( folderPath ) )
			return new ConnecterScanResult( items, false );

		if ( recursive )
		{
			foreach ( var directory in SafeEnumerateDirectories( folderPath, true ) )
			{
				token.ThrowIfCancellationRequested();
				TryAdd( repository, directory, true, query, filter, items );

				if ( items.Count >= MaxSearchResults )
				{
					truncated = true;
					break;
				}
			}
		}
		else
		{
			foreach ( var directory in SafeEnumerateDirectories( folderPath, false ) )
			{
				token.ThrowIfCancellationRequested();
				TryAdd( repository, directory, true, query, filter, items );
			}
		}

		if ( !truncated )
		{
			foreach ( var file in SafeEnumerateFiles( folderPath, recursive ) )
			{
				token.ThrowIfCancellationRequested();
				TryAdd( repository, file, false, query, filter, items );

				if ( recursive && items.Count >= MaxSearchResults )
				{
					truncated = true;
					break;
				}
			}
		}

		var ordered = items
			.OrderByDescending( x => x.IsDirectory )
			.ThenBy( x => x.Name, StringComparer.OrdinalIgnoreCase )
			.ToList();

		return new ConnecterScanResult( ordered, truncated );
	}

	private static ConnecterScanResult ScanAssets( IReadOnlyList<ConnecterRepository> repositories, string query, ConnecterBrowserFilter filter, CancellationToken token )
	{
		var items = new List<ConnecterAssetRecord>();
		var truncated = false;

		foreach ( var repository in repositories )
		{
			token.ThrowIfCancellationRequested();

			if ( string.IsNullOrWhiteSpace( repository.FullPath ) || !Directory.Exists( repository.FullPath ) )
				continue;

			foreach ( var file in SafeEnumerateFiles( repository.FullPath, true ) )
			{
				token.ThrowIfCancellationRequested();
				TryAddAsset( repository, file, query, filter, items );

				if ( items.Count >= MaxSearchResults )
				{
					truncated = true;
					break;
				}
			}

			if ( truncated )
				break;
		}

		var ordered = items
			.OrderBy( x => GetKindSortOrder( x.Kind ) )
			.ThenBy( x => x.Name, StringComparer.OrdinalIgnoreCase )
			.ThenBy( x => x.RepositoryName, StringComparer.OrdinalIgnoreCase )
			.ToList();

		return new ConnecterScanResult( ordered, truncated );
	}

	private static void TryAdd( ConnecterRepository repository, string path, bool isDirectory, string query, ConnecterBrowserFilter filter, List<ConnecterAssetRecord> items )
	{
		if ( ShouldSkipPath( path ) )
			return;

		var record = ConnecterAssetRecord.FromPath( repository, path, isDirectory );

		if ( !MatchesQuery( record, query ) )
			return;

		if ( !MatchesFilter( record, filter ) )
			return;

		items.Add( record );
	}

	private static void TryAddAsset( ConnecterRepository repository, string path, string query, ConnecterBrowserFilter filter, List<ConnecterAssetRecord> items )
	{
		if ( ShouldSkipPath( path ) )
			return;

		var record = ConnecterAssetRecord.FromPath( repository, path, false );

		if ( !MatchesAssetModeFilter( record, filter ) )
			return;

		if ( !MatchesQuery( record, query ) )
			return;

		items.Add( record );
	}

	private static bool MatchesQuery( ConnecterAssetRecord record, string query )
	{
		if ( string.IsNullOrWhiteSpace( query ) )
			return true;

		foreach ( var part in query.Split( ' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries ) )
		{
			if ( !record.Name.Contains( part, StringComparison.OrdinalIgnoreCase )
				&& !record.RelativePath.Contains( part, StringComparison.OrdinalIgnoreCase )
				&& !record.Kind.ToString().Contains( part, StringComparison.OrdinalIgnoreCase ) )
			{
				return false;
			}
		}

		return true;
	}

	private static bool MatchesFilter( ConnecterAssetRecord record, ConnecterBrowserFilter filter )
	{
		return filter switch
		{
			ConnecterBrowserFilter.Models => record.IsDirectory || record.Kind is ConnecterAssetKind.ModelSource or ConnecterAssetKind.SboxModel,
			ConnecterBrowserFilter.Materials => record.IsDirectory || record.Kind is ConnecterAssetKind.Material,
			ConnecterBrowserFilter.Images => record.IsDirectory || record.Kind is ConnecterAssetKind.Image,
			ConnecterBrowserFilter.Audio => record.IsDirectory || record.Kind is ConnecterAssetKind.Audio,
			ConnecterBrowserFilter.Sbox => record.IsDirectory || record.Kind is ConnecterAssetKind.SboxModel or ConnecterAssetKind.Material,
			ConnecterBrowserFilter.Unsupported => record.Kind is ConnecterAssetKind.Unsupported or ConnecterAssetKind.Unknown,
			_ => true
		};
	}

	private static bool MatchesAssetModeFilter( ConnecterAssetRecord record, ConnecterBrowserFilter filter )
	{
		return filter switch
		{
			ConnecterBrowserFilter.Models => record.Kind is ConnecterAssetKind.ModelSource or ConnecterAssetKind.SboxModel,
			ConnecterBrowserFilter.Materials => record.Kind is ConnecterAssetKind.Material,
			ConnecterBrowserFilter.Images => record.Kind is ConnecterAssetKind.Image,
			ConnecterBrowserFilter.Audio => record.Kind is ConnecterAssetKind.Audio,
			ConnecterBrowserFilter.Sbox => record.Kind is ConnecterAssetKind.SboxModel or ConnecterAssetKind.Material,
			ConnecterBrowserFilter.Unsupported => record.Kind is ConnecterAssetKind.Unsupported or ConnecterAssetKind.Unknown,
			_ => record.CanImport
		};
	}

	private static int GetKindSortOrder( ConnecterAssetKind kind )
	{
		return kind switch
		{
			ConnecterAssetKind.ModelSource or ConnecterAssetKind.SboxModel => 0,
			ConnecterAssetKind.Material => 1,
			ConnecterAssetKind.Image => 2,
			ConnecterAssetKind.Audio => 3,
			ConnecterAssetKind.Unsupported => 8,
			ConnecterAssetKind.Unknown => 9,
			_ => 7
		};
	}

	private static bool ShouldSkipPath( string path )
	{
		var name = Path.GetFileName( path );
		if ( string.IsNullOrWhiteSpace( name ) )
			return true;

		return name.StartsWith( "." ) || name.Equals( "Thumbs.db", StringComparison.OrdinalIgnoreCase );
	}

	private static IEnumerable<string> SafeEnumerateDirectories( string folderPath, bool recursive )
	{
		var options = new EnumerationOptions
		{
			IgnoreInaccessible = true,
			RecurseSubdirectories = recursive,
			AttributesToSkip = FileAttributes.Hidden | FileAttributes.System
		};

		try
		{
			return Directory.EnumerateDirectories( folderPath, "*", options );
		}
		catch
		{
			return [];
		}
	}

	private static IEnumerable<string> SafeEnumerateFiles( string folderPath, bool recursive )
	{
		var options = new EnumerationOptions
		{
			IgnoreInaccessible = true,
			RecurseSubdirectories = recursive,
			AttributesToSkip = FileAttributes.Hidden | FileAttributes.System
		};

		try
		{
			return Directory.EnumerateFiles( folderPath, "*", options );
		}
		catch
		{
			return [];
		}
	}
}