Editor/ImportConflictWindow.cs
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Editor;
using Sandbox;

namespace ImportUnityPackage;

/// <summary>Review destination conflicts without changing package selection.</summary>
public sealed class ImportConflictWindow : Widget
{
	readonly TaskCompletionSource<bool> completion = new();
	readonly CancellationToken token;
	public static async Task Review( string title, string destination, IEnumerable<ImportFileChoice> entries, CancellationToken cancel )
	{
		var window = new ImportConflictWindow( title, destination, entries.ToArray(), cancel );
		window.Show();
		if ( !await window.completion.Task ) throw new OperationCanceledException( "Import review cancelled", cancel );
	}

	ImportConflictWindow( string title, string destination, ImportFileChoice[] entries, CancellationToken cancel ) : base( null, true )
	{
		token = cancel;
		WindowTitle = title;
		Size = new Vector2( 850, 600 );
		MinimumSize = new Vector2( 600, 350 );
		Layout = Layout.Column();
		Layout.Margin = 16;
		Layout.Spacing = 8;
		Layout.Add( new Label( $"Destination: {destination}\n{entries.Count( c => c.ExistingHash == null )} new · {entries.Count( c => c.ExistingHash != null && !c.Conflict )} identical · {entries.Count( c => c.Conflict )} conflicts" ) { WordWrap = true } );
		Layout.Add( new Label( "Conflicts keep the existing file unless Replace is checked. Replacing shared files can change models and scenes that already use them." ) { WordWrap = true } );
		var controls = Layout.AddRow();
		var keep = controls.Add( new Button( "Keep all existing" ) );
		var replace = controls.Add( new Button( "Replace all conflicts" ) );
		controls.AddStretchCell();
		var scroll = Layout.Add( new ScrollArea( this ), 1 );
		scroll.Canvas = new Widget( this );
		scroll.Canvas.SetSizeMode( SizeMode.Default, SizeMode.CanGrow );
		scroll.Canvas.Layout = Layout.Column();
		scroll.Canvas.Layout.Spacing = 8;
		var checkboxes = new List<Checkbox>();
		foreach ( var entry in entries.OrderByDescending( c => c.Conflict ).ThenBy( c => c.Path ) )
		{
			if ( entry.Conflict )
			{
				var box = scroll.Canvas.Layout.Add( new Checkbox( $"Replace · {entry.Status} · {entry.Path}" ) { Value = entry.Replace } );
				box.StateChanged += _ => entry.Replace = box.Value;
				checkboxes.Add( box );
				var detail = entry.Detail;
				var asset = AssetSystem.FindByPath( Path.Combine( destination, entry.Path ) );
				if ( asset != null )
				{
					var users = asset.GetDependants( true ).Select( a => a.Path ).Distinct().ToArray();
					if ( users.Length > 0 ) detail += " Project references: " + string.Join( ", ", users.Take( 6 ) ) + (users.Length > 6 ? $" (+{users.Length - 6} more)" : "");
				}
				if ( !string.IsNullOrWhiteSpace( detail ) ) scroll.Canvas.Layout.Add( new Label( detail ) { WordWrap = true } );
			}
			else scroll.Canvas.Layout.Add( new Label( $"{entry.Action} · {entry.Path}" ) );
		}
		scroll.Canvas.Layout.AddStretchCell();
		keep.Clicked = () => { foreach ( var entry in entries.Where( e => e.Conflict ) ) entry.Replace = false; checkboxes.ForEach( b => b.Value = false ); };
		replace.Clicked = () => { foreach ( var entry in entries.Where( e => e.Conflict ) ) entry.Replace = true; checkboxes.ForEach( b => b.Value = true ); };
		var buttons = Layout.AddRow();
		buttons.AddStretchCell();
		buttons.Add( new Button( "Cancel import" ) ).Clicked = () => Close();
		buttons.Add( new Button.Primary( "Continue" ) ).Clicked = () => { completion.TrySetResult( true ); Close(); };
	}

	[EditorEvent.Frame]
	void Tick() { if ( IsValid && token.IsCancellationRequested ) Close(); }
	protected override bool OnClose() { completion.TrySetResult( false ); return true; }
}