Editor/Core/Rigging/SkeletonSolver.cs
#nullable enable annotations
using System.Numerics;
namespace HumanoidRigger;
using Vector3 = System.Numerics.Vector3;

public sealed record RigBone(string Role,string Name,int Parent,Vector3 Position,Quaternion Rotation,bool Deform);
public readonly record struct Influence(int Bone,float Weight);
public sealed class GeneratedRig
{
    public RigProfile Profile {get;init;}=new();
    public RigBone[] Bones {get;init;}=[];
    public Influence[][][] Weights {get;set;}=[];
    public ValidationReport Report {get;set;}=new();
    public Anatomy? Anatomy {get;init;}
}
public static class RigGeometry
{
    internal static Vector3? AnatomicalEnd(Anatomy? anatomy,string role)
    {
        if(anatomy is null)return null;
        if(role=="Head")return anatomy.HeadEnd;
        if(role.StartsWith("Hand.")&&anatomy.HandEnds?.TryGetValue(role[^1..],out var palm)==true)return palm;
        if(role.StartsWith("Toe.")&&anatomy.FootEnds?.TryGetValue(role[^1..],out var foot)==true)return foot;
        if(!role.EndsWith(".L")&&!role.EndsWith(".R"))return null;
        foreach(string finger in Profiles.Fingers)foreach(int joint in Enumerable.Range(1,3))
            if(role==finger+joint+"."+role[^1..]&&anatomy.Points.TryGetValue(finger+"Tip."+role[^1..],out var tip))return tip.Position;
        return null;
    }
    /// <summary>Skinning segments follow anatomy, independent of profile child ordering.</summary>
    public static Vector3[] SegmentEnds(GeneratedRig rig)=>rig.Bones.Select((bone,index)=>
    {
        var children=rig.Bones.Where(b=>b.Parent==index&&b.Deform).ToArray();
        if(children.Length==0&&AnatomicalEnd(rig.Anatomy,bone.Role) is {} terminal)return terminal;
        if(bone.Role.StartsWith("Hand."))
        {
            string side=bone.Role[^1..];
            var knuckles=children.Where(b=>Profiles.Fingers.Where(f=>f!="Thumb").Any(f=>b.Role==f+"1."+side)).ToArray();
            if(knuckles.Length>0)return Geometry.Mean(knuckles.Select(b=>b.Position));
        }
        return children.FirstOrDefault()?.Position??bone.Position;
    }).ToArray();
}
public static class SkeletonSolver
{
    public static GeneratedRig Fit(ImportedCharacter character,Anatomy anatomy,RigProfile profile)
    {
        var changes=anatomy.GeometricHandPoints.Where(p=>anatomy.Points.TryGetValue(p.Key,out var current)&&!current.Corrected&&current.Position!=p.Value.Position).ToArray();
        if(changes.Length==0)return FitGeometry(character,anatomy,profile);
        var geometry=anatomy.Copy();
        foreach(var point in changes)geometry.Points[point.Key]=point.Value;
        geometry.GeometricHandPoints.Clear();
        var baseline=FitGeometry(character,geometry,profile);
        // Reuse validated geometry weights before considering a second full solve.
        // A small visual prior must not disturb unrelated body weight candidates.
        var proposal=CreateSkeleton(anatomy,profile);
        proposal.Weights=baseline.Weights.Select(p=>p.Select(w=>w.ToArray()).ToArray()).ToArray();
        proposal.Report=RigValidator.ValidateAndRepair(character,proposal);
        if(AcceptHandPrior(proposal,baseline))return proposal;
        if(!baseline.Report.Passed)
        {
            proposal=FitGeometry(character,anatomy,profile);
            if(AcceptHandPrior(proposal,baseline))return proposal;
        }
        foreach(string side in changes.Select(p=>p.Key[^1..]).Distinct())
            if(baseline.Anatomy!.HandRefinements.TryGetValue(side,out var report))
                baseline.Anatomy.HandRefinements[side]=report with{Status="Geometry retained after deformation validation",AdjustedJoints=0};
        return baseline;
    }
    static bool AcceptHandPrior(GeneratedRig candidate,GeneratedRig baseline)
    {
        var roles=candidate.Bones.Select(b=>b.Role).ToHashSet();
        var expected=Deformation.Poses.Where(p=>Deformation.IsApplicable(p,roles)).Select(p=>p.Name).Order().ToArray();
        if(!candidate.Report.Passed||!WeightRepair.HasCompleteEvidence(candidate.Report,expected)||candidate.Report.StressTests.Any(t=>t.MaximumStretch>4||t.MinimumAreaRatio<.025f))return false;
        if(!baseline.Report.Passed)return true;
        if(!WeightRepair.HasCompleteEvidence(baseline.Report,expected))return false;
        return candidate.Report.StressTests.Zip(baseline.Report.StressTests).All(p=>p.First.Pose==p.Second.Pose&&p.First.ReversedTriangles<=p.Second.ReversedTriangles&&p.First.ReversedAreaFraction<=p.Second.ReversedAreaFraction+1e-7f);
    }
    static GeneratedRig CreateSkeleton(Anatomy anatomy,RigProfile profile)
    {
        profile.Validate();var bones=new List<RigBone>();var ids=new Dictionary<string,int>();
        foreach(var definition in profile.Bones)
        {
            if(!anatomy.Points.TryGetValue(definition.Role,out var p))
            {
                if(definition.Placement is {} placement && anatomy.Points.TryGetValue(placement.StartRole,out var start) && anatomy.Points.TryGetValue(placement.EndRole,out var end))
                    p=new Landmark(definition.Role,Vector3.Lerp(start.Position,end.Position,placement.Fraction),Math.Min(start.Confidence,end.Confidence));
                else
                {
                if(definition.Required) throw new InvalidOperationException($"Missing required landmark: {definition.Role}");
                continue;
                }
            }
            int parent=definition.Parent is null ? -1 : ids.GetValueOrDefault(definition.Parent,-1);
            if(definition.Parent is not null && parent<0) { if(!definition.Required)continue;throw new InvalidOperationException("Required bone parent is missing."); }
            var child=profile.Bones.FirstOrDefault(b=>b.Parent==definition.Role && anatomy.Points.ContainsKey(b.Role));
            var aim=child is null ? (parent<0 ? Vector3.UnitY : p.Position-bones[parent].Position) : anatomy[child.Role]-p.Position;
            if(child is null&&RigGeometry.AnatomicalEnd(anatomy,definition.Role) is {} terminal)aim=terminal-p.Position;
            HandFrame? handFrame=null;
            if(definition.Role.StartsWith("Hand.")||Profiles.Fingers.Any(f=>definition.Role.StartsWith(f)))
                anatomy.Hands.TryGetValue(definition.Role.EndsWith(".L")?"L":"R",out handFrame);
            // A palm aims along the fingers, not toward its first child (often
            // the thumb base, which may lie behind the reviewed wrist).
            if(definition.Role.StartsWith("Hand.")&&handFrame is not null)aim=handFrame.Forward;
            if(aim.LengthSquared()<1e-8f) aim=Vector3.UnitY;
            var rotation=Frame(aim,definition.AimAxis,definition.Roll,handFrame?.Normal);
            ids.Add(definition.Role,bones.Count);bones.Add(new(definition.Role,definition.Name,parent,p.Position,rotation,definition.Deform));
        }
        return new GeneratedRig{Profile=profile,Bones=bones.ToArray(),Anatomy=anatomy};
    }
    static GeneratedRig FitGeometry(ImportedCharacter character,Anatomy anatomy,RigProfile profile)
    {
        var rig=CreateSkeleton(anatomy,profile);
        // Heat depends on geometry and the fitted skeleton, not trial weights.
        // Keep its immutable candidates within this fit so body and finger repair
        // share one solve; validation receives writable copies below.
        var heat=new Lazy<Influence[][][][]>(()=>HeatSkinning.Candidates(character,rig).ToArray());
        var skinning=new Skinning.SolveCache(character);
        var validation=new ValidationGeometry(character);
        rig.Weights=Skinning.Solve(character,rig,skinning);
        rig.Report=RigValidator.ValidateAndRepair(character,rig,validation);
        rig.Report=FingerWeightRepair.Improve(character,rig,rig.Report,()=>heat.Value[0]);
        var repaired=rig.Weights;
        var failed=TrySkinningCandidates(character,rig,skinning,()=>heat.Value,validation);
        if(rig.Weights!=repaired)rig.Report=FingerWeightRepair.Improve(character,rig,rig.Report,()=>heat.Value[0]);
        var refined=JointWeightRepair.Improve(character,rig.Report.Passed?rig:failed??rig,validation);
        return refined.Report.Passed?refined:rig;
    }
    static GeneratedRig? TrySkinningCandidates(ImportedCharacter character,GeneratedRig rig,Skinning.SolveCache skinning,Func<Influence[][][][]> heatCandidates,ValidationGeometry validation)
    {
        if((rig.Report.Passed&&rig.Report.StressTests.All(t=>t.ReversedTriangles==0))||rig.Report.Issues.Any(i=>i.Error&&i.Code is not ("deformation" or "weight-region")))return null;
        var bestWeights=rig.Weights;var bestReport=rig.Report;bool initiallyPassed=bestReport.Passed;
        var roles=rig.Bones.Select(b=>b.Role).ToHashSet();var expectedPoses=Deformation.Poses.Where(p=>Deformation.IsApplicable(p,roles)).Select(p=>p.Name).Order().ToArray();
        GeneratedRig? failed=null;double failureScore=double.PositiveInfinity;
        try
        {
            foreach(var weights in Candidates())
            {
                rig.Weights=weights;
                var candidate=RigValidator.ValidateAndRepair(character,rig,validation);
                if(!candidate.Passed&&WeightRepair.HasCompleteEvidence(candidate,expectedPoses))
                {
                    double score=candidate.StressTests.Sum(t=>Math.Max(0,t.MaximumStretch/4-1)+Math.Max(0,1-t.MinimumAreaRatio/.025f));
                    if(score<failureScore){failureScore=score;failed=new(){Profile=rig.Profile,Bones=rig.Bones.ToArray(),Anatomy=rig.Anatomy?.Copy(),Weights=rig.Weights,Report=candidate};}
                }
                if(!BetterSkinning(candidate,bestReport,expectedPoses))continue;
                candidate.RepairPasses++;
                bestWeights=rig.Weights;bestReport=candidate;
                if(candidate.StressTests.All(t=>t.ReversedTriangles==0))break;
            }
        }
        finally{rig.Weights=bestWeights;rig.Report=bestReport;}
        return failed;
        IEnumerable<Influence[][][]> Candidates()
        {
            // A technically valid rig can still fold. Try the existing volumetric
            // candidate before more envelope variants when those folds persist.
            if(initiallyPassed)foreach(var weights in Heat())yield return weights;
            foreach(var (locality,regions) in new[]{(true,false),(false,true),(true,true)})
                yield return Skinning.Solve(character,rig,skinning,useLocalityPrior:locality,useRegionSeeds:regions);
            if(!initiallyPassed)foreach(var weights in Heat())yield return weights;
        }
        IEnumerable<Influence[][][]> Heat()
        {
            Influence[][][][] candidates;
            try{candidates=heatCandidates();}
            catch(InvalidOperationException e)
            {
                bestReport.Issues.Add(new("skinning-candidate",e.Message,false));candidates=[];
            }
            foreach(var weights in candidates)yield return weights.Select(p=>p.Select(v=>(Influence[])v.Clone()).ToArray()).ToArray();
        }
    }
    internal static bool BetterSkinning(ValidationReport candidate,ValidationReport current,string[] expectedPoses)
    {
        if(!candidate.Passed||!WeightRepair.HasCompleteEvidence(candidate,expectedPoses)||candidate.StressTests.Any(t=>t.MaximumStretch>4||t.MinimumAreaRatio<.025f))return false;
        if(!current.Passed)return true;
        if(!WeightRepair.HasCompleteEvidence(current,expectedPoses))return false;
        if(!candidate.StressTests.Select(t=>t.Pose).SequenceEqual(current.StressTests.Select(t=>t.Pose)))return false;
        return candidate.StressTests.Zip(current.StressTests).All(p=>p.First.ReversedTriangles<=p.Second.ReversedTriangles&&p.First.ReversedAreaFraction<=p.Second.ReversedAreaFraction+1e-7f)&&
            candidate.StressTests.Sum(t=>t.ReversedTriangles)<current.StressTests.Sum(t=>t.ReversedTriangles);
    }
    public static Quaternion Frame(Vector3 direction,string aimAxis,float roll,Vector3? planeNormal=null)
    {
        var aim=Vector3.Normalize(direction);var reference=planeNormal??Vector3.UnitZ;
        if(!Geometry.Finite(reference)||reference.LengthSquared()<1e-8f||Math.Abs(Vector3.Dot(aim,Vector3.Normalize(reference)))>.95f)
            reference=Math.Abs(Vector3.Dot(aim,Vector3.UnitZ))>.95f?Vector3.UnitY:Vector3.UnitZ;
        var perpendicular=Vector3.Normalize(Vector3.Cross(reference,aim));
        Vector3 x,y,z;
        if(aimAxis=="Y") {y=aim;x=perpendicular;z=Vector3.Cross(x,y);}
        else if(aimAxis=="Z") {z=aim;y=perpendicular;x=Vector3.Cross(y,z);}
        else {x=aim;y=perpendicular;z=Vector3.Cross(x,y);}
        var matrix=new Matrix4x4(x.X,x.Y,x.Z,0,y.X,y.Y,y.Z,0,z.X,z.Y,z.Z,0,0,0,0,1);
        return Quaternion.Normalize(Quaternion.CreateFromAxisAngle(aim,roll*MathF.PI/180)*Quaternion.CreateFromRotationMatrix(matrix));
    }
}