Editor/WeaponImporter/Tool/UI/Steps/CheckStep.cs
using Editor;
using Sandbox;
using WeaponImporter.Core.Setup;

namespace WeaponImporter.Tool.UI;

/// <summary>Step 4: what still needs attention, then Bake and the generated files.</summary>
public sealed class CheckStep : StepPanel
{
	public CheckStep( Widget parent, ImporterController controller ) : base( parent, controller )
	{
		controller.BusyChanged += MarkDirty;
	}

	public override string Title => "Export";

	protected override string StructureKey()
	{
		var checks = string.Join( ";", C.Session.Checks.Select( c => $"{c.Name}:{c.Severity}:{c.Message}" ) );
		return $"{checks}|{C.Busy}|{C.LastBake?.GetHashCode()}";
	}

	protected override void Build()
	{
		BuildChecks();
		BuildBake();
	}

	private void BuildChecks()
	{
		var checks = C.Session.Checks;
		var problems = Validation.Problems( checks ).ToList();
		var card = AddCard( "fact_check", "Validation", out var header, "Everything the importer verified" );
		header.AddStretchCell();
		if ( problems.Count == 0 )
			header.Add( new Pill( card, "ALL GOOD", Theme.Green ) );
		else
			header.Add( new Pill( card, $"{problems.Count} TO LOOK AT", problems.Any( p => p.Severity == CheckSeverity.Error ) ? Theme.Red : Theme.Yellow ) );

		foreach ( var check in problems )
			card.Layout.Add( new CheckLine( card, check, expanded: true, () => Fix( check ) ) );
		if ( problems.Count > 0 )
			card.Layout.AddSpacingCell( 4 );
		foreach ( var check in checks.Where( c => !c.NeedsAttention ) )
			card.Layout.Add( new CheckLine( card, check, expanded: false, check.AutoFix is null ? null : () => Fix( check ) ) );
		if ( checks.Count == 0 )
			card.Layout.Add( UiStyle.Muted( new Label( "Nothing checked yet.", card ) ) );
	}

	private void Fix( CheckResult check )
	{
		Safe( () =>
		{
			check.AutoFix?.Invoke();
			C.Session.Revalidate();
			C.MarkChanged();
			C.ScheduleResolve( 0.01f );
			C.SetStatus( $"{check.Name}: {check.AutoFixLabel.ToLowerInvariant()} applied.", Theme.Green );
		} );
	}

	private void BuildBake()
	{
		var session = C.Session;
		var card = AddCard( "inventory_2", "Bake", out _, "Write the model, animations, attachments and a ready prefab" );
		card.Layout.Add( UiStyle.Muted( new Label( $"Output: {session.OutputFolder}/", card ) { WordWrap = true, ToolTip = "Files are written here inside the project's assets" }, small: true ) );
		var s = session.Setup;
		if ( session.Analysis?.ArmBones.Count > 0 )
		{
			var fp = card.Layout.Add( new Checkbox( "Bake the first-person viewmodel", card ) { Value = s.ExportFirstPerson } );
			fp.ToolTip = "Also write <name>_fp.vmdl: this file's own arms, weapon and camera with every animation as authored. The prefab shows it to the player holding the weapon, in sync with the third-person hold.";
			fp.StateChanged = _ => s.ExportFirstPerson = fp.Value;
		}
		var sizeRow = UiStyle.FieldRow( card, card.Layout, "Textures", "Largest texture size in the baked weapon; bigger images are scaled down (smaller files, same look at normal distances)" );
		var size = sizeRow.Add( UiStyle.Framed( new ComboBox( card ) ), 1 );
		foreach ( var (label, value) in new[] { ("Original size", 0), ("4096", 4096), ("2048", 2048), ("1024", 1024) } )
		{
			var v = value;
			size.AddItem( label, "photo_size_select_large", () => s.MaxTextureSize = v, selected: s.MaxTextureSize == value );
		}
		var corrected = card.Layout.Add( new Checkbox( "Bake corrected third-person animations", card ) { Value = s.BakeCorrectedAnimations } );
		corrected.ToolTip = "Also record the character's actions with the hands on this weapon as new clips (<name>_corrected.vmdl). The character's own animations are never changed.";
		corrected.StateChanged = _ => s.BakeCorrectedAnimations = corrected.Value;
		var use = card.Layout.Add( new Checkbox( "Play the corrected clips in the prefab", card ) { Value = s.UseCorrectedAnimations } );
		use.ToolTip = "Off: the prefab corrects the character's live animation every frame (works with any animgraph blend). On: it plays the baked corrected clips over the upper body.";
		use.StateChanged = _ => s.UseCorrectedAnimations = use.Value;
		var bake = card.Layout.Add( new Button.Primary( "Bake", card ) { Icon = "inventory_2", Tint = Theme.Green, FixedHeight = 36, Enabled = !C.Busy, ToolTip = "Write and compile the weapon model and its prefab" } );
		bake.Clicked = () => _ = C.BakeAsync();

		var result = C.LastBake;
		if ( result is null )
			return;

		card.Layout.Add( new SectionHeader( card, result.Success ? "Baked" : "Bake finished with errors" ) );
		var links = card.Layout.AddRow();
		links.Spacing = 6;
		links.Add( new UiButton( card, "Open model", "view_in_ar", () => Open( result.ModelPath ), result.ModelPath ) { Enabled = FindAsset( result.ModelPath ) is not null } );
		links.Add( new UiButton( card, "Open prefab", "inventory_2", () => Open( result.PrefabPath ), result.PrefabPath ) { Enabled = FindAsset( result.PrefabPath ) is not null } );
		links.Add( new UiButton( card, "Show in asset browser", "folder_open", () => Reveal( result.ModelPath ), "Reveal the generated model in the asset browser" ) );
		links.AddStretchCell();

		foreach ( var file in result.Files )
		{
			var errors = result.Errors.TryGetValue( file, out var list ) ? list : null;
			var row = card.Layout.AddRow();
			row.Spacing = 6;
			row.Add( new IconLabel( card, errors is null ? "check_circle" : "error", errors is null ? Theme.Green : Theme.Red ) );
			row.Add( new Label( file, card ) { ToolTip = AssetCompiler.Absolute( file ) }, 1 );
			if ( errors is not null )
			{
				foreach ( var e in errors.Take( 6 ) )
				{
					var er = card.Layout.AddRow();
					er.AddSpacingCell( 26 );
					er.Add( UiStyle.Colored( new Label( e, card ) { WordWrap = true }, Theme.Red ), 1 );
				}
			}
		}
		foreach ( var (file, errors) in result.Errors.Where( kv => !result.Files.Contains( kv.Key ) ) )
			card.Layout.Add( UiStyle.Colored( new Label( $"{file}: {errors.FirstOrDefault()}", card ) { WordWrap = true }, Theme.Red ) );
		foreach ( var note in result.Notes )
			card.Layout.Add( UiStyle.Muted( new Label( note, card ) { WordWrap = true }, small: true ) );
	}

	private static Asset FindAsset( string relative ) => string.IsNullOrEmpty( relative ) ? null : AssetSystem.FindByPath( relative );

	private void Open( string relative )
	{
		Safe( () =>
		{
			var asset = FindAsset( relative );
			if ( asset is null )
				C.SetStatus( $"{relative} is not in the asset system yet.", Theme.Yellow );
			else
				asset.OpenInEditor();
		} );
	}

	private void Reveal( string relative )
	{
		Safe( () =>
		{
			var asset = FindAsset( relative );
			if ( asset is not null )
				AssetBrowser.OpenTo( asset );
			else
				EditorUtility.OpenFolder( System.IO.Path.GetDirectoryName( AssetCompiler.Absolute( relative ) ) );
		} );
	}

	public override void OnDestroyed()
	{
		C.BusyChanged -= MarkDirty;
		base.OnDestroyed();
	}
}

/// <summary>"✓ Skeleton" / "! Left wrist penetration detected" / "× Muzzle missing".</summary>
public sealed class CheckLine : Widget
{
	private readonly CheckResult _check;
	private readonly bool _expanded;

	public CheckLine( Widget parent, CheckResult check, bool expanded, Action fix ) : base( parent )
	{
		_check = check;
		_expanded = expanded;
		Layout = Layout.Row();
		Layout.Spacing = 8;
		Layout.Margin = expanded ? new Sandbox.UI.Margin( 10, 7, 7, 7 ) : new Sandbox.UI.Margin( 10, 1, 4, 1 );
		var color = ColorOf( check.Severity );
		Layout.Add( new Glyph( this, GlyphOf( check.Severity ), color ) );
		var text = Layout.AddColumn( 1 );
		text.Spacing = 2;
		if ( expanded )
		{
			text.Add( UiStyle.Colored( new Label( check.Name, this ), color, bold: true ) );
			if ( !string.IsNullOrEmpty( check.Message ) )
				text.Add( new Label( check.Message, this ) { WordWrap = true } );
		}
		else
		{
			var line = text.AddRow();
			line.Spacing = 6;
			line.Add( new Label( check.Name, this ) );
			if ( !string.IsNullOrEmpty( check.Message ) )
				line.Add( UiStyle.Muted( new Label( check.Message, this ) { ToolTip = check.Message, WordWrap = true, MinimumWidth = 20 }, small: true ), 1 );
			else
				line.AddStretchCell();
		}
		if ( fix is not null && check.AutoFix is not null )
		{
			var button = Layout.Add( new UiButton( this, check.AutoFixLabel, "auto_fix_high", null, $"Apply the safe fix for: {check.Name}" ) );
			button.Clicked = fix;
		}
		ToolTip = string.IsNullOrEmpty( check.Message ) ? check.Name : $"{check.Name}: {check.Message}";
	}

	public static Color ColorOf( CheckSeverity s ) => s switch
	{
		CheckSeverity.Error => Theme.Red,
		CheckSeverity.Warning => Theme.Yellow,
		CheckSeverity.Info => Theme.Blue,
		_ => Theme.Green,
	};

	public static string GlyphOf( CheckSeverity s ) => s switch
	{
		CheckSeverity.Error => "×",
		CheckSeverity.Warning => "!",
		CheckSeverity.Info => "i",
		_ => "✓",
	};

	protected override void OnPaint()
	{
		if ( !_expanded )
			return;
		Paint.Antialiasing = true;
		var color = ColorOf( _check.Severity );
		Paint.ClearPen();
		Paint.SetBrush( color.WithAlpha( .07f ) );
		Paint.DrawRect( LocalRect, 4 );
		Paint.SetBrush( color );
		Paint.DrawRect( new Rect( 0, 0, 3, Height ), 1.5f );
	}

	private sealed class Glyph : Widget
	{
		private readonly string _glyph;
		private readonly Color _color;

		public Glyph( Widget parent, string glyph, Color color ) : base( parent )
		{
			_glyph = glyph;
			_color = color;
			FixedSize = 16;
		}

		protected override void OnPaint()
		{
			Paint.SetPen( _color );
			Paint.SetDefaultFont( 9, 700 );
			Paint.DrawText( LocalRect, _glyph, TextFlag.Center );
		}
	}
}