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

/// <summary>Recover split surface edges without welding positions, normals, UVs or materials.
/// Point contacts and ambiguous nonmanifold edges stay independent.</summary>
internal sealed class SurfaceSeams
{
    internal readonly record struct Vertex(int Part,int Index);
    readonly record struct Edge(int A,int B,int X,int Y,MeshKind Kind);
    internal Vertex[][] Groups {get;}

    internal SurfaceSeams(ImportedCharacter character)
    {
        var offsets=new int[character.Meshes.Length+1];
        for(int p=0;p<character.Meshes.Length;p++)offsets[p+1]=offsets[p]+character.Meshes[p].Vertices.Length;
        var parent=Enumerable.Range(0,offsets[^1]).ToArray();
        int Root(int v){while(parent[v]!=v){parent[v]=parent[parent[v]];v=parent[v];}return v;}
        void Join(int a,int b){a=Root(a);b=Root(b);if(a!=b)parent[Math.Max(a,b)]=Math.Min(a,b);}
        var points=new Dictionary<Vector3,int>();
        var edges=new Dictionary<(int,int),(Edge First,Edge Second,int Count)>();
        for(int p=0;p<character.Meshes.Length;p++)
        {
            var mesh=character.Meshes[p];
            var ids=mesh.Vertices.Select(point=>{if(!points.TryGetValue(point,out int id))points.Add(point,id=points.Count);return id;}).ToArray();
            var boundary=new Dictionary<(int,int),(int A,int B,int Count)>();
            void Add(int a,int b)
            {
                var key=(Math.Min(a,b),Math.Max(a,b));
                boundary[key]=boundary.TryGetValue(key,out var old)?(old.A,old.B,old.Count+1):(a,b,1);
            }
            for(int t=0;t<mesh.Triangles.Length;t+=3)
            {
                int a=mesh.Triangles[t],b=mesh.Triangles[t+1],c=mesh.Triangles[t+2];
                if(Vector3.Cross(mesh.Vertices[b]-mesh.Vertices[a],mesh.Vertices[c]-mesh.Vertices[a]).LengthSquared()==0)continue;
                Add(a,b);Add(b,c);Add(c,a);
            }
            foreach(var edge in boundary.Values)
            {
                if(edge.Count!=1)continue;
                int x=ids[edge.A],y=ids[edge.B];if(x==y)continue;
                var key=(Math.Min(x,y),Math.Max(x,y));
                var next=new Edge(offsets[p]+edge.A,offsets[p]+edge.B,x,y,mesh.Kind);
                edges[key]=edges.TryGetValue(key,out var old)?(old.First,next,old.Count+1):(next,default,1);
            }
        }
        foreach(var pair in edges.Values)
        {
            if(pair.Count!=2)continue;
            var a=pair.First;var b=pair.Second;
            if(a.X!=b.Y||a.Y!=b.X||a.Kind!=b.Kind)continue;
            Join(a.A,b.B);Join(a.B,b.A);
        }
        Groups=character.Meshes.SelectMany((m,p)=>Enumerable.Range(0,m.Vertices.Length).Select(v=>new Vertex(p,v)))
            .GroupBy(v=>Root(offsets[v.Part]+v.Index)).Where(g=>g.Count()>1).Select(g=>g.ToArray()).ToArray();
    }
    internal int Mismatches(Influence[][][] weights)=>Groups.Count(group=>
    {
        var first=weights[group[0].Part][group[0].Index];
        return group.Skip(1).Any(v=>!Match(first,weights[v.Part][v.Index]));
    });
    static bool Match(Influence[] a,Influence[] b)=>a.Length==b.Length&&a.All(w=>b.Any(x=>x.Bone==w.Bone&&x.Weight==w.Weight));
    internal Influence[][][] Couple(GeneratedRig rig)
    {
        var result=rig.Weights.Select(p=>(Influence[][])p.Clone()).ToArray();
        foreach(var group in Groups)
        {
            var first=rig.Weights[group[0].Part][group[0].Index];
            if(group.All(v=>Match(first,rig.Weights[v.Part][v.Index])))continue;
            var weights=Skinning.Cleanup(group.SelectMany(v=>rig.Weights[v.Part][v.Index].Select(w=>w with{Weight=w.Weight/group.Length})),rig.Bones.Length,rig.Profile.MaximumInfluences);
            foreach(var v in group)result[v.Part][v.Index]=weights;
        }
        return result;
    }
}