Editor/CitizenRetarget/RetargetSolveDiagnosticsWriter.cs
using System.Text;
using System.Text.Json;

namespace Editor.CitizenRetarget;

[Obsolete( "Deprecated solve-summary writer retained for historical troubleshooting only.", false )]
internal sealed class RetargetSolveDiagnosticsWriter
{
	private static readonly JsonSerializerOptions JsonOptions = new()
	{
		WriteIndented = true
	};

	public RetargetSolveArtifactResult Write( RetargetSolveDiagnostics diagnostics )
	{
		ArgumentNullException.ThrowIfNull( diagnostics );

		var diagnosticsFolder = CitizenRetargetPaths.GetProjectAbsolutePath( CitizenTargetProfile.SolveDiagnosticsRelativeFolder );
		var markdownPath = CitizenRetargetPaths.GetProjectAbsolutePath( CitizenTargetProfile.SolveSummaryMarkdownRelativePath );
		Directory.CreateDirectory( diagnosticsFolder );
		Directory.CreateDirectory( Path.GetDirectoryName( markdownPath )! );

		var jsonPath = Path.Combine( diagnosticsFolder, $"{diagnostics.SequenceName}.json" );
		File.WriteAllText( jsonPath, JsonSerializer.Serialize( diagnostics, JsonOptions ) );
		WriteMarkdownSummary( diagnosticsFolder, markdownPath );

		return new RetargetSolveArtifactResult
		{
			JsonAbsolutePath = jsonPath,
			MarkdownAbsolutePath = markdownPath
		};
	}

	private static void WriteMarkdownSummary( string diagnosticsFolder, string markdownPath )
	{
		var reports = Directory.GetFiles( diagnosticsFolder, "*.json", SearchOption.TopDirectoryOnly )
			.Select( path => JsonSerializer.Deserialize<RetargetSolveDiagnostics>( File.ReadAllText( path ) ) )
			.Where( report => report is not null )
			.Select( report => report! )
			.OrderBy( report => GetSortKey( report.SequenceName ) )
			.ThenBy( report => report.SequenceName, StringComparer.OrdinalIgnoreCase )
			.ToList();

		var builder = new StringBuilder();
		builder.AppendLine( "# CARL Solve Summary" );
		builder.AppendLine();
		builder.AppendLine( "- `DMX` is the active output path." );
		builder.AppendLine( "- `SMD` remains diagnostics-only." );
		builder.AppendLine();

		foreach ( var report in reports )
		{
			builder.AppendLine( $"## {report.SequenceName}" );
			builder.AppendLine();
			builder.AppendLine( $"- Source clip: `{report.SourceClipName}`" );
			builder.AppendLine( $"- Solve stage: `{report.Stage}`" );
			builder.AppendLine( $"- Source profile: `{report.SourceProfile}`" );
			builder.AppendLine( $"- Target bind asset: `{report.TargetBindAssetPath}`" );
			builder.AppendLine( $"- Source scale ratio: `{report.SourceScaleRatio:0.###}`" );
			builder.AppendLine( $"- Clip alignment translation: `{FormatArray( report.ClipAlignmentTranslation )}`" );
			builder.AppendLine( $"- Clip alignment rotation: `{FormatArray( report.ClipAlignmentRotation )}`" );
			builder.AppendLine( $"- Pelvis delta min/max: `{FormatArray( report.PelvisTranslationDeltaMin )}` / `{FormatArray( report.PelvisTranslationDeltaMax )}`" );
			builder.AppendLine( $"- Full target skeleton written every frame: `{YesNo( report.FullTargetSkeletonWrittenEveryFrame )}`" );
			builder.AppendLine( $"- Only pelvis local translation animated: `{YesNo( report.OnlyPelvisLocalTranslationAnimated )}`" );
			builder.AppendLine( $"- Unmapped bones changed from rest: `{YesNo( report.AnyUnmappedBoneReceivedNonRestLocal )}`" );
			builder.AppendLine( $"- Non-finite animated quaternions: `{YesNo( report.AnyAnimatedBoneReceivedNonFiniteQuaternion )}`" );
			builder.AppendLine( $"- Non-unit animated quaternions: `{YesNo( report.AnyAnimatedBoneReceivedNonUnitQuaternion )}`" );
			builder.AppendLine();
			builder.AppendLine( "Top angular deltas:" );
			foreach ( var bone in report.MappedBones
				.OrderByDescending( item => item.MaxAngularDeltaDegrees )
				.Take( 8 ) )
			{
				builder.AppendLine( $"- `{bone.BoneName}`: `{bone.MaxAngularDeltaDegrees:0.##} deg`" );
			}

			builder.AppendLine();
		}

		File.WriteAllText( markdownPath, builder.ToString() );
	}

	private static int GetSortKey( string sequenceName )
	{
		if ( sequenceName.Contains( "a_tpose", StringComparison.OrdinalIgnoreCase ) )
			return 0;
		if ( sequenceName.Contains( "walk_fwd_loop", StringComparison.OrdinalIgnoreCase ) )
			return 1;
		if ( sequenceName.Contains( "wallrun_l_loop", StringComparison.OrdinalIgnoreCase ) )
			return 2;

		return 3;
	}

	private static string YesNo( bool value ) => value ? "yes" : "no";

	private static string FormatArray( IReadOnlyList<float> values )
	{
		return string.Join( ", ", values.Select( value => value.ToString( "0.###" ) ) );
	}
}