Editor/Infrastructure/TailBoxProjectFileSystem.cs
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;

namespace Sandbox.TailBox;

internal static class TailBoxProjectFileSystem
{
	public static string NormalizeProjectRoot( string projectRoot )
	{
		return Path.GetFullPath( projectRoot );
	}

	public static string GetConfigPath( string projectRoot )
	{
		return Path.GetFullPath( GetActiveConfigPath( projectRoot ) );
	}

	public static bool ConfigExists( string projectRoot )
	{
		return File.Exists( GetConfigPath( projectRoot ) );
	}

	public static TailBoxConfig LoadConfig( string projectRoot )
	{
		var path = GetConfigPath( projectRoot );
		if ( !File.Exists( path ) )
		{
			var defaults = TailBoxConfig.CreateDefault();
			defaults.ConfigPath = path;
			return defaults;
		}

		return TailBoxConfig.LoadJson( File.ReadAllText( path ), path );
	}

	public static void SaveConfig( string projectRoot, TailBoxConfig config )
	{
		var path = GetConfigPath( projectRoot );
		Directory.CreateDirectory( Path.GetDirectoryName( path )! );
		File.WriteAllText( path, config.ToJson() );
		config.ConfigPath = path;
	}

	private static string GetActiveConfigPath( string projectRoot )
	{
		var modernPath = TailBoxConfig.GetConfigPath( projectRoot );
		if ( File.Exists( modernPath ) )
			return modernPath;

		var legacyPath = Path.Combine( projectRoot, TailBoxConfig.LegacyFileName );
		if ( File.Exists( legacyPath ) )
			return legacyPath;

		return modernPath;
	}

	public static IReadOnlyCollection<string> FindContentFiles( string projectRoot, TailBoxConfig config, string outputPath )
	{
		return EnumerateContentFiles( projectRoot, config, outputPath ).ToArray();
	}

	public static IReadOnlyList<TailBoxSourceText> ReadSources( IEnumerable<string> sourceFiles )
	{
		return sourceFiles
			.Select( file => new TailBoxSourceText( file, File.ReadAllText( file ) ) )
			.ToArray();
	}

	public static string ResolveOutputPath( string projectRoot, TailBoxConfig config )
	{
		var outputPath = config?.OutputPath;
		if ( string.IsNullOrWhiteSpace( outputPath ) )
			outputPath = TailBoxConfig.CreateDefault().OutputPath;

		return Path.IsPathRooted( outputPath )
			? Path.GetFullPath( outputPath )
			: Path.GetFullPath( Path.Combine( projectRoot, outputPath ) );
	}

	public static bool WriteIfChanged( string outputPath, string content )
	{
		Directory.CreateDirectory( Path.GetDirectoryName( outputPath )! );

		if ( File.Exists( outputPath ) && string.Equals( File.ReadAllText( outputPath ), content, StringComparison.Ordinal ) )
			return false;

		File.WriteAllText( outputPath, content );
		return true;
	}

	public static bool ShouldSkipPath( string projectRoot, string outputPath, string changedPath )
	{
		if ( string.IsNullOrWhiteSpace( projectRoot ) || string.IsNullOrWhiteSpace( changedPath ) )
			return true;

		var fullPath = Path.GetFullPath( changedPath );
		if ( !string.IsNullOrWhiteSpace( outputPath )
			&& string.Equals( fullPath, Path.GetFullPath( outputPath ), StringComparison.OrdinalIgnoreCase ) )
		{
			return true;
		}

		if ( !TryGetRelativeProjectPath( projectRoot, fullPath, out var relative ) )
			return true;

		var segments = relative.Split( '/', StringSplitOptions.RemoveEmptyEntries );
		return segments.Any( segment => segment is ".git" or ".sbox" or ".vs" or "bin" or "obj" );
	}

	public static bool TryGetRelativeProjectPath( string projectRoot, string path, out string relativePath )
	{
		relativePath = "";
		if ( string.IsNullOrWhiteSpace( projectRoot ) || string.IsNullOrWhiteSpace( path ) )
			return false;

		var root = Path.GetFullPath( projectRoot );
		var fullPath = Path.GetFullPath( path );
		var relative = Path.GetRelativePath( root, fullPath );
		if ( Path.IsPathRooted( relative ) )
			return false;

		relative = relative.Replace( '\\', '/' );
		if ( relative == ".." || relative.StartsWith( "../", StringComparison.Ordinal ) )
			return false;

		relativePath = relative;
		return true;
	}

	private static IEnumerable<string> EnumerateContentFiles( string projectRoot, TailBoxConfig config, string outputPath )
	{
		var globs = (config?.Content is { Count: > 0 } ? config.Content : TailBoxConfig.CreateDefault().Content)
			.Select( TailBoxGlobMatcher.FromGlob )
			.ToArray();

		foreach ( var file in Directory.EnumerateFiles( projectRoot, "*.*", SearchOption.AllDirectories ) )
		{
			var fullPath = Path.GetFullPath( file );
			if ( ShouldSkipPath( projectRoot, outputPath, fullPath ) )
				continue;

			if ( !TryGetRelativeProjectPath( projectRoot, fullPath, out var relative ) )
				continue;

			if ( globs.Any( glob => glob.IsMatch( relative ) ) )
				yield return fullPath;
		}
	}
}