Editor/CitizenRetarget/RetargetToolSettings.cs
#nullable enable

using System.Diagnostics;
using System.Text.Json;

namespace Editor.CitizenRetarget;

internal sealed class RetargetToolSettings
{
	public string BackendRootPath { get; set; } = string.Empty;
	public string BlenderExecutablePath { get; set; } = string.Empty;
	public string RokokoAddonPath { get; set; } = string.Empty;

	public static string SettingsAssetPath => ".sbox/citizen_retarget/settings.json";
	public static string SettingsAbsolutePath => Path.Combine( CitizenRetargetPaths.LocalSettingsRoot, "settings.json" );
	private static string LegacySettingsAbsolutePath => CitizenRetargetPaths.GetAssetAbsolutePath( "tools/citizen_retarget/settings.json" );

	public static RetargetToolSettings Load()
	{
		try
		{
			var path = SettingsAbsolutePath;
			if ( !File.Exists( path ) )
			{
				var legacyPath = LegacySettingsAbsolutePath;
				if ( File.Exists( legacyPath ) )
				{
					var migrated = LoadFromFile( legacyPath );
					migrated.Save();
					return migrated;
				}

				return new RetargetToolSettings();
			}

			return LoadFromFile( path );
		}
		catch
		{
			return new RetargetToolSettings();
		}
	}

	public void Save()
	{
		var path = SettingsAbsolutePath;
		Directory.CreateDirectory( Path.GetDirectoryName( path )! );
		File.WriteAllText( path, JsonSerializer.Serialize( this, new JsonSerializerOptions
		{
			WriteIndented = true
		} ) );
	}

	private static RetargetToolSettings LoadFromFile( string path )
	{
		return JsonSerializer.Deserialize<RetargetToolSettings>( File.ReadAllText( path ), new JsonSerializerOptions
		{
			PropertyNameCaseInsensitive = true,
			AllowTrailingCommas = true,
			ReadCommentHandling = JsonCommentHandling.Skip
		} ) ?? new RetargetToolSettings();
	}
}

internal sealed class RetargetSetupResolution
{
	public string BackendRootPath { get; init; } = string.Empty;
	public string BackendSource { get; init; } = string.Empty;
	public string ConfiguredBackendRootPath { get; init; } = string.Empty;
	public string BackendRecipePath { get; init; } = string.Empty;
	public string BackendScriptPath { get; init; } = string.Empty;
	public string BlenderExecutablePath { get; init; } = string.Empty;
	public string BlenderSource { get; init; } = string.Empty;
	public string TargetReferencePath { get; init; } = string.Empty;
}

internal static class RetargetSetupResolver
{
	private const string BundledBackendAssetPath = "tools/citizen_retarget/backend";
	private const string KnownGoodRokokoBlenderPath = @"C:\Program Files\Blender Foundation\Blender 4.4\blender.exe";
	private static readonly string[] BlenderCandidatePaths =
	{
		KnownGoodRokokoBlenderPath,
		@"C:\Program Files\Blender Foundation\Blender 4.3\blender.exe",
		@"C:\Program Files\Blender Foundation\Blender 5.0\blender.exe",
		@"C:\Program Files\Blender Foundation\Blender 5.1\blender.exe"
	};

	public static RetargetSetupResolution Resolve( RetargetToolSettings settings )
	{
		var configuredBackendRoot = NormalizePath( settings.BackendRootPath );
		var backendRoot = ResolveBackendRoot( configuredBackendRoot, out var backendSource );
		return new RetargetSetupResolution
		{
			BackendRootPath = backendRoot,
			BackendSource = backendSource,
			ConfiguredBackendRootPath = configuredBackendRoot,
			BackendRecipePath = string.IsNullOrWhiteSpace( backendRoot )
				? string.Empty
				: Path.Combine( backendRoot, CitizenTargetProfile.DefaultBackendRecipeRelativePath ),
			BackendScriptPath = string.IsNullOrWhiteSpace( backendRoot )
				? string.Empty
				: Path.Combine( backendRoot, "tools", "blender", "retarget_job.py" ),
			BlenderExecutablePath = ResolveBlenderExecutable( settings, out var blenderSource ),
			BlenderSource = blenderSource,
			TargetReferencePath = ResolveTargetReferencePath()
		};
	}

	public static string BundledBackendRootPath => CitizenRetargetPaths.GetPluginAssetAbsolutePath( BundledBackendAssetPath );

	private static string ResolveBackendRoot( string configuredBackendRoot, out string source )
	{
		if ( !string.IsNullOrWhiteSpace( configuredBackendRoot ) )
		{
			source = "developer override";
			return configuredBackendRoot;
		}

		var bundledBackendRoot = BundledBackendRootPath;
		if ( Directory.Exists( bundledBackendRoot ) )
		{
			source = "bundled plugin backend";
			return bundledBackendRoot;
		}

		source = string.Empty;
		return string.Empty;
	}

	public static string ResolveTargetReferencePath()
	{
		foreach ( var assetPath in new[]
		{
			CitizenTargetProfile.CitizenFallbackBindAssetPath,
			CitizenTargetProfile.CitizenAnimationBindAssetPath
		} )
		{
			try
			{
				var asset = AssetSystem.FindByPath( assetPath );
				if ( asset is not null && File.Exists( asset.AbsolutePath ) )
					return asset.AbsolutePath;
			}
			catch
			{
				// The diagnostics layer reports the missing reference with a user-facing hint.
			}
		}

		return string.Empty;
	}

	private static string ResolveBlenderExecutable( RetargetToolSettings settings, out string source )
	{
		var envOverride = Environment.GetEnvironmentVariable( "CITIZEN_RETARGET_BLENDER" );
		if ( IsExistingFile( envOverride ) )
		{
			source = "CITIZEN_RETARGET_BLENDER";
			return envOverride!.Trim().Trim( '"' );
		}

		if ( IsExistingFile( settings.BlenderExecutablePath ) )
		{
			source = "tool settings";
			return settings.BlenderExecutablePath.Trim().Trim( '"' );
		}

		foreach ( var candidate in BlenderCandidatePaths )
		{
			if ( File.Exists( candidate ) )
			{
				source = "known install path";
				return candidate;
			}
		}

		try
		{
			using var process = new Process
			{
				StartInfo = new ProcessStartInfo
				{
					FileName = "where.exe",
					Arguments = "blender",
					RedirectStandardOutput = true,
					RedirectStandardError = true,
					UseShellExecute = false,
					CreateNoWindow = true
				}
			};
			process.Start();
			var output = process.StandardOutput.ReadToEnd();
			process.WaitForExit();
			var resolved = output
				.Split( [ '\r', '\n' ], StringSplitOptions.RemoveEmptyEntries )
				.FirstOrDefault( File.Exists );
			if ( !string.IsNullOrWhiteSpace( resolved ) )
			{
				source = "PATH";
				return resolved;
			}
		}
		catch
		{
			// Caller reports the friendly setup error.
		}

		source = string.Empty;
		return string.Empty;
	}

	private static bool IsExistingFile( string? path )
	{
		return !string.IsNullOrWhiteSpace( path ) && File.Exists( path.Trim().Trim( '"' ) );
	}

	private static string NormalizePath( string? path )
	{
		return (path ?? string.Empty).Trim().Trim( '"' );
	}
}