Editor/WeaponImporter/Core/Ik/ArmIk.cs
#nullable enable annotations

using System.Numerics;
using WeaponImporter.Core.Hands;
using WeaponImporter.Core.Maths;
using WeaponImporter.Core.Rig;

namespace WeaponImporter.Core.Ik;

using Vector3 = System.Numerics.Vector3;

/// <summary>A character pose: parent-relative locals with lazily derived world transforms.</summary>
public sealed class CharacterPose
{
    public Skeleton Skeleton { get; }
    public XForm[] Locals { get; }
    private XForm[]? _world;

    public CharacterPose(Skeleton skeleton, XForm[] locals)
    {
        if (locals.Length != skeleton.Count)
            throw new ArgumentException("Pose bone count does not match the skeleton.", nameof(locals));
        Skeleton = skeleton;
        Locals = locals;
    }

    public static CharacterPose Rest(Skeleton skeleton) => new(skeleton, skeleton.Bones.Select(b => b.RestLocal).ToArray());

    public CharacterPose Clone() => new(Skeleton, Locals.ToArray());

    public XForm[] World => _world ??= ComputeWorld();

    public void Invalidate() => _world = null;

    private XForm[] ComputeWorld()
    {
        var w = new XForm[Locals.Length];
        for (var i = 0; i < w.Length; i++)
        {
            var p = Skeleton[i].ParentIndex;
            w[i] = p < 0 ? Locals[i] : XForm.Compose(w[p], Locals[i]);
        }
        return w;
    }

    /// <summary>Sets a bone's world rotation (children follow), keeping its position.</summary>
    public void SetWorldRotation(int bone, Quaternion world)
    {
        var parent = Skeleton[bone].ParentIndex;
        var parentRot = parent < 0 ? Quaternion.Identity : World[parent].Rot;
        Locals[bone].Rot = MathQ.Normalize(Quaternion.Conjugate(parentRot) * world);
        Invalidate();
    }

    /// <summary>Sets a bone's world transform (children follow).</summary>
    public void SetWorld(int bone, XForm world)
    {
        var parent = Skeleton[bone].ParentIndex;
        Locals[bone] = parent < 0 ? world : XForm.ToLocal(World[parent], world);
        Invalidate();
    }
}

/// <summary>Result of reaching for a target.</summary>
public readonly record struct ReachResult(bool Reached, float Shortfall, float ElbowBendDegrees);

/// <summary>
/// Analytic two-bone arm IK. The elbow stays in the plane it already had in the animation, so
/// the solve makes the smallest change that gets the wrist onto its target.
/// </summary>
public static class ArmIk
{
    /// <summary>
    /// Moves upper arm and forearm so the hand reaches <paramref name="target"/>, blending by
    /// <paramref name="weight"/>. The hand takes the target rotation. Returns reach info.
    /// </summary>
    public static ReachResult Solve(CharacterPose pose, HandRig arm, XForm target, float weight = 1f)
    {
        if (weight <= 1e-4f)
            return new ReachResult(true, 0f, 0f);
        var world = pose.World;
        var upper = world[arm.UpperArm];
        var lower = world[arm.LowerArm];
        var hand = world[arm.Hand];

        var desired = weight >= 0.9999f ? target : new XForm(Vector3.Lerp(hand.Pos, target.Pos, weight), MathQ.Slerp(hand.Rot, target.Rot, weight));

        var s = upper.Pos;
        var e = lower.Pos;
        var w = hand.Pos;
        var a = Vector3.Distance(s, e);
        var b = Vector3.Distance(e, w);
        var toTarget = desired.Pos - s;
        var dist = toTarget.Length();
        var reach = a + b;
        var shortfall = MathF.Max(0f, dist - reach * 0.999f);
        var c = Math.Clamp(dist, MathF.Abs(a - b) + 1e-3f, reach * 0.999f);
        var d = dist > 1e-5f ? toTarget / dist : Vector3.Normalize(w - s);

        // Bend direction: the animation's own bend (elbow offset from the shoulder->animated-wrist
        // line), so it never flips when the target swings past the elbow. Nearly straight arms
        // ease toward a relaxed down-and-back elbow.
        var animReach = w - s;
        var animPole = animReach.LengthSquared() > 1e-8f ? (e - s) - Vector3.Normalize(animReach) * Vector3.Dot(e - s, Vector3.Normalize(animReach)) : Vector3.Zero;
        var relaxed = Vector3.Normalize(new Vector3(-0.2f, 0f, -0.8f));
        var straightness = Math.Clamp(animPole.Length() / ((a + b) * 0.08f), 0f, 1f);
        var bend = (animPole.LengthSquared() > 1e-10f ? Vector3.Normalize(animPole) : relaxed) * straightness + relaxed * (1f - straightness);
        bend -= d * Vector3.Dot(bend, d);
        if (bend.LengthSquared() < 1e-8f)
        {
            bend = Vector3.Cross(d, Vector3.Cross(-Vector3.UnitZ, d));
            if (bend.LengthSquared() < 1e-8f)
                bend = MathQ.Perpendicular(d);
        }
        bend = Vector3.Normalize(bend);

        var cosA = Math.Clamp((a * a + c * c - b * b) / (2f * a * c), -1f, 1f);
        var sinA = MathF.Sqrt(MathF.Max(0f, 1f - cosA * cosA));
        var newElbow = s + (d * cosA + bend * sinA) * a;

        // Rotate the upper arm so the elbow lands on its new spot.
        var upperDelta = MathQ.FromTo(e - s, newElbow - s);
        pose.SetWorldRotation(arm.UpperArm, MathQ.Normalize(upperDelta * upper.Rot));

        // Then the forearm so the wrist lands on the (clamped) target.
        world = pose.World;
        var elbowNow = world[arm.LowerArm].Pos;
        var wristNow = world[arm.Hand].Pos;
        var clampedTarget = s + d * c;
        var lowerDelta = MathQ.FromTo(wristNow - elbowNow, clampedTarget - elbowNow);
        pose.SetWorldRotation(arm.LowerArm, MathQ.Normalize(lowerDelta * world[arm.LowerArm].Rot));

        // Hand orientation.
        pose.SetWorldRotation(arm.Hand, desired.Rot);

        var bendDeg = 180f - MathF.Acos(Math.Clamp((a * a + b * b - c * c) / (2f * a * b), -1f, 1f)) * 180f / MathF.PI;
        return new ReachResult(shortfall < 0.05f, shortfall, bendDeg);
    }

    /// <summary>Applies finger joint rotations of a hand pose to a character pose.</summary>
    public static void ApplyFingers(CharacterPose pose, HandRig rig, HandPose hand, float weight = 1f)
    {
        foreach (var finger in rig.Fingers)
            for (var j = 0; j < finger.Joints.Length; j++)
            {
                var bone = finger.Joints[j];
                var target = hand.LocalRotation(rig, finger, j);
                pose.Locals[bone].Rot = weight >= 0.9999f ? target : MathQ.Slerp(pose.Locals[bone].Rot, target, weight);
            }
        pose.Invalidate();
    }
}