Code/HumanoidRetargeter/Solve/RuntimePoseRetargeter.cs
#nullable enable annotations

using System;
using System.Collections.Generic;
using HumanoidRetargeter.Mapping;
using HumanoidRetargeter.Maths;
using HumanoidRetargeter.Skeleton;
using HumanoidRetargeter.Target;
using SkeletonModel = HumanoidRetargeter.Skeleton.Skeleton;

namespace HumanoidRetargeter.Solve;

/// <summary>
/// Cached, in-memory humanoid pose retargeter for live pose streams. It reuses the same
/// mapping, rest normalization, canonical frames, proportion handling and root transform
/// math as <see cref="GeometricSolver"/>, without creating clips or writing files per frame.
/// </summary>
/// <remarks>
/// A runtime instance owns reusable scratch buffers and is intentionally not thread-safe.
/// Create one instance per independently evaluated character/pose stream. Construction is
/// the expensive mapping/rig-analysis boundary; <see cref="TryRetarget"/> performs no
/// hierarchy scan, mapping rebuild, file IO, or successful-path heap allocation.
/// </remarks>
public sealed class RuntimePoseRetargeter
{
    private readonly GeometricSolver.Plan _plan;
    private readonly XForm[] _scratchDestination;

    /// <summary>The immutable source skeleton used to compile this retarget plan.</summary>
    public SkeletonModel SourceSkeleton { get; }

    /// <summary>The source role mapping used to compile this retarget plan.</summary>
    public MappingResult SourceMapping { get; }

    /// <summary>The target rig used to compile this retarget plan.</summary>
    public TargetRig TargetRig { get; }

    /// <summary>Number of local transforms required in every source pose.</summary>
    public int SourceBoneCount => SourceSkeleton.Count;

    /// <summary>Number of local transforms required in every destination pose.</summary>
    public int TargetBoneCount => TargetRig.Skeleton.Count;

    /// <summary>
    /// Compiles a reusable runtime solve plan.
    /// </summary>
    /// <param name="sourceSkeleton">Source hierarchy and rest pose, in centimeters.</param>
    /// <param name="sourceMapping">Canonical humanoid roles on the source hierarchy.</param>
    /// <param name="targetRig">Mapped target hierarchy.</param>
    /// <param name="initialSourcePose">
    /// Optional initial/reference pose for sources whose bind is non-anatomical (for example
    /// the SOMA/G1 stick-style capture path). Null uses the source rest pose.
    /// </param>
    /// <param name="options">Existing geometric solve options; null uses compatible defaults.</param>
    /// <param name="restPlacementAuthored">
    /// Whether source root placement shares the source rest coordinate space. Set false for
    /// absolute capture-volume streams that should be recentered like BVH input.
    /// </param>
    public RuntimePoseRetargeter(
        SkeletonModel sourceSkeleton,
        MappingResult sourceMapping,
        TargetRig targetRig,
        IReadOnlyList<XForm>? initialSourcePose = null,
        SolveOptions? options = null,
        bool restPlacementAuthored = true)
    {
        SourceSkeleton = sourceSkeleton ?? throw new ArgumentNullException(nameof(sourceSkeleton));
        SourceMapping = sourceMapping ?? throw new ArgumentNullException(nameof(sourceMapping));
        TargetRig = targetRig ?? throw new ArgumentNullException(nameof(targetRig));

        ValidateMapping(sourceSkeleton, sourceMapping);

        var reference = new XForm[sourceSkeleton.Count];
        if (initialSourcePose is null)
        {
            for (var i = 0; i < reference.Length; i++)
                reference[i] = sourceSkeleton[i].RestLocal;
        }
        else
        {
            if (initialSourcePose.Count != sourceSkeleton.Count)
                throw new ArgumentException(
                    $"Initial pose has {initialSourcePose.Count} bones but the source skeleton has {sourceSkeleton.Count}.",
                    nameof(initialSourcePose));
            for (var i = 0; i < reference.Length; i++)
                reference[i] = initialSourcePose[i];
        }

        var clip = new Clip("runtime_reference", 30f, false, new List<XForm[]> { reference });
        var scene = new SourceScene(sourceSkeleton, new[] { clip }, unitScaleCm: 1f)
        {
            RestPlacementAuthored = restPlacementAuthored,
        };

        _plan = new GeometricSolver.Plan(scene, clip, sourceMapping, targetRig, options ?? new SolveOptions());
        _scratchDestination = new XForm[targetRig.Skeleton.Count];
    }

    /// <summary>
    /// Retargets one parent-local source pose into a caller-owned parent-local destination.
    /// Throws for invalid buffer sizes or non-finite/degenerate input.
    /// </summary>
    public void Retarget(ReadOnlySpan<XForm> sourceLocals, Span<XForm> destinationLocals)
    {
        if (destinationLocals.Length < TargetBoneCount)
            throw new ArgumentException(
                $"Destination has {destinationLocals.Length} bones but the target skeleton requires {TargetBoneCount}.",
                nameof(destinationLocals));

        _plan.SolveFrameInto(sourceLocals, _scratchDestination);
        _scratchDestination.AsSpan().CopyTo(destinationLocals);
    }

    /// <summary>
    /// Safe runtime variant. On failure it returns false, reports an actionable diagnostic,
    /// and leaves <paramref name="destinationLocals"/> unchanged.
    /// </summary>
    public bool TryRetarget(
        ReadOnlySpan<XForm> sourceLocals,
        Span<XForm> destinationLocals,
        out string? error)
    {
        if (sourceLocals.Length != SourceBoneCount)
        {
            error = $"Source pose has {sourceLocals.Length} bones; expected {SourceBoneCount}.";
            return false;
        }

        if (destinationLocals.Length < TargetBoneCount)
        {
            error = $"Destination pose has {destinationLocals.Length} bones; expected at least {TargetBoneCount}.";
            return false;
        }

        try
        {
            _plan.SolveFrameInto(sourceLocals, _scratchDestination);
            _scratchDestination.AsSpan().CopyTo(destinationLocals);
            error = null;
            return true;
        }
        catch (Exception ex) when (ex is ArgumentException or InvalidOperationException)
        {
            error = $"Runtime retarget failed: {ex.Message}";
            return false;
        }
    }

    /// <summary>
    /// Builds a target rig from the library's existing automatic humanoid mapper and then
    /// compiles a runtime plan. This is an additive convenience for mocap, VR and procedural
    /// animation consumers; it has no dependency on any motion-generation package.
    /// </summary>
    public static bool TryCreateAutoMapped(
        SkeletonModel sourceSkeleton,
        MappingResult sourceMapping,
        SkeletonModel targetSkeleton,
        out RuntimePoseRetargeter? retargeter,
        out string? error,
        IReadOnlyList<XForm>? initialSourcePose = null,
        SolveOptions? options = null)
    {
        retargeter = null;
        error = null;
        try
        {
            var targetMap = AutoMapper.Map(targetSkeleton);
            if (!HasMinimumHumanoidRoles(targetMap))
            {
                error = "Target skeleton could not be mapped as a humanoid (hips, head, hands, and feet are required).";
                return false;
            }

            var targetRig = TargetRig.FromSkeleton(targetSkeleton, targetMap);
            retargeter = new RuntimePoseRetargeter(
                sourceSkeleton, sourceMapping, targetRig, initialSourcePose, options);
            return true;
        }
        catch (Exception ex) when (ex is ArgumentException or InvalidOperationException)
        {
            error = $"Could not create runtime retargeter: {ex.Message}";
            return false;
        }
    }

    private static void ValidateMapping(SkeletonModel skeleton, MappingResult mapping)
    {
        foreach (var (role, bone) in mapping.RoleToBone)
        {
            if ((uint)bone >= (uint)skeleton.Count)
                throw new ArgumentException(
                    $"Source mapping assigns {role} to bone {bone}, outside the source skeleton (count {skeleton.Count}).",
                    nameof(mapping));
        }

        if (!mapping.RoleToBone.ContainsKey(BoneRole.Hips))
            throw new ArgumentException("Source mapping must include the Hips role.", nameof(mapping));
    }

    private static bool HasMinimumHumanoidRoles(MappingResult mapping)
        => mapping.RoleToBone.ContainsKey(BoneRole.Hips)
           && mapping.RoleToBone.ContainsKey(BoneRole.Head)
           && mapping.RoleToBone.ContainsKey(BoneRole.HandL)
           && mapping.RoleToBone.ContainsKey(BoneRole.HandR)
           && mapping.RoleToBone.ContainsKey(BoneRole.FootL)
           && mapping.RoleToBone.ContainsKey(BoneRole.FootR);
}