Editor/Core/Analysis/HumanoidFacing.cs
namespace HumanoidRigger;
using Vector3=System.Numerics.Vector3;

/// <summary>Resolve up from body geometry, then an unambiguous quarter-turn
/// from separated lower legs. Feet select the forward sign.</summary>
internal static class HumanoidFacing
{
    public static ImportedCharacter Normalize(ImportedCharacter character)
    {
        var up=HumanoidUp.Find(character);
        if(up!=Vector3.UnitY)
        {
            Vector3 Upright(Vector3 p)=>up==Vector3.UnitX?new(-p.Y,p.X,p.Z)
                :up==-Vector3.UnitX?new(p.Y,-p.X,p.Z)
                :up==-Vector3.UnitY?new(p.X,-p.Y,-p.Z)
                :up==Vector3.UnitZ?new(p.X,p.Z,-p.Y):new(p.X,-p.Z,p.Y);
            character=Reorient(character,Upright,"The character's up direction was corrected from its body geometry.");
        }
        var body=character.Meshes.Where(m=>m.Kind==MeshKind.Body).SelectMany(m=>m.Vertices).ToArray();
        if(body.Length<100)return character;
        float bottom=body.Min(p=>p.Y),height=body.Max(p=>p.Y)-bottom;
        var legs=body.Where(p=>p.Y>bottom+height*.15f&&p.Y<bottom+height*.35f).Distinct().ToArray();
        if(legs.Length<20)return character;
        var center=Geometry.Mean(legs);
        float xVariance=legs.Average(p=>(p.X-center.X)*(p.X-center.X));
        float zVariance=legs.Average(p=>(p.Z-center.Z)*(p.Z-center.Z));
        if(zVariance<xVariance*3)return character;
        float middle=(BodyDetector.Quantile(legs.Select(p=>p.Z),.1f)+BodyDetector.Quantile(legs.Select(p=>p.Z),.9f))*.5f;
        if(legs.Count(p=>Math.Abs(p.Z-middle)<height*.012f)>legs.Length*.15f)return character;
        var feet=body.Where(p=>p.Y<bottom+height*.07f).ToArray();
        if(feet.Length<8)return character;
        float forward=(BodyDetector.Quantile(feet.Select(p=>p.X),.1f)+BodyDetector.Quantile(feet.Select(p=>p.X),.9f))*.5f-center.X;
        forward=SeparatedFeetForward(character,bottom,height,middle)??forward;
        if(Math.Abs(forward)<height*.012f)return character;
        float sign=forward<0?1:-1;
        Vector3 Turn(Vector3 p)=>new(sign*p.Z,p.Y,-sign*p.X);
        return Reorient(character,Turn,"The character was turned to face forward.");
    }
    static float? SeparatedFeetForward(ImportedCharacter character,float bottom,float height,float middle)
    {
        // Long hands can reach the floor. If the lower slice contains more than
        // two limbs, follow the inner calf surfaces down to their own feet.
        var origin=(character.Minimum+character.Maximum)*.5f;origin.Y=bottom+height*.25f;
        var sections=MeshSections.Cut(character,origin,Vector3.UnitY,(character.Maximum-character.Minimum).Length(),height*1e-5f);
        if(sections.Length<=2)return null;
        var left=sections.Where(s=>s.Center.Z>middle+height*.025f).OrderBy(s=>s.Center.Z).FirstOrDefault();
        var right=sections.Where(s=>s.Center.Z<middle-height*.025f).OrderByDescending(s=>s.Center.Z).FirstOrDefault();
        if(left is null||right is null)return null;
        var mesh=Geometry.Merge(character.Meshes.Where(m=>m.Kind==MeshKind.Body));
        var neighbors=Geometry.Neighbors(mesh,height*1e-5f);var visited=new bool[mesh.Vertices.Length];var queue=new Queue<int>();
        foreach(var section in new[]{left,right})
        {
            int seed=-1;float distance=float.PositiveInfinity;
            for(int i=0;i<mesh.Vertices.Length;i++)
            {
                float candidate=Vector3.DistanceSquared(mesh.Vertices[i],section.Center);
                if(candidate<distance){seed=i;distance=candidate;}
            }
            if(seed<0||distance>height*height*.08f*.08f)return null;
            if(!visited[seed]){visited[seed]=true;queue.Enqueue(seed);}
        }
        while(queue.TryDequeue(out int vertex))foreach(int next in neighbors[vertex])
            if(!visited[next]&&mesh.Vertices[next].Y<bottom+height*.4f){visited[next]=true;queue.Enqueue(next);}
        var feet=mesh.Vertices.Where((p,i)=>visited[i]&&p.Y<bottom+height*.07f).Select(p=>p.X).ToArray();
        if(feet.Length<8)return null;
        return(BodyDetector.Quantile(feet,.1f)+BodyDetector.Quantile(feet,.9f))*.5f-(left.Center.X+right.Center.X)*.5f;
    }
    static ImportedCharacter Reorient(ImportedCharacter character,Func<Vector3,Vector3> turn,string warning)
    {
        return new ImportedCharacter{Name=character.Name,SourcePath=character.SourcePath,SourceUnitCm=character.SourceUnitCm,SourceUpAxis=character.SourceUpAxis,
            HasExistingSkin=character.HasExistingSkin,ExistingBones=character.ExistingBones.Select(b=>b with{Position=turn(b.Position)}).ToArray(),
            Meshes=character.Meshes.Select(m=>m with{Vertices=m.Vertices.Select(turn).ToArray(),CornerNormals=m.CornerNormals.Select(turn).ToArray()}).ToArray(),
            Materials=character.Materials,EmbeddedTextures=character.EmbeddedTextures,
            ImportWarnings=character.ImportWarnings.Append(warning).ToArray()};
    }
}