Editor/Decompiler/Formats/BspFormatRegistry.cs
using BspImport.Decompiler.Formats.Descriptors;

namespace BspImport.Decompiler.Formats;

/// <summary>
/// Registry of all known BSP format descriptors.
/// Drives two-phase format detection: version-first, then optional entity refinement.
///
/// <b>Adding a new game format:</b>
/// Implement <see cref="IBspFormatDescriptor"/>, add an instance to
/// <see cref="GetDescriptors"/> in the correct position. No other changes required.
/// </summary>
public static class BspFormatRegistry
{
	/// <summary>
	/// Phase 1: detect the format descriptor from the file's BSP version number alone.
	/// For definitively-versioned formats the result is final.
	/// For shared-version formats, call <see cref="RefineWithEntities"/> afterward.
	/// </summary>
	public static IBspFormatDescriptor DetectByVersion( int version )
	{
		var candidates = CandidatesForVersion( version );

		if ( candidates.Count == 0 )
		{
			Log.Warning( $"[BSP] Unrecognised version {version}. " +
			             "Using unknown-format fallback (standard Source v20 readers)." );
			return UnknownBspFormatDescriptor.Instance;
		}

		if ( IsVersionDefinitive( version ) )
		{
			var winner = candidates[0];
			Log.Info( $"[BSP] v{version} → '{winner.DisplayName}' (definitive — sole owner)." );
			return winner;
		}

		// NEW: always start with the generic fallback (lowest SpecificityScore)
		// Because candidates are already ordered descending by score, the last one is the safest.
		var fallback = candidates[^1];

		Log.Info( $"[BSP] v{version} → '{fallback.DisplayName}' " +
		          $"(fallback — {candidates.Count} formats share this version; " +
		          "map name and/or entity refinement pending)." );

		return fallback;
	}

	/// <summary>
	/// Refine using the map file name (no lump parsing required).
	/// Safe to call immediately after first phase. No-op for definitively-versioned formats.
	/// </summary>
	/// <param name="current">Descriptor from first phase.</param>
	/// <param name="bspVersion">The exact BSP version from the file header.</param>
	/// <param name="mapName">
	/// Bare map filename without extension, e.g. <c>"la_sewers"</c>.
	/// Typically <c>Path.GetFileNameWithoutExtension(Context.Name)</c>.
	/// </param>
	public static IBspFormatDescriptor RefineWithMapName( IBspFormatDescriptor current,
		int bspVersion,
		string mapName )
	{
		if ( IsVersionDefinitive( bspVersion ) || string.IsNullOrEmpty( mapName ) )
			return current;

		var refined = SelectBestMatchingCandidate( bspVersion,
			d => d.MatchesMapName( mapName ) );

		return LogRefinement( current, refined, $"map name '{mapName}'" );
	}

	/// <summary>
	/// Refine using entity classnames (no lump parsing required).
	/// </summary>
	/// <param name="current"></param>
	/// <param name="bspVersion"></param>
	/// <param name="entityClassNames"></param>
	/// <returns></returns>
	public static IBspFormatDescriptor RefineWithEntities(
		IBspFormatDescriptor current,
		int bspVersion,
		IReadOnlyList<string> entityClassNames )
	{
		if ( IsVersionDefinitive( bspVersion ) || entityClassNames is not { Count: > 0 } )
			return current;

		var refined = SelectBestMatchingCandidate(
			bspVersion,
			d => d.MatchesEntities( entityClassNames ) );

		return LogRefinement( current, refined, "entity classnames" );
	}

	public static bool IsVersionDefinitive( int version ) =>
		GetVersionOwnerCount().TryGetValue( version, out var count ) && count == 1;

	private static IReadOnlyList<IBspFormatDescriptor> CandidatesForVersion( int version ) =>
		GetDescriptors()
			.Where( d => d.SupportedVersions.Contains( version ) )
			.OrderByDescending( d => d.SpecificityScore )
			.ThenBy( d => d.DisplayName, StringComparer.Ordinal )
			.ToList();

	/// <summary>
	/// Rebuild descriptors on demand instead of caching static instances so hot reload
	/// picks up registry edits without requiring a full editor/domain restart.
	/// </summary>
	private static IReadOnlyList<IBspFormatDescriptor> GetDescriptors()
	{
		return
		[
			new VtmbBspFormatDescriptor(),
			new Portal2BspFormatDescriptor(),
			new AlienSwarmBspFormatDescriptor(),
			new CounterStrikeGlobalOffensiveBspFormatDescriptor(),
			new Left4DeadBspFormatDescriptor(),
			new Left4Dead2BspFormatDescriptor(),
			new SourceV22BspFormatDescriptor(),
			new SourceV21BspFormatDescriptor(),
			new SourceV20BspFormatDescriptor()
		];
	}

	private static IReadOnlyDictionary<int, int> GetVersionOwnerCount()
	{
		return GetDescriptors()
			.SelectMany( d => d.SupportedVersions.Select( v => (version: v, descriptor: d) ) )
			.GroupBy( x => x.version )
			.ToDictionary( g => g.Key, g => g.Count() );
	}

	private static IBspFormatDescriptor? SelectBestMatchingCandidate(
		int bspVersion,
		Func<IBspFormatDescriptor, bool> predicate )
	{
		return CandidatesForVersion( bspVersion )
			.Where( predicate )
			.FirstOrDefault();
	}

	private static IBspFormatDescriptor LogRefinement(
		IBspFormatDescriptor current,
		IBspFormatDescriptor? refined,
		string refinementSource )
	{
		if ( refined is null || refined.GetType() == current.GetType() )
			return current;

		Log.Info( $"[BSP] Refined via {refinementSource}: " +
		          $"'{current.DisplayName}' → '{refined.DisplayName}'." );
		return refined;
	}
}