Editor/HumanoidRetargeter/SmartPortDialog.cs
#nullable enable annotations

using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Editor;

namespace HumanoidRetargeter.Editor;

/// <summary>A separate, non-destructive source-to-target model port workflow.</summary>
public sealed class SmartPortDialog : Dialog
{
	readonly Button _sourcePick, _targetPick, _port;
	readonly LineEdit _folder, _name;
	readonly Label _status, _description;
	readonly Checkbox _extend;
	readonly CancellationTokenSource _cancel = new();
	Asset _source, _target;
	bool _busy, _destroyed;

	public SmartPortDialog( Widget parent ) : base( parent )
	{
		Window.WindowTitle = "Smart Port";
		Window.SetWindowIcon( "swap_horiz" );
		Window.SetModal( true, true );
		Window.MinimumWidth = 620;
		Layout = Layout.Column();
		Layout.Margin = 20;
		Layout.Spacing = 12;
		Layout.Add( new Label.Subtitle( "Bring an animation setup to another character" ) );
		_description = Layout.Add( new Label( this ) { WordWrap = true } );
		_sourcePick = Layout.Add( new Button( "Source: choose animation model…", "directions_run" ) );
		_targetPick = Layout.Add( new Button( "Target: choose your character…", "accessibility_new" ) );
		_sourcePick.Clicked = () => Pick( true );
		_targetPick.Clicked = () => Pick( false );
		_extend = Layout.Add( new Checkbox( "Keep target animations and extend its graph" ) );
		_extend.ToolTip = "Keep the target's existing animations and graph as the default. Add source clips through a new graph selector; do not merge the source graph's logic. Originals are never overwritten.";
		_extend.Clicked = Refresh;
		Layout.Add( new Label( this ) { Text = "Output folder (inside Assets):" } );
		_folder = Layout.Add( new LineEdit( this ) { Text = "models/smart_port" } );
		Layout.Add( new Label( this ) { Text = "New model name:" } );
		_name = Layout.Add( new LineEdit( this ) { Text = "ported_character" } );
		Layout.Add( new Label( this ) { WordWrap = true, Text =
			"Local and cloud assets are available in the picker. Compiled recovery is built in using ValveResourceFormat source (s2v.app); no separate program is needed. "
			+ "Recovery may not reproduce every authoring feature; review the generated model in game. Use assets you have permission to reuse." } );
		_status = Layout.Add( new Label( this ) { WordWrap = true } );
		var actions = Layout.AddRow();
		actions.Spacing = 8;
		_port = actions.Add( new Button( "Smart Port", "auto_fix_high" ) );
		_port.Clicked = () => _ = PortAsync();
		actions.AddStretchCell();
		var close = actions.Add( new Button( "Close" ) );
		close.Clicked = Close;
		Refresh();
		Window.AdjustSize();
	}

	void Pick( bool source )
	{
		var picker = AssetPicker.Create( this, AssetType.Model );
		picker.Window.Title = source ? "Smart Port — source animation model (local or cloud)" : "Smart Port — target character (local or cloud)";
		picker.OnAssetPicked = assets =>
		{
			if ( _destroyed || _busy ) return;
			var asset = assets.FirstOrDefault();
			if ( asset is null ) return;
			if ( source ) { _source = asset; _sourcePick.Text = "Source: " + asset.Path; }
			else { _target = asset; _targetPick.Text = "Target: " + asset.Path; }
			Refresh();
		};
		picker.Show();
	}

	void Refresh()
	{
		_description.Text = _extend.Value
			? "Target keeps its existing animations, graph logic and character setup. Source adds retargeted clips through a new graph selector; its graph logic is not merged. A new model and editable graph are created, leaving both originals untouched."
			: "Source supplies animations, events, rig settings and the actual animgraph. Target supplies its mesh, materials and collision. Matching armatures copy directly; other recognized humanoids are retargeted automatically.";
		string reason;
		try { reason = _extend.Value ? SmartPortModels.CheckExtended( _source, _target ) : SmartPortModels.Check( _source, _target ); }
		catch ( Exception e ) { reason = e.Message; }
		_port.Enabled = !_busy && reason is null;
		_port.SetStyles( reason is null ? "background-color: #287d46;" : "" );
		_status.Text = reason ?? "Compatible armatures. Ready to create a new model and project-owned animgraph.";
	}

	async Task PortAsync()
	{
		if ( _busy ) return;
		_busy = true;
		_status.ToolTip = "";
		_port.Enabled = _sourcePick.Enabled = _targetPick.Enabled = _folder.Enabled = _name.Enabled = _extend.Enabled = false;
		try
		{
			Action<string> progress = message => { if ( !_destroyed ) _status.Text = message; };
			var result = _extend.Value
				? await SmartPortModels.CreateExtendedAsync( _source, _target, _folder.Text, _name.Text, progress, _cancel.Token )
				: await SmartPortModels.CreateAsync( _source, _target, _folder.Text, _name.Text, progress, _cancel.Token );
			if ( !_destroyed ) _status.Text = result.Compiled ? "Created and verified: " + result.VmdlAsset.Path
				+ (_extend.Value ? "\nExisting animations remain the default. Clip controls are listed in the output's _smart_port/clips.txt." : "")
				: "Port did not pass verification:\n" + string.Join( "\n", result.Errors );
		}
		catch ( OperationCanceledException ) { }
		catch ( Exception e )
		{
			Log.Warning( $"[humanoid-retargeter] Smart Port failed: {e}" );
			if ( !_destroyed )
			{
				_status.Text = "Smart Port did not finish.\n" + e.Message;
				_status.ToolTip = e.Message;
			}
		}
		finally
		{
			_busy = false;
			if ( !_destroyed )
			{
				_sourcePick.Enabled = _targetPick.Enabled = _folder.Enabled = _name.Enabled = _extend.Enabled = true;
				var status = _status.Text;
				Refresh();
				_status.Text = status;
			}
		}
	}

	public override void OnDestroyed()
	{
		_destroyed = true;
		_cancel.Cancel();
		base.OnDestroyed();
	}
}