Editor/CitizenRetarget/CitizenRetargetModels.cs
#nullable enable

namespace Editor.CitizenRetarget;

using NQuaternion = System.Numerics.Quaternion;
using NVector3 = System.Numerics.Vector3;

internal sealed class RetargetClipDescriptor
{
	public string SourceName { get; set; } = string.Empty;
	public string DisplayName { get; set; } = string.Empty;
	public double TimeBegin { get; set; }
	public double TimeEnd { get; set; }
	public double FrameRate { get; set; }
	public int FrameCount { get; set; }

	public override string ToString() => $"{DisplayName} ({FrameCount}f @ {FrameRate:0.##}fps)";
}

internal sealed class RetargetImportResult
{
	public string SequenceName { get; set; } = string.Empty;
	public string GeneratedClipAbsolutePath { get; set; } = string.Empty;
	public string SourcePreviewClipAbsolutePath { get; set; } = string.Empty;
	public string PostImportError { get; set; } = string.Empty;
	public RetargetOutputFormat OutputFormat { get; set; }
	public string VmdlAbsolutePath { get; set; } = string.Empty;
	public string VmdlResourcePath { get; set; } = string.Empty;
	public string SourcePreviewVmdlAbsolutePath { get; set; } = string.Empty;
	public string SourcePreviewVmdlResourcePath { get; set; } = string.Empty;
	public string JobAbsolutePath { get; set; } = string.Empty;
	public string AuditMarkdownPath { get; set; } = string.Empty;
	public string SolveDiagnosticsJsonPath { get; set; } = string.Empty;
	public string SolveSummaryMarkdownPath { get; set; } = string.Empty;
	public string DiagnosticsVmdlAbsolutePath { get; set; } = string.Empty;
	public string ManifestAbsolutePath { get; set; } = string.Empty;
	public RetargetRunManifest Manifest { get; set; } = new();
	public RetargetPreviewArtifacts PreviewArtifacts { get; set; } = new();
	public RetargetArtifactLinks ArtifactLinks { get; set; } = new();
	public RetargetBackendInvocation BackendInvocation { get; set; } = new();
	public string Log { get; set; } = string.Empty;
}

internal enum RetargetOutputFormat
{
	// Deprecated diagnostic path retained for historical SMD experiments.
	SmdDebug,
	// Deprecated diagnostic path retained for historical DMX experiments.
	DmxSpike,
	FbxBackend
}

internal sealed class GeneratedAnimationSource
{
	public string SequenceName { get; set; } = string.Empty;
	public string ResourcePath { get; set; } = string.Empty;
	public bool Looping { get; set; }
}

internal sealed class NativeScanResult
{
	public string Error { get; set; } = string.Empty;
	public string SourcePath { get; set; } = string.Empty;
	public double UnitMeters { get; set; }
	public double OutputUnitMeters { get; set; }
	public double FrameRate { get; set; }
	public List<RetargetClipDescriptor> Clips { get; set; } = new();
}

internal sealed class NativeSampleResult
{
	public string Error { get; set; } = string.Empty;
	public string SourcePath { get; set; } = string.Empty;
	public string TargetPath { get; set; } = string.Empty;
	public string ClipName { get; set; } = string.Empty;
	public string DisplayName { get; set; } = string.Empty;
	public string SourceProfile { get; set; } = string.Empty;
	public string TargetProfile { get; set; } = string.Empty;
	public double SourceUnitMeters { get; set; }
	public double SourceOutputUnitMeters { get; set; }
	public double TargetUnitMeters { get; set; }
	public double TargetOutputUnitMeters { get; set; }
	public double FrameRate { get; set; }
	public int FrameCount { get; set; }
	public List<NativeBoneInfo> SourceBones { get; set; } = new();
	public List<NativeBoneInfo> TargetBones { get; set; } = new();
	public List<NativeFrameInfo> Frames { get; set; } = new();
}

internal sealed class NativeSceneAuditResult
{
	public string Error { get; set; } = string.Empty;
	public string ScenePath { get; set; } = string.Empty;
	public string Profile { get; set; } = string.Empty;
	public double UnitMeters { get; set; }
	public double OutputUnitMeters { get; set; }
	public double FrameRate { get; set; }
	public NativeSceneMetadata Metadata { get; set; } = new();
	public List<NativeAuditBoneInfo> Bones { get; set; } = new();
}

internal sealed class NativeSceneMetadata
{
	public string Creator { get; set; } = string.Empty;
	public NativeApplicationInfo OriginalApplication { get; set; } = new();
	public NativeApplicationInfo LatestApplication { get; set; } = new();
}

internal sealed class NativeApplicationInfo
{
	public string Name { get; set; } = string.Empty;
	public string Vendor { get; set; } = string.Empty;
	public string Version { get; set; } = string.Empty;
}

internal sealed class NativeAuditBoneInfo
{
	public string Name { get; set; } = string.Empty;
	public string ParentName { get; set; } = string.Empty;
	public float[] Translation { get; set; } = new[] { 0f, 0f, 0f };
	public float[] Rotation { get; set; } = new[] { 0f, 0f, 0f, 1f };
	public float[] Scale { get; set; } = new[] { 1f, 1f, 1f };
	public float[] WorldTranslation { get; set; } = new[] { 0f, 0f, 0f };
	public float[] WorldRotation { get; set; } = new[] { 0f, 0f, 0f, 1f };

	public BoneTransform LocalTransform => new( Translation.AsVector3(), Rotation.AsQuaternion() );
	public WorldTransform WorldTransform => new( WorldTranslation.AsVector3(), WorldRotation.AsQuaternion() );
}

internal sealed class NativeBoneInfo
{
	public string Name { get; set; } = string.Empty;
	public string ParentName { get; set; } = string.Empty;
	public float[] Translation { get; set; } = new[] { 0f, 0f, 0f };
	public float[] Rotation { get; set; } = new[] { 0f, 0f, 0f, 1f };

	public BoneTransform LocalTransform => new( Translation.AsVector3(), Rotation.AsQuaternion() );
}

internal sealed class NativeFrameInfo
{
	public int Index { get; set; }
	public double Time { get; set; }
	public List<float[]> Translations { get; set; } = new();
	public List<float[]> Rotations { get; set; } = new();
}

internal sealed class BoneMapEntry
{
	public string SourceBone { get; set; } = string.Empty;
	public string TargetBone { get; set; } = string.Empty;
}

internal enum TargetBindProfile
{
	Current,
	ModifyGeometry,
	RawUnits,
	TargetOnlyControl
}

internal enum RetargetSolveStage
{
	BodyOnly,
	BodyAndRootMotion,
	BodyRootAndIk,
	Full
}

internal sealed class RetargetSolveOptions
{
	public RetargetSolveStage Stage { get; set; } = RetargetSolveStage.BodyOnly;
	public CitizenRetargetRootMotionMode RootMotionMode { get; set; } = CitizenRetargetRootMotionMode.Keep;
	public bool EnableLocalCompensation { get; set; }

	public bool IncludeIkTargets => Stage >= RetargetSolveStage.BodyRootAndIk;
	public bool IncludeFingers => Stage == RetargetSolveStage.Full;
	public bool IncludeRootMotionPolicy => Stage >= RetargetSolveStage.BodyAndRootMotion;
}

internal sealed class RetargetAuditConfig
{
	public string SourceFbxAbsolutePath { get; set; } = string.Empty;
	public string TargetBindAssetPath { get; set; } = string.Empty;
	public string FallbackTargetBindAssetPath { get; set; } = string.Empty;
	public string DiagnosticsAssetFolder { get; set; } = string.Empty;
	public string DiagnosticsVmdlPath { get; set; } = string.Empty;
	public string AuditJsonPath { get; set; } = string.Empty;
	public string AuditMarkdownPath { get; set; } = string.Empty;
	public List<TargetBindProfile> Profiles { get; set; } = new()
	{
		TargetBindProfile.Current,
		TargetBindProfile.ModifyGeometry,
		TargetBindProfile.RawUnits,
		TargetBindProfile.TargetOnlyControl
	};
}

internal sealed class RetargetAuditReport
{
	public string SourcePath { get; set; } = string.Empty;
	public TargetBindProfile LockedSourceProfile { get; set; }
	public bool HasLockedSourceProfile { get; set; }
	public double LockedSourceOutputUnitMeters { get; set; }
	public string LockedTargetBindAssetPath { get; set; } = string.Empty;
	public string LockedTargetBindAbsolutePath { get; set; } = string.Empty;
	public TargetBindProfile LockedTargetProfile { get; set; }
	public bool FallbackTargetWasUsed { get; set; }
	public bool HasLockedTargetProfile { get; set; }
	public string AuditJsonPath { get; set; } = string.Empty;
	public string AuditMarkdownPath { get; set; } = string.Empty;
	public string DiagnosticsVmdlAbsolutePath { get; set; } = string.Empty;
	public List<RetargetAuditSceneReport> SceneReports { get; set; } = new();
	public List<string> Warnings { get; set; } = new();
	public List<NativeBoneInfo> LockedSourceBones { get; set; } = new();
	public List<NativeBoneInfo> LockedTargetBones { get; set; } = new();
	public double LockedTargetOutputUnitMeters { get; set; }
}

internal sealed class RetargetDiagnosticResult
{
	public string DiagnosticsFolderAbsolutePath { get; set; } = string.Empty;
	public string DiagnosticsVmdlAbsolutePath { get; set; } = string.Empty;
	public List<string> GeneratedClipPaths { get; set; } = new();
}

internal sealed class DmxReferenceSpikeResult
{
	public string ReferenceDmxAbsolutePath { get; set; } = string.Empty;
	public string ReferenceVmdlAbsolutePath { get; set; } = string.Empty;
	public string StockSummaryJsonAbsolutePath { get; set; } = string.Empty;
	public string ReferenceSummaryJsonAbsolutePath { get; set; } = string.Empty;
	public string MarkdownAbsolutePath { get; set; } = string.Empty;
	public string BindCacheJsonAbsolutePath { get; set; } = string.Empty;
}

internal sealed class BlenderTargetBindCache
{
	public string JsonAbsolutePath { get; set; } = string.Empty;
	public double OutputUnitMeters { get; set; } = 0.01;
	public List<NativeBoneInfo> Bones { get; set; } = new();
}

internal sealed class BlenderForensicArtifactResult
{
	public string PayloadJsonAbsolutePath { get; set; } = string.Empty;
	public string AppliedPoseJsonAbsolutePath { get; set; } = string.Empty;
	public string ReimportedPoseJsonAbsolutePath { get; set; } = string.Empty;
	public string SummaryJsonAbsolutePath { get; set; } = string.Empty;
	public string SummaryMarkdownAbsolutePath { get; set; } = string.Empty;
	public string GeneratedDmxAbsolutePath { get; set; } = string.Empty;
}

internal sealed class BindPassthroughDebugResult
{
	public string PayloadJsonAbsolutePath { get; set; } = string.Empty;
	public string DmxAbsolutePath { get; set; } = string.Empty;
	public string VmdlAbsolutePath { get; set; } = string.Empty;
}

internal sealed class StockRoundTripDebugResult
{
	public string SourceDmxAbsolutePath { get; set; } = string.Empty;
	public string RoundTripDmxAbsolutePath { get; set; } = string.Empty;
	public string VmdlAbsolutePath { get; set; } = string.Empty;
}

internal sealed class RetargetAuditSceneReport
{
	public string Kind { get; set; } = string.Empty;
	public string AssetPath { get; set; } = string.Empty;
	public string AbsolutePath { get; set; } = string.Empty;
	public TargetBindProfile Profile { get; set; }
	public double SceneUnitMeters { get; set; }
	public double OutputUnitMeters { get; set; }
	public string Creator { get; set; } = string.Empty;
	public string OriginalApplication { get; set; } = string.Empty;
	public string LatestApplication { get; set; } = string.Empty;
	public float ApproximateHeightCm { get; set; }
	public float PlausibilityScore { get; set; }
	public bool IsBelievable { get; set; }
	public List<RetargetAuditBoneReport> CoreBones { get; set; } = new();
}

internal sealed class RetargetAuditBoneReport
{
	public string Name { get; set; } = string.Empty;
	public string ParentName { get; set; } = string.Empty;
	public float LocalLengthCm { get; set; }
	public float[] LocalTranslation { get; set; } = new[] { 0f, 0f, 0f };
	public float[] LocalRotation { get; set; } = new[] { 0f, 0f, 0f, 1f };
	public float[] LocalScale { get; set; } = new[] { 1f, 1f, 1f };
	public float[] WorldTranslation { get; set; } = new[] { 0f, 0f, 0f };
	public float[] WorldRotation { get; set; } = new[] { 0f, 0f, 0f, 1f };
}

internal sealed class RetargetedClip
{
	public string SourceClipName { get; set; } = string.Empty;
	public string SequenceName { get; set; } = string.Empty;
	public string DisplayName { get; set; } = string.Empty;
	public bool Looping { get; set; }
	public double FrameRate { get; set; }
	public List<RetargetedBone> Bones { get; set; } = new();
	public List<RetargetedFrame> Frames { get; set; } = new();
}

internal sealed class RetargetSolveResult
{
	public RetargetedClip Clip { get; set; } = new();
	public RetargetSolveDiagnostics Diagnostics { get; set; } = new();
}

internal sealed class RetargetSolveDiagnostics
{
	public string SourceClipName { get; set; } = string.Empty;
	public string DisplayName { get; set; } = string.Empty;
	public string SequenceName { get; set; } = string.Empty;
	public string SourceProfile { get; set; } = string.Empty;
	public string TargetBindAssetPath { get; set; } = string.Empty;
	public RetargetSolveStage Stage { get; set; }
	public int FrameCount { get; set; }
	public float SourceScaleRatio { get; set; }
	public float[] ClipAlignmentTranslation { get; set; } = new[] { 0f, 0f, 0f };
	public float[] ClipAlignmentRotation { get; set; } = new[] { 0f, 0f, 0f, 1f };
	public float[] PelvisTranslationDeltaMin { get; set; } = new[] { 0f, 0f, 0f };
	public float[] PelvisTranslationDeltaMax { get; set; } = new[] { 0f, 0f, 0f };
	public bool FullTargetSkeletonWrittenEveryFrame { get; set; }
	public bool AnyUnmappedBoneReceivedNonRestLocal { get; set; }
	public bool AnyAnimatedBoneReceivedNonFiniteQuaternion { get; set; }
	public bool AnyAnimatedBoneReceivedNonUnitQuaternion { get; set; }
	public bool OnlyPelvisLocalTranslationAnimated { get; set; }
	public bool UnexpectedScaleValuesDetected { get; set; }
	public List<RetargetBoneSolveDiagnostics> MappedBones { get; set; } = new();
}

internal sealed class RetargetBoneSolveDiagnostics
{
	public string BoneName { get; set; } = string.Empty;
	public float MaxAngularDeltaDegrees { get; set; }
}

internal sealed class RetargetSolveArtifactResult
{
	public string JsonAbsolutePath { get; set; } = string.Empty;
	public string MarkdownAbsolutePath { get; set; } = string.Empty;
}

internal sealed class RetargetedBone
{
	public int Id { get; set; }
	public string Name { get; set; } = string.Empty;
	public int ParentId { get; set; }
	public BoneTransform RestLocal { get; set; }
}

internal sealed class RetargetedFrame
{
	public int Index { get; set; }
	public List<BoneTransform> BoneTransforms { get; set; } = new();
}

internal enum SmdRotationContract
{
	RawLocal,
	InverseLocal,
	LegacyValve,
	LegacyValveInverse,
	LegacyValveBasis,
	LegacyValveBasisInverse,
	RawLocalRootYUp,
	InverseLocalRootYUp
}

internal sealed class SmdWriteOptions
{
	public SmdRotationContract RotationContract { get; set; } = SmdRotationContract.RawLocal;
}

internal interface IRetargetOutputWriter
{
	RetargetOutputFormat Format { get; }
	string FileExtension { get; }
	string WriteClip( RetargetedClip clip, string outputFolder );
}

internal readonly struct BoneTransform
{
	public BoneTransform( NVector3 translation, NQuaternion rotation )
	{
		Translation = translation;
		Rotation = rotation;
	}

	public NVector3 Translation { get; }
	public NQuaternion Rotation { get; }
}

internal readonly struct WorldTransform
{
	public WorldTransform( NVector3 translation, NQuaternion rotation )
	{
		Translation = translation;
		Rotation = rotation;
	}

	public NVector3 Translation { get; }
	public NQuaternion Rotation { get; }
}

internal static class RetargetMath
{
	public static NQuaternion Normalize( NQuaternion rotation )
	{
		return rotation.LengthSquared() <= 0.000001f
			? NQuaternion.Identity
			: NQuaternion.Normalize( rotation );
	}

	public static WorldTransform Compose( WorldTransform? parent, BoneTransform local )
	{
		if ( parent is null )
			return new WorldTransform( local.Translation, Normalize( local.Rotation ) );

		var parentValue = parent.Value;
		var worldRotation = Normalize( parentValue.Rotation * local.Rotation );
		var worldTranslation = parentValue.Translation + NVector3.Transform( local.Translation, parentValue.Rotation );
		return new WorldTransform( worldTranslation, worldRotation );
	}

	public static BoneTransform ToLocal( WorldTransform? parent, WorldTransform world )
	{
		if ( parent is null )
			return new BoneTransform( world.Translation, Normalize( world.Rotation ) );

		var parentValue = parent.Value;
		var inverseParentRotation = Normalize( NQuaternion.Inverse( parentValue.Rotation ) );
		var localTranslation = NVector3.Transform( world.Translation - parentValue.Translation, inverseParentRotation );
		var localRotation = Normalize( inverseParentRotation * world.Rotation );
		return new BoneTransform( localTranslation, localRotation );
	}

	public static NVector3 TransformPointInverse( WorldTransform transform, NVector3 point )
	{
		return NVector3.Transform( point - transform.Translation, Normalize( NQuaternion.Inverse( transform.Rotation ) ) );
	}

	public static float LengthOrZero( NVector3 value ) => value.LengthSquared() > 0.000001f ? value.Length() : 0f;

	public static float CentimetersPerUnit( double outputUnitMeters )
	{
		return outputUnitMeters > 0.000001
			? (float)(outputUnitMeters / 0.01)
			: 1f;
	}

	public static BoneTransform ScaleTranslation( BoneTransform transform, float centimetersPerUnit )
	{
		return centimetersPerUnit == 1f
			? transform
			: new BoneTransform( transform.Translation * centimetersPerUnit, transform.Rotation );
	}

	public static WorldTransform ScaleTranslation( WorldTransform transform, float centimetersPerUnit )
	{
		return centimetersPerUnit == 1f
			? transform
			: new WorldTransform( transform.Translation * centimetersPerUnit, transform.Rotation );
	}

	public static WorldTransform Multiply( WorldTransform first, WorldTransform second )
	{
		var translation = first.Translation + NVector3.Transform( second.Translation, first.Rotation );
		var rotation = Normalize( first.Rotation * second.Rotation );
		return new WorldTransform( translation, rotation );
	}

	public static WorldTransform Inverse( WorldTransform transform )
	{
		var inverseRotation = Normalize( NQuaternion.Inverse( transform.Rotation ) );
		var inverseTranslation = NVector3.Transform( -transform.Translation, inverseRotation );
		return new WorldTransform( inverseTranslation, inverseRotation );
	}

	public static WorldTransform Transform( WorldTransform frame, WorldTransform value )
	{
		return Multiply( frame, value );
	}

	public static NQuaternion CreateRotationFromBasis( NVector3 right, NVector3 forward, NVector3 up )
	{
		var matrix = new System.Numerics.Matrix4x4(
			right.X, forward.X, up.X, 0f,
			right.Y, forward.Y, up.Y, 0f,
			right.Z, forward.Z, up.Z, 0f,
			0f, 0f, 0f, 1f );
		return Normalize( NQuaternion.CreateFromRotationMatrix( matrix ) );
	}

	public static float QuaternionAngleDegrees( NQuaternion rotation )
	{
		var normalized = Normalize( rotation );
		var halfAngle = MathF.Acos( Math.Clamp( MathF.Abs( normalized.W ), 0f, 1f ) );
		return halfAngle * 2f * (180f / MathF.PI);
	}
}

internal static class RetargetArrayExtensions
{
	public static NVector3 AsVector3( this float[] values )
	{
		if ( values is null || values.Length < 3 )
			return NVector3.Zero;

		return new NVector3( values[0], values[1], values[2] );
	}

	public static NQuaternion AsQuaternion( this float[] values )
	{
		if ( values is null || values.Length < 4 )
			return NQuaternion.Identity;

		return RetargetMath.Normalize( new NQuaternion( values[0], values[1], values[2], values[3] ) );
	}
}