Editor/HumanoidMocap/MocapContactKeys.cs
using System;
using System.Globalization;
using System.Linq;
using Editor;
using HumanoidMocap.Motion;

namespace HumanoidMocap.Editor;

/// <summary>Edits a dialog-owned draft. No capture changes until the dialog is saved.</summary>
sealed class MocapContactKeys : Widget
{
    readonly MotionDocument motion;
    readonly ContactInterval draft;
    readonly Action readInterval,changed;
    readonly Action<string> status;
    readonly Widget pickerHost;
    int selected=-1;
    string savedTime,savedPosition;
    internal readonly LineEdit Time,LocalPositionInput;
    internal readonly Button ApplyKey,RemoveKey,SampleWrist,NewKey;
    internal bool HasUnappliedChanges=>Time.Text!=savedTime||LocalPositionInput.Text!=savedPosition;

    public MocapContactKeys(Widget parent,MotionDocument motion,ContactInterval draft,Action readInterval,
        Func<double> playhead,Action<string> status,Action changed):base(parent)
    {
        this.motion=motion;this.draft=draft;this.readInterval=readInterval;this.status=status;this.changed=changed;
        Layout=Layout.Column();Layout.Spacing=6;
        Layout.Add(new Label("Sliding keys · object-local metres / video seconds",this));
        pickerHost=Layout.Add(new Widget(this));pickerHost.Layout=Layout.Column();
        LineEdit Input(string title){var row=Layout.AddRow();row.Spacing=8;row.Add(new Label(title,this){FixedWidth=160});return row.Add(new LineEdit(this),1);}
        Time=Input("Key time (s)");LocalPositionInput=Input("Local X, Y, Z (m)");
        var placement=Layout.AddRow();placement.Spacing=6;
        placement.Add(new Button("Use playhead","schedule"){Clicked=()=>Time.Text=playhead().ToString("R",CultureInfo.InvariantCulture)});
        SampleWrist=placement.Add(new Button("Sample captured wrist","my_location"));
        SampleWrist.ToolTip="Fill from the nearest observed wrist and prop sample. Click Add key or Update key to keep it. This does not detect sliding automatically.";
        SampleWrist.Clicked=()=>Try(()=>{
            readInterval();var sample=ContactAuthoring.SampleWristAnchor(motion,draft,double.Parse(Time.Text,CultureInfo.InvariantCulture));
            Time.Text=sample.Time.ToString("R",CultureInfo.InvariantCulture);LocalPositionInput.Text=Text(sample.Local.Pos);
            status("Observed wrist sampled. Add or update the key to keep this placement.");
        });
        var buttons=Layout.AddRow();buttons.Spacing=6;
        NewKey=buttons.Add(new Button("New key","add"));NewKey.Clicked=()=>{selected=-1;Refresh();};
        ApplyKey=buttons.Add(new Button("Add key","check"));
        ApplyKey.Clicked=()=>Try(()=>{
            readInterval();var values=LocalPositionInput.Text.Split(',').Select(v=>float.Parse(v.Trim(),CultureInfo.InvariantCulture)).ToArray();
            if(values.Length!=3)throw new ArgumentException("Enter three comma-separated position values.");
            selected=ContactAuthoring.SetSlidingKey(draft,selected,double.Parse(Time.Text,CultureInfo.InvariantCulture),new(values[0],values[1],values[2]));
            Refresh();changed();status($"{draft.TargetKeys.Count} sliding keys. Save and review before confirming.");
        });
        RemoveKey=buttons.Add(new Button("Remove key","delete"));
        RemoveKey.Clicked=()=>Try(()=>{
            if(selected<0)return;draft.TargetKeys.RemoveAt(selected);selected=-1;Refresh();changed();
            status("Key removed from this draft. Sliding requires at least two keys before saving.");
        });
        selected=draft.TargetKeys.Count>0?0:-1;Refresh();
    }
    void Try(Action action){try{action();}catch(Exception error){status(error.Message);}}
    static string Text(System.Numerics.Vector3 v)=>string.Join(",",new[]{v.X,v.Y,v.Z}.Select(x=>x.ToString("R",CultureInfo.InvariantCulture)));
    void Select(int index)
    {
        selected=index;ApplyKey.Text=index<0?"Add key":"Update key";RemoveKey.Enabled=index>=0;
        if(index>=0){var key=draft.TargetKeys[index];Time.Text=key.Time.ToString("R",CultureInfo.InvariantCulture);LocalPositionInput.Text=Text(MotionDocument.V(key.Position));}
        else{Time.Text=draft.Start.ToString("R",CultureInfo.InvariantCulture);LocalPositionInput.Text=Text(MotionDocument.V(draft.LocalTarget));}
        savedTime=Time.Text;savedPosition=LocalPositionInput.Text;
    }
    void Refresh()
    {
        pickerHost.Layout.Clear(true);var picker=pickerHost.Layout.Add(new ComboBox(this));
        picker.AddItem("New key…",onSelected:()=>Select(-1),selected:selected<0);
        for(var i=0;i<draft.TargetKeys.Count;i++)
        {var index=i;var key=draft.TargetKeys[i];picker.AddItem($"{key.Time:F3} s · {Text(MotionDocument.V(key.Position))}",onSelected:()=>Select(index),selected:i==selected);}
        Select(selected);
    }
}