Editor/UI/ArchPresetBrowserDialog.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Editor;
using Sandbox;

namespace Sunless.Architecture;

// The expanded half of the preset chooser, reached from every inline browser's header so browsing,
// comparing and designing take one route whatever the tool.
public sealed class ArchPresetBrowserDialog<T> : Dialog, IArchPreviewWatcher {
	const string Everything = "All";
	const string Preferred = "Recommended";
	const float Strip = 34f;

	readonly ArchKit kit;
	readonly IReadOnlyList<ArchPresetItem<T>> items;
	readonly Func<T, bool> chosen;
	readonly Action<T> use;
	readonly LineEdit search;
	readonly ArchTileGrid grid;
	readonly Label counted;
	readonly List<Widget> cards = new();
	readonly List<Button> categories = new();

	string category = Preferred;
	ArchPresetItem<T> pending;

	public ArchPresetBrowserDialog(
		Widget parent,
		ArchKit kit,
		string title,
		IReadOnlyList<ArchPresetItem<T>> items,
		Func<T, bool> chosen,
		Action<T> use ) : base( parent ) {
		this.kit = kit;
		this.items = items;
		this.chosen = chosen;
		this.use = use;

		pending = items.FirstOrDefault( item => chosen?.Invoke( item.Value ) == true );

		Window.Title = title;
		Window.SetWindowIcon( "grid_view" );
		Window.SetModal( true, true );
		Window.Size = new Vector2( 1000, 680 );
		Window.MinimumWidth = 780;
		Window.MinimumHeight = 520;

		Layout = Layout.Column();
		Layout.Margin = 0;
		Layout.Spacing = 0;

		var split = Layout.AddRow( 1 );

		split.Add( Navigation() );
		split.AddSeparator();

		var right = split.AddColumn( 1 );
		right.Margin = 10;
		right.Spacing = 8;

		var top = right.AddRow();
		top.Spacing = 8;

		search = new LineEdit( "" ) { PlaceholderText = $"Search {title.ToLowerInvariant()}" };
		search.TextEdited += _ => Fill();
		top.Add( search, 1 );

		counted = new Label( "" );
		counted.SetStyles( "font-size: 10px; color: rgba(255,255,255,0.4);" );
		top.Add( counted );

		grid = new ArchTileGrid( this, ArchTileSize.Browsed );
		right.Add( grid, 1 );

		Layout.AddSeparator();

		var footer = Layout.AddRow();
		footer.Margin = 10;
		footer.Spacing = 6;
		footer.AddStretchCell();

		DesignButton = new Button( "Design new preset", "tune" ) { Clicked = () => Design?.Invoke() };
		footer.Add( DesignButton );

		footer.Add( new Button( "Cancel", "close" ) { Clicked = Close } );
		footer.Add( new Button.Primary( "Use preset", "check" ) { Clicked = Confirm } );

		Fill();

		ArchPreviewPump.Watch( this );

		search.Focus();
	}

	public Button DesignButton { get; }

	public Action Design { get; set; }

	// Forwarded from the inline browser, so the expanded wall of cards offers the same right-click it does.
	public Action<ArchPresetItem<T>> Remove { get; set; }

	public Func<ArchPresetItem<T>, string> Removes { get; set; }

	public Action<ArchPresetItem<T>, Menu> Options { get; set; }

	public Action<ArchPresetItem<T>> Export { get; set; }

	// The cards are laid in the CONSTRUCTOR, so anything assigned after it has to say so - a tile built while
	// Remove and Options were still null carries no menu at all, and nothing anywhere reports that.
	public void Refill() => Fill();

	Widget Navigation() {
		var column = new Widget( this ) { Layout = Layout.Column() };
		column.Layout.Margin = 10;
		column.Layout.Spacing = 3;
		column.FixedWidth = 168;

		foreach ( var name in Names() ) {
			var captured = name;
			var button = new Button( name ) { IsToggle = true, Clicked = () => Navigate( captured ) };

			categories.Add( button );
			column.Layout.Add( button );
		}

		column.Layout.AddStretchCell();

		Lit();

		return column;
	}

	List<string> Names() {
		var names = new List<string>();

		if ( items.Any( item => item.Recommended ) ) {
			names.Add( Preferred );
		}

		names.Add( Everything );
		names.AddRange( items
			.Select( item => item.Category )
			.Where( name => !string.IsNullOrWhiteSpace( name ) )
			.Distinct()
			.OrderBy( name => name ) );

		category = names[0];

		return names;
	}

	void Navigate( string name ) {
		category = name;

		Lit();
		Fill();
	}

	void Lit() {
		foreach ( var button in categories ) {
			button.IsChecked = button.Text == category;
		}
	}

	void Fill() {
		var showing = items
			.Where( Included )
			.Where( item => item.Matches( search.Text ) )
			.OrderByDescending( item => item.Recommended )
			.ToList();

		counted.Text = $"{showing.Count} of {items.Count}";
		cards.Clear();

		var section = new ArchTileSection();

		foreach ( var item in showing ) {
			var captured = item;

			section.Tiles.Add( canvas => Card( canvas, captured ) );
		}

		grid.Set( new[] { section } );
	}

	bool Included( ArchPresetItem<T> item ) {
		return category switch {
			Everything => true,
			Preferred => item.Recommended,
			_ => item.Category == category
		};
	}

	Widget Card( Widget canvas, ArchPresetItem<T> item ) {
		var tile = new ArchTile( canvas, null, item.Name, item.Detail, false, ArchTileSize.Browsed ) {
			Badge = item.Badge,
			ToolTip = string.IsNullOrWhiteSpace( item.Detail ) ? item.Name : $"{item.Name} — {item.Detail}",
			Current = () => pending == item,
			Clicked = () => { pending = item; Repaint(); },
			Opened = Confirm
		};

		var exports = Export is not null && item.Recipe is not null;

		if ( Remove is not null || Options is not null || exports ) {
			tile.Menu = menu => {
				Options?.Invoke( item, menu );

				if ( exports ) {
					menu.AddOption( $"Export {item.Name} to OBJ…", "download", () => Export( item ) );
				}

				if ( Remove is not null ) {
					menu.AddOption( Removes?.Invoke( item ) ?? $"Remove {item.Name}", "delete", () => Remove( item ) );
				}
			};
		}

		if ( item.Recipe is not null ) {
			var art = new Vector2( ArchTileSize.Browsed.x, ArchTileSize.Browsed.y - Strip ) * 1.75f;

			tile.Source = () => ArchPresetPreview.For( item.Identity, item.Shot( art ), kit, item.Recipe );
		} else if ( !string.IsNullOrWhiteSpace( item.Glyph ) ) {
			tile.Glyph = rect => Paint.DrawIcon( rect, item.Glyph, rect.Height );
		}

		cards.Add( tile );

		return tile;
	}

	void Repaint() {
		foreach ( var card in cards.Where( card => card.IsValid() ) ) {
			card.Update();
		}
	}

	void Confirm() {
		if ( pending is null ) {
			return;
		}

		use( pending.Value );
		Close();
	}

	public bool Alive => this.IsValid();

	public void PreviewTick( bool advanced ) {
		if ( advanced ) {
			Repaint();
		}
	}

	protected override void OnKeyPress( KeyEvent e ) {
		if ( e.Key == KeyCode.Enter || e.Key == KeyCode.Return ) {
			Confirm();
			e.Accepted = true;
			return;
		}

		base.OnKeyPress( e );
	}
}