Editor/Core/Validation/TrunkSkinning.cs
namespace HumanoidRigger;
using Vector3=System.Numerics.Vector3;

/// <summary>Refine axial skinning with normal-aware heat and compact joint
/// support. Full deformation evidence must remain safe before accepting it.</summary>
internal static class TrunkSkinning
{
    internal static ValidationReport Improve(ImportedCharacter character,GeneratedRig rig,ValidationReport initial)
    {
        var expected=Deformation.Poses.Where(p=>Deformation.IsApplicable(p,rig.Bones.Select(b=>b.Role).ToHashSet())).Select(p=>p.Name).Order().ToArray();
        if(!initial.Passed||!WeightRepair.HasCompleteEvidence(initial,expected))return initial;
        var region=new TrunkRegion(character,rig);if(!region.HasBleeding(character,rig))return initial;
        var original=rig.Weights;bool accepted=false;
        try
        {
            var heat=HeatSkinning.Candidates(character,rig,normalPrior:true,trunk:region).First();
            var geometry=new ValidationGeometry(character,trunk:region);
            bool Safe(ValidationReport report)=>!region.HasBleeding(character,rig)&&report.Passed&&WeightRepair.HasCompleteEvidence(report,expected)&&
                !report.StressTests.Zip(initial.StressTests).Any(p=>p.First.Pose!=p.Second.Pose||p.First.ReversedTriangles>p.Second.ReversedTriangles||p.First.ReversedAreaFraction>p.Second.ReversedAreaFraction+1e-7f);
            foreach(float amount in new[]{1f,.5f,0f})
            {
                rig.Weights=Apply(character,rig,region,original,heat,amount);
                var candidate=RigValidator.ValidateAndRepair(character,rig,geometry);
                // Restricting a socket can expose a fold at its boundary. Fit
                // that transition across poses without reopening trunk ownership.
                if(WeightRepair.HasCompleteEvidence(candidate,expected)&&(!candidate.Passed||candidate.StressTests.Zip(initial.StressTests).Any(p=>p.First.ReversedTriangles>p.Second.ReversedTriangles)))
                {
                    var trial=new GeneratedRig{Profile=rig.Profile,Bones=rig.Bones,Anatomy=rig.Anatomy,Weights=rig.Weights,Report=candidate};
                    var refined=PoseWeightRepair.Improve(character,trial,geometry,JointCoverage.Measure(character,trial,geometry));
                    rig.Weights=refined.Weights;candidate=refined.Report;
                }
                // Local surface repair may adjust a protected boundary. Never
                // accept a trial that silently reintroduces remote attachments.
                if(!Safe(candidate))continue;
                // This stage owns the trunk. Those repairs can also reach a hand or
                // the head; keep the reviewed weights there whenever the
                // correction validates without the change.
                // Surface that shares a bone with the trunk boundary stays as
                // repaired: it has to meet the corrected trunk.
                var repaired=rig.Weights;
                // Renormalizing alone shifts the last digits; that is no repair.
                bool Rounding(Influence[] a,Influence[] b)=>a.Length==b.Length&&a.Zip(b).All(w=>w.First.Bone==w.Second.Bone&&Math.Abs(w.First.Weight-w.Second.Weight)<1e-5f);
                bool Remote(int p,int v)=>!region.Vertices[p][v]&&!repaired[p][v].SequenceEqual(original[p][v])&&(Rounding(repaired[p][v],original[p][v])||
                    !repaired[p][v].Concat(original[p][v]).Any(w=>region.Axial[w.Bone]||rig.Bones[w.Bone].Role is "Neck"||rig.Bones[w.Bone].Role.StartsWith("Clavicle.")||rig.Bones[w.Bone].Role.StartsWith("UpperArm.")||rig.Bones[w.Bone].Role.StartsWith("UpperLeg.")));
                if(Enumerable.Range(0,repaired.Length).Any(p=>Enumerable.Range(0,repaired[p].Length).Any(v=>Remote(p,v))))
                {
                    rig.Weights=repaired.Select((part,p)=>part.Select((w,v)=>Remote(p,v)?original[p][v]:w).ToArray()).ToArray();
                    var scoped=RigValidator.Validate(character,rig,null,geometry);
                    if(Safe(scoped)){scoped.Repairs=candidate.Repairs;scoped.RepairPasses=candidate.RepairPasses;candidate=scoped;}
                    else rig.Weights=repaired;
                }
                candidate.Repairs+=initial.Repairs;candidate.RepairPasses+=initial.RepairPasses+1;
                accepted=true;return candidate;
            }
        }
        catch(InvalidOperationException e){initial.Issues.Add(new("skinning-candidate",e.Message,false));}
        finally{if(!accepted)rig.Weights=original;}
        // An arm bound against the body shares a crease with the flank. Forcing
        // that apart tears it, so the correction is withheld. The reviewed rig
        // still passed every deformation test: report it rather than discard it.
        initial.Issues.Add(new("weight-bleeding","Part of the torso still follows a limb, which a closed rest pose can make unavoidable. Check the shoulder, hip and neck landmarks, or import an open T- or A-pose.",false));
        return initial;
    }
    static Influence[][][] Apply(ImportedCharacter character,GeneratedRig rig,TrunkRegion region,Influence[][][] source,Influence[][][] heat,float amount)
    {
        var result=source.Select(p=>(Influence[][])p.Clone()).ToArray();var ends=RigGeometry.SegmentEnds(rig);
        for(int p=0;p<result.Length;p++)for(int v=0;v<result[p].Length;v++)if(region.Vertices[p][v])
        {
            var point=character.Meshes[p].Vertices[v];float blend=amount*region.Blend(point);
            var values=new float[rig.Bones.Length];
            foreach(var w in source[p][v])values[w.Bone]+=w.Weight*(1-blend);
            foreach(var w in heat[p][v])values[w.Bone]+=w.Weight*blend;
            float removed=0;
            foreach(var attachment in region.Attachments)
            {
                float support=attachment.Support(point);
                for(int b=0;b<values.Length;b++)if(attachment.Moving[b])
                {
                    float cut=values[b]*(1-support);values[b]-=cut;
                    if(attachment.Receiver>=0)values[attachment.Receiver]+=cut;else removed+=cut;
                }
            }
            float axial=heat[p][v].Where(w=>region.Axial[w.Bone]).Sum(w=>w.Weight);
            if(axial>1e-6f)foreach(var w in heat[p][v]){if(region.Axial[w.Bone])values[w.Bone]+=removed*w.Weight/axial;}
            else
            {
                int nearest=Enumerable.Range(0,rig.Bones.Length).Where(b=>region.Axial[b]).MinBy(b=>Vector3.DistanceSquared(point,Geometry.ClosestOnSegment(point,rig.Bones[b].Position,ends[b])));
                values[nearest]+=removed;
            }
            result[p][v]=Skinning.Cleanup(Skinning.Limit(values,rig.Profile.MaximumInfluences),rig.Profile.MaximumInfluences);
        }
        return result;
    }
}