Editor/Core/Validation/FingerWeightRepair.cs
namespace HumanoidRigger;

/// <summary>Repair persistent finger folds using a visibility-constrained heat
/// candidate. Preserve thumb and body weights, and accept only measured improvements.</summary>
public static class FingerWeightRepair
{
    public static ValidationReport Improve(ImportedCharacter character,GeneratedRig rig,ValidationReport initial)
        =>Improve(character,rig,initial,()=>HeatSkinning.Solve(character,rig));

    internal static ValidationReport Improve(ImportedCharacter character,GeneratedRig rig,ValidationReport initial,Func<Influence[][][]> heatCandidate)
    {
        if(rig.Anatomy is null)return initial;
        var roles=rig.Bones.Select(b=>b.Role).ToHashSet();
        var specifications=Deformation.Poses.Where(p=>Deformation.IsApplicable(p,roles)).ToArray();
        if(!WeightRepair.HasCompleteEvidence(initial,specifications.Select(p=>p.Name).Order().ToArray()))return initial;
        var failing=initial.StressTests.Where(t=>t.ReversedTriangles>0).Select(t=>t.Pose).ToHashSet();
        bool Finger(string role,string side)=>role.EndsWith("."+side)&&Profiles.Fingers.Where(f=>f!="Thumb").Any(role.StartsWith);
        var sides=new[]{"L","R"}.Where(side=>specifications.Any(p=>failing.Contains(p.Name)&&
            (p.Degrees!=0&&Finger(p.Role,side)||p.AdditionalJoints?.Any(j=>j.Degrees!=0&&Finger(j.Role,side))==true))).ToArray();
        if(sides.Length==0)return initial;
        Influence[][][] candidate;
        try{candidate=heatCandidate();}
        catch(InvalidOperationException){return initial;}
        var result=initial;
        if(!initial.Passed)
        {
            // Both hands may fail independent curls. Validate their combined
            // candidate before committing either side of a partially repaired rig.
            foreach(float amount in new[]{1f,.75f,.5f})
            {
                var trial=rig.Weights;
                foreach(string side in sides)trial=Blend(rig,trial,candidate,side,amount);
                var tested=SurfaceRepair.TryWeights(character,rig,result,trial);
                if(tested!=result)return tested;
            }
            return result;
        }
        foreach(string side in sides)
        {
            var original=rig.Weights;
            foreach(float amount in new[]{1f,.75f,.5f})
            {
                var trial=Blend(rig,original,candidate,side,amount);
                var tested=SurfaceRepair.TryWeights(character,rig,result,trial);
                if(tested==result)continue;
                result=tested;break;
            }
        }
        return result;
    }

    internal static Influence[][][] Blend(GeneratedRig rig,Influence[][][] original,Influence[][][] candidate,string side,float amount)
    {
        var roles=Profiles.Fingers.Where(f=>f!="Thumb").SelectMany(f=>Enumerable.Range(1,3).Select(j=>f+j+"."+side)).Append("Hand."+side).ToHashSet();
        var movable=rig.Bones.Select(b=>roles.Contains(b.Role)).ToArray();
        return original.Select((part,p)=>part.Select((weights,v)=>
        {
            var fixedWeights=weights.Where(w=>!movable[w.Bone]).ToArray();
            float total=weights.Where(w=>movable[w.Bone]).Sum(w=>w.Weight);
            float replacement=candidate[p][v].Where(w=>movable[w.Bone]).Sum(w=>w.Weight);
            if(total<.01f||replacement<.01f)return weights;
            var values=weights.Where(w=>movable[w.Bone]).Select(w=>w with{Weight=w.Weight*(1-amount)})
                .Concat(candidate[p][v].Where(w=>movable[w.Bone]).Select(w=>w with{Weight=w.Weight/replacement*total*amount}));
            // Reserve the original influences before limiting the replacement.
            // Otherwise top-N cleanup can alter a good thumb or wrist transition.
            var cleaned=Skinning.Cleanup(values,rig.Bones.Length,rig.Profile.MaximumInfluences-fixedWeights.Length);
            return fixedWeights.Concat(cleaned.Select(w=>w with{Weight=w.Weight*total})).ToArray();
        }).ToArray()).ToArray();
    }
}