Editor/HumanoidHandRetargeter/HandMappingDialog.cs
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using Editor;
using Sandbox;
using HumanoidHandRetargeter.Mapping;
using HumanoidHandRetargeter.Calibration;
using Quat=System.Numerics.Quaternion;
using Vec=System.Numerics.Vector3;
using Skel=HumanoidHandRetargeter.Skeleton.Skeleton;

namespace HumanoidHandRetargeter.Editor;

/// <summary>Legacy MappingEditor layout with variable hand/digit roles and optional arms.</summary>
public sealed class HandMappingDialog : Dialog
{
    readonly Skel skeleton; readonly List<HandEdit> hands=new(); readonly Label error;
    public IReadOnlyDictionary<HandSide,Quat> PalmFrames {get;private set;}=new Dictionary<HandSide,Quat>();
    public Action<HandMappingResult>? Applied {get;set;}
    public HandMappingDialog(Widget? parent,string name,Skel rig,HandMappingResult mapping,IReadOnlyDictionary<HandSide,Quat>? palms=null) : base(parent)
    {
        skeleton=rig;Window.WindowTitle="Hand Mapping - "+name;Window.SetWindowIcon("device_hub");Window.SetModal(true,true);
        Window.MinimumWidth=460;Window.MinimumHeight=600;Window.Size=new Vector2(520,720);
        Layout=Layout.Column();Layout.Margin=12;Layout.Spacing=8;
        Layout.Add(new Label(this){Text="Confirm each hand and its ordered finger joints. Leave missing roles empty. Metacarpals and terminal tips are separate from bending joints.",WordWrap=true});
        var scroll=Layout.Add(new ScrollArea(this),1);scroll.Canvas=new Widget(scroll);scroll.Canvas.Layout=Layout.Column();scroll.Canvas.Layout.Margin=new Sandbox.UI.Margin(4,4,16,4);scroll.Canvas.Layout.Spacing=4;
        foreach(var side in Enum.GetValues<HandSide>())
        {
            var current=mapping.Hands.FirstOrDefault(h=>h.Side==side);var edit=new HandEdit(side,current);hands.Add(edit);
            var header=scroll.Canvas.Layout.Add(new Checkbox(side+" hand"){Value=current is not null});header.Clicked=()=>edit.Enabled=header.Value;
            var canvas=scroll.Canvas.Layout;
            Choice(canvas,"Wrist",edit.Wrist,v=>edit.Wrist=v);
            var manual=canvas.Add(new Checkbox("Manual palm directions (partial rigs)"){Value=palms?.ContainsKey(side)??false});
            edit.ManualPalm=manual;
            var frame=palms is not null && palms.TryGetValue(side,out var saved)?saved:Quat.Identity;
            LineEdit[] Direction(string title,Vec vector){var row=canvas.AddRow();row.Spacing=4;row.Add(new Label(this){Text=title,FixedWidth=130});return new[]{vector.X,vector.Y,vector.Z}.Select(v=>row.Add(new LineEdit(this){Text=v.ToString("G6"),MinimumWidth=40},1)).ToArray();}
            edit.Forward=Direction("Forward X / Y / Z",Vec.Transform(Vec.UnitX,frame));
            edit.Dorsal=Direction("Dorsal X / Y / Z",Vec.Transform(Vec.UnitZ,frame));
            manual.ToolTip="Model-space directions: forward points from wrist to fingers; dorsal points out of the back of the hand. Use when a palm plane cannot be detected.";
            Choice(canvas,"Clavicle",edit.Clavicle,v=>edit.Clavicle=v);
            Choice(canvas,"Upper arm",edit.Upper,v=>edit.Upper=v);
            Choice(canvas,"Forearm",edit.Fore,v=>edit.Fore=v);
            foreach(var role in new[]{DigitRole.Thumb,DigitRole.Index,DigitRole.Middle,DigitRole.Ring,DigitRole.Pinky})
                DigitRows(canvas,edit,new DigitEdit(role,current?.Digits.FirstOrDefault(d=>d.Role==role)));
            foreach(var digit in current?.Digits.Where(d=>d.Role==DigitRole.Extra)??Enumerable.Empty<DigitChain>())
                DigitRows(canvas,edit,new DigitEdit(DigitRole.Extra,digit));
            var add=canvas.Add(new Button("Add extra digit","add"));
            add.Clicked=()=>DigitRows(canvas,edit,new DigitEdit(DigitRole.Extra,null){ExtraSlot="extra"+edit.Digits.Count(d=>d.Role==DigitRole.Extra)});
        }
        error=Layout.Add(new Label(this){WordWrap=true});error.SetStyles($"color: {Theme.Red.Hex};");
        var footer=Layout.AddRow();footer.Spacing=8;
        var save=footer.Add(new Checkbox("Save mapping for this rig"){Value=true});footer.AddStretchCell();
        footer.Add(new Button("Cancel"){Clicked=Close});
        footer.Add(new Button.Primary("Apply Mapping"){Icon="check",Clicked=()=>{
            try{
                var rigs=hands.Where(h=>h.Enabled).Select(h=>{
                    if(h.Wrist is null) throw new InvalidOperationException(h.Side+" wrist is required.");
                    var digits=h.Digits.Where(d=>d.Segments.Any(v=>v.HasValue)).Select(d=>new DigitChain(d.Role,d.Segments.Where(v=>v.HasValue).Select(v=>v!.Value),d.Meta,d.Tip,d.ExtraSlot)).ToArray();
                    return new HandRigDefinition(h.Side,h.Wrist.Value,digits,h.Clavicle,h.Upper,h.Fore,h.Helpers);
                }).ToArray();
                var result=HandRigDetector.Detect(skeleton,rigs);
                if(result.NeedsReview) throw new InvalidOperationException(string.Join("\n",result.Issues.Select(i=>i.Message)));
                var frames=new Dictionary<HandSide,Quat>();
                foreach(var hand in hands.Where(h=>h.Enabled&&h.ManualPalm.Value)) {
                    Vec Read(LineEdit[] fields){var v=fields.Select(f=>float.TryParse(f.Text,out var n)&&float.IsFinite(n)?n:throw new InvalidOperationException("Palm directions must be finite numbers.")).ToArray();return new Vec(v[0],v[1],v[2]);}
                    if(!HandRetargetProfile.TryCreatePalmFrame(Read(hand.Forward),Read(hand.Dorsal),out var frame))throw new InvalidOperationException("Palm forward and dorsal directions must be nonzero and not parallel.");
                    frames.Add(hand.Side,frame);
                }
                PalmFrames=frames;
                if(save.Value){HandPresetStore.Save(skeleton,result);HandPresetStore.SavePalms(skeleton,frames);}Applied?.Invoke(result);Close();
            }catch(Exception ex){error.Text=ex.Message;}
        }});
    }
    void Choice(Layout layout,string label,int? value,Action<int?> changed)
    {
        var row=layout.AddRow();row.Spacing=8;row.Add(new Label(this){Text=label,FixedWidth=130});
        var combo=row.Add(new ComboBox(this),1);combo.AddItem("<none>","block",()=>changed(null),selected:value is null);
        foreach(var bone in skeleton.Bones){var index=bone.Index;combo.AddItem(bone.Name,null,()=>changed(index),selected:value==index);}
    }
    void DigitRows(Layout canvas,HandEdit hand,DigitEdit digit)
    {
        hand.Digits.Add(digit);
        var title=canvas.Add(new Label(this){Text=hand.Side+" "+digit.Role+(digit.Role==DigitRole.Extra?" "+digit.ExtraSlot:"")});title.SetStyles($"font-weight:600;color:{Theme.Blue.Hex};margin-top:8px;");
        Choice(canvas,"Metacarpal",digit.Meta,v=>digit.Meta=v);
        var jointRows=canvas.AddColumn();
        void AddJoint(int? value){var index=digit.Segments.Count;digit.Segments.Add(value);Choice(jointRows,"Joint "+(index+1),value,v=>digit.Segments[index]=v);}
        var initial=digit.Segments.ToArray();digit.Segments.Clear();foreach(var v in initial)AddJoint(v);
        var add=canvas.Add(new Button("Add joint","add"));add.Clicked=()=>AddJoint(null);
        Choice(canvas,"Terminal tip",digit.Tip,v=>digit.Tip=v);
    }
    sealed class HandEdit
    {
        public Checkbox ManualPalm=null!; public LineEdit[] Forward=Array.Empty<LineEdit>(),Dorsal=Array.Empty<LineEdit>();
        public HandSide Side;public bool Enabled;public int? Wrist,Clavicle,Upper,Fore;public int[] Helpers;public List<DigitEdit> Digits=new();
        public HandEdit(HandSide side,HandRigDefinition? h){Side=side;Enabled=h is not null;Wrist=h?.Wrist;Clavicle=h?.Clavicle;Upper=h?.UpperArm;Fore=h?.Forearm;Helpers=h?.TwistOrHelperBones.ToArray()??Array.Empty<int>();}
    }
    sealed class DigitEdit
    {
        public DigitRole Role;public int? Meta,Tip;public string ExtraSlot;public List<int?> Segments;
        public DigitEdit(DigitRole role,DigitChain? d){Role=role;Meta=d?.Metacarpal;Tip=d?.Tip;ExtraSlot=d?.ExtraSlot??"";Segments=d?.Segments.Select(i=>(int?)i).ToList()??new(){null,null,null};}
    }
}