Editor/WeaponImporter/Core/Grip/LibraryGripGenerators.cs
#nullable enable annotations

using System.Numerics;
using WeaponImporter.Core.Analysis;
using WeaponImporter.Core.Geometry;
using WeaponImporter.Core.Grasp;
using WeaponImporter.Core.Hands;
using WeaponImporter.Core.Maths;
using WeaponImporter.Core.Rig;
using WeaponImporter.Core.Weapon;

namespace WeaponImporter.Core.Grip;

using Vector3 = System.Numerics.Vector3;

/// <summary>
/// Grips from the library (captured from animations): each preset for the requested hand is
/// placed on the grip surface, refined against the geometry for this hand's size, and scored.
/// </summary>
public sealed class PresetGripGenerator : IGripGenerator
{
    private readonly IReadOnlyList<GripPreset> _presets;

    /// <summary>Per hand: only this preset is offered (the user picked it).</summary>
    public string? OnlyRight { get; init; }
    public string? OnlyLeft { get; init; }

    public PresetGripGenerator(IEnumerable<GripPreset> presets) => _presets = presets.ToList();

    public string Name => "Library";
    public bool IsAvailable => _presets.Count > 0;

    public IEnumerable<GraspCandidate> Generate(GraspRequest request, CancellationToken cancel = default)
    {
        var list = new List<GraspCandidate>();
        foreach (var preset in _presets)
        {
            cancel.ThrowIfCancellationRequested();
            if (preset.Side != request.Hand.Side || !Compatible(preset.Style, request.Style))
                continue;
            var only = request.Hand.Side == Side.Right ? OnlyRight : OnlyLeft;
            if (!string.IsNullOrEmpty(only) && preset.Name != only)
                continue;
            var pose = GripLibrary.Apply(preset, request.Hand, request.Surface);
            ContactRefiner.Refine(request, pose);
            var quality = GraspEvaluator.Evaluate(request, pose);
            list.Add(new GraspCandidate(pose, quality, preset.Name) { Bonus = preset.Priority });
        }
        return list.OrderByDescending(c => c.Rank);
    }

    /// <summary>A handguard grip can cradle a pump and vice versa; a pistol grip grip is a wrap.</summary>
    public static bool Compatible(GripStyle preset, GripStyle surface) => preset == surface
        || (preset, surface) is (GripStyle.Cradle, GripStyle.Pump) or (GripStyle.Pump, GripStyle.Cradle)
        || (preset, surface) is (GripStyle.Wrap, GripStyle.Handle) or (GripStyle.Handle, GripStyle.Wrap);
}

/// <summary>
/// Several generators competing: their candidates are pooled and ranked together (the solver
/// then re-ranks the best ones with wrist bend and reach). A learned generator such as GrabNet
/// plugs in here the same way; it runs inside the background grip solve, never on the UI thread.
/// </summary>
public sealed class CompositeGripGenerator : IGripGenerator
{
    private readonly IReadOnlyList<IGripGenerator> _parts;

    public CompositeGripGenerator(params IGripGenerator[] parts) => _parts = parts.Where(p => p.IsAvailable).ToList();

    public string Name => string.Join(" + ", _parts.Select(p => p.Name));
    public bool IsAvailable => _parts.Count > 0;

    public IEnumerable<GraspCandidate> Generate(GraspRequest request, CancellationToken cancel = default)
        => _parts.SelectMany(p => p.Generate(request, cancel)).OrderByDescending(c => c.Rank).ToList();
}

/// <summary>
/// The grip source per hand from the user's choice: "" = automatic (library grips and the
/// procedural fit compete), "Procedural" = only the procedural fit, a preset name = that grip.
/// </summary>
public sealed class ChoiceGripGenerator : IGripGenerator
{
    public const string Procedural = "Procedural";

    private readonly IReadOnlyList<GripPreset> _presets;
    private readonly string _right, _left;
    private readonly ProceduralGripGenerator _procedural = new();

    public ChoiceGripGenerator(IEnumerable<GripPreset> presets, string? right, string? left)
    {
        _presets = presets.ToList();
        _right = right ?? "";
        _left = left ?? "";
    }

    public string Name => "Grips";
    public bool IsAvailable => true;

    public IEnumerable<GraspCandidate> Generate(GraspRequest request, CancellationToken cancel = default)
    {
        var choice = request.Hand.Side == Side.Right ? _right : _left;
        if (choice == Procedural)
            return _procedural.Generate(request, cancel);
        if (choice.Length > 0)
        {
            var chosen = new PresetGripGenerator(_presets.Where(p => p.Name == choice)).Generate(request, cancel).ToList();
            if (chosen.Count > 0)
                return chosen;
        }
        return new CompositeGripGenerator(new PresetGripGenerator(_presets), _procedural).Generate(request, cancel);
    }

    /// <summary>Only the library grips this hand may use (none when the procedural fit is chosen).</summary>
    public IEnumerable<GraspCandidate> GenerateLibrary(GraspRequest request, CancellationToken cancel = default)
    {
        var choice = request.Hand.Side == Side.Right ? _right : _left;
        if (choice == Procedural)
            return Array.Empty<GraspCandidate>();
        var presets = choice.Length > 0 ? _presets.Where(p => p.Name == choice) : _presets;
        return new PresetGripGenerator(presets).Generate(request, cancel);
    }
}

/// <summary>Captures the grips of a file's own arms (first-person rigs, animation packs).</summary>
public static class GripExtractor
{
    /// <summary>A hand counts as holding when its palm is this close to the weapon surface (inches).</summary>
    public const float HoldingDistance = 2.5f;

    /// <summary>Ranking bonus of grips taken from the weapon's own animation (see <see cref="GripPreset.Priority"/>).</summary>
    public const float OwnPriority = 2.5f;

    /// <summary>The weapon's own grips, preferred when grips compete.</summary>
    public static List<GripPreset> ExtractOwn(WeaponAnalysis a, string source)
    {
        var list = Extract(a, source);
        foreach (var p in list)
            p.Priority = OwnPriority;
        return list;
    }

    /// <summary>
    /// The hands in the analysed file's reference pose (canonical weapon space), captured on the
    /// grip surface each one holds. Hands that aren't on the weapon are skipped.
    /// </summary>
    public static List<GripPreset> Extract(WeaponAnalysis a, string source)
    {
        var list = new List<GripPreset>();
        var skeleton = a.Asset.Skeleton;
        if (skeleton.Count == 0 || a.WeaponBvh.IsEmpty)
            return list;
        var world = ReferenceWorld(a);
        foreach (var side in new[] { Side.Right, Side.Left })
        {
            var rig = HandRig.Build(skeleton, side);
            if (rig is null || rig.Fingers.Count < 3)
                continue;
            var palm = world[rig.Hand].TransformPoint(rig.PalmCenter);
            var candidate = side == Side.Right ? a.Primary : a.Support;
            GripSurface? surface = null;
            var style = side == Side.Right ? (a.Type.Type == WeaponType.Melee ? GripStyle.Handle : GripStyle.Wrap) : GripStyle.Cradle;
            if (candidate is not null && Vector3.Distance(candidate.Surface.Contact, palm) < rig.HandLength)
            {
                surface = candidate.Surface;
                style = candidate.Style;
            }
            else
            {
                var hint = side == Side.Right ? Vector3.UnitZ : Vector3.UnitX;
                surface = SurfaceProbe.FromPoint(a.WeaponBvh, palm, hint);
            }
            if (surface is null || Vector3.Distance(surface.Contact, palm) > HoldingDistance + rig.PalmThickness)
                continue;
            var name = $"{source} · {(side == Side.Right ? "right" : "left")}";
            var preset = GripLibrary.Capture(rig, world, surface, style, name, source);
            // A palm inside the surface means the "weapon" is the arms themselves (an arms-only
            // file) or the hand isn't really holding it.
            if (preset.Palm[2] < -0.12f)
                continue;
            preset.WeaponType = a.Type.Type.ToString();
            list.Add(preset);
        }
        return list;
    }

    /// <summary>World transforms of the analysis reference frame (bind pose without a clip).</summary>
    public static XForm[] ReferenceWorld(WeaponAnalysis a)
    {
        var skeleton = a.Asset.Skeleton;
        var clip = a.Asset.FindClip(a.ReferenceClip);
        if (clip is { FrameCount: > 0 })
        {
            var frame = Math.Clamp(a.ReferenceFrame, 0, clip.FrameCount - 1);
            return new Pose(clip.Frames[frame]).ToWorld(skeleton);
        }
        return skeleton.RestWorld.ToArray();
    }
}