Editor/WeaponImporter/Core/Generation/ExportRig.cs
#nullable enable annotations

using System.Numerics;
using WeaponImporter.Core.Analysis;
using WeaponImporter.Core.Maths;
using WeaponImporter.Core.Rig;
using WeaponImporter.Core.Weapon;

namespace WeaponImporter.Core.Generation;

using Vector3 = System.Numerics.Vector3;

/// <summary>
/// The rig a world model ships with: the weapon body becomes the fixed root, only bones the
/// weapon uses are kept (arms, cameras and scene helpers are dropped) and parts parented
/// elsewhere (a magazine hanging off a hand or the scene root) are re-parented under the body
/// with their motion relative to it preserved. Root motion of the body is removed, so a
/// first-person sway never moves the weapon in third person.
/// </summary>
public sealed class ExportRig
{
    public required WeaponAsset Asset { get; init; }
    public required int[] Triangles { get; init; }
    public required string RootName { get; init; }

    /// <summary>Old bone index -> new bone index (-1 when dropped).</summary>
    public required int[] Map { get; init; }

    public static ExportRig Build(WeaponAnalysis a, IEnumerable<string>? keepNames = null)
    {
        var source = a.Asset;
        var skeleton = source.Skeleton;
        var mesh = source.Mesh;

        // Body: the bone most weapon triangles are skinned to.
        var counts = new Dictionary<int, int>();
        foreach (var t in a.WeaponTriangles)
        {
            var b = mesh.TriangleBone(t);
            if (b >= 0)
                counts[b] = counts.GetValueOrDefault(b) + 1;
        }
        var body = counts.Count > 0 ? counts.OrderByDescending(kv => kv.Value).First().Key : a.RootBone;

        // Bones to keep: skinned weapon bones, named/moving parts, explicitly requested ones.
        var keep = new HashSet<int> { body };
        foreach (var b in counts.Keys)
            keep.Add(b);
        foreach (var p in a.Parts)
            if (p.Bone.Length > 0 && skeleton.IndexOf(p.Bone) is var pi && pi >= 0)
                keep.Add(pi);
        if (keepNames is not null)
            foreach (var n in keepNames)
                if (!string.IsNullOrEmpty(n) && skeleton.IndexOf(n) is var ki && ki >= 0 && !a.ArmBones.Contains(ki))
                    keep.Add(ki);
        // Close over ancestors that sit between the body and a kept bone (keeps hierarchies intact).
        foreach (var b in keep.ToList())
            for (var p = skeleton[b].ParentIndex; p >= 0 && p != body; p = skeleton[p].ParentIndex)
            {
                if (!IsDescendant(skeleton, p, body))
                    break;
                keep.Add(p);
            }

        // New parent of each kept bone: nearest kept ancestor under the body, else the body.
        int NewParent(int b)
        {
            if (b == body)
                return -1;
            for (var p = skeleton[b].ParentIndex; p >= 0; p = skeleton[p].ParentIndex)
                if (keep.Contains(p) && (p == body || IsDescendant(skeleton, p, body)))
                    return p;
            return body;
        }

        // Order: body first, then topological by new parent (stable, source order).
        var order = new List<int> { body };
        var placed = new HashSet<int> { body };
        var remaining = skeleton.Bones.Select(x => x.Index).Where(i => keep.Contains(i) && i != body).ToList();
        while (remaining.Count > 0)
        {
            var progress = false;
            foreach (var b in remaining.ToList())
                if (placed.Contains(NewParent(b)))
                {
                    order.Add(b);
                    placed.Add(b);
                    remaining.Remove(b);
                    progress = true;
                }
            if (!progress)
                break;
        }

        var rest = skeleton.RestWorld;
        var names = new HashSet<string>(StringComparer.Ordinal);
        var defs = new List<BoneDefinition>();
        foreach (var b in order)
        {
            var parent = NewParent(b);
            var local = parent < 0 ? rest[b] : XForm.ToLocal(rest[parent], rest[b]);
            defs.Add(new BoneDefinition(skeleton[b].Name, parent < 0 ? null : skeleton[parent].Name, local));
            names.Add(skeleton[b].Name);
        }
        var exportSkeleton = Skeleton.Create(defs);
        var map = new int[skeleton.Count];
        for (var i = 0; i < map.Length; i++)
            map[i] = keep.Contains(i) ? exportSkeleton.IndexOf(skeleton[i].Name) : -1;

        // Vertices skinned to dropped bones fall back to their nearest kept ancestor (or the body).
        int MapBone(int b)
        {
            for (var x = b; x >= 0; x = skeleton[x].ParentIndex)
                if (map[x] >= 0)
                    return map[x];
            return map[body];
        }
        var vertexBone = mesh.VertexBone.Select(b => b >= 0 ? MapBone(b) : map[body]).ToArray();
        var exportMesh = mesh.WithVertices(mesh.Positions, vertexBone, _ => Quaternion.Identity);

        // Clips: re-express every kept bone relative to its new parent, frame by frame.
        var clips = new List<Clip>();
        foreach (var clip in source.Clips)
        {
            var frames = new List<XForm[]>(clip.FrameCount);
            foreach (var locals in clip.Frames)
            {
                var world = new XForm[skeleton.Count];
                for (var i = 0; i < skeleton.Count; i++)
                {
                    var p = skeleton[i].ParentIndex;
                    world[i] = p < 0 ? locals[i] : XForm.Compose(world[p], locals[i]);
                }
                // Body stays at its rest placement; everything else keeps its motion relative to it.
                var bodyWorld = world[body];
                var toRest = XForm.Compose(rest[body], bodyWorld.Inverse());
                var outLocals = new XForm[exportSkeleton.Count];
                foreach (var b in order)
                {
                    var nb = map[b];
                    var parent = NewParent(b);
                    if (parent < 0)
                        outLocals[nb] = rest[body];
                    else
                        outLocals[nb] = XForm.ToLocal(XForm.Compose(toRest, world[parent]), XForm.Compose(toRest, world[b]));
                }
                frames.Add(outLocals);
            }
            clips.Add(new Clip(clip.Name, clip.Fps, clip.Looping, frames, clip.NativeFps));
        }

        var asset = new WeaponAsset
        {
            Name = source.Name,
            Kind = source.Kind,
            SourcePath = source.SourcePath,
            Skeleton = exportSkeleton,
            Mesh = exportMesh,
            Clips = clips,
            Attachments = source.Attachments,
        };
        return new ExportRig { Asset = asset, Triangles = a.WeaponTriangles, RootName = skeleton[body].Name, Map = map };
    }

    private static bool IsDescendant(Skeleton skeleton, int bone, int ancestor)
    {
        for (var p = skeleton[bone].ParentIndex; p >= 0; p = skeleton[p].ParentIndex)
            if (p == ancestor)
                return true;
        return false;
    }
}