Editor/UI/ArchMaterialBrowser.cs
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Editor;
using Sandbox;
using Margin = Sandbox.UI.Margin;

namespace Sunless.Architecture;

public sealed class ArchMaterialBrowser : Dialog {
	sealed class Entry {
		public string Name { get; init; }
		public string Category { get; init; }
		public string Id { get; init; }
		public string Kind { get; init; }
	}

	readonly ArchMapPalette.Document document;
	readonly Action changed;
	readonly List<Entry> library = new();

	ArchTileGrid roles;
	ArchTileGrid grid;
	Label roleHeading;
	Label roleDetail;
	LineEdit search;
	ComboBox categories;

	Dictionary<ArchSurface, ArchMapPalette.Surface> bindings = new();
	ArchMapPalette.Surface selected;
	string categoryFilter = "";
	string query = "";

	public ArchMaterialBrowser( Widget parent, ArchMapPalette.Document document, Action changed ) : base( parent ) {
		this.document = document;
		this.changed = changed;

		Window.Title = $"RoP Materials - {document.Map}";
		Window.SetWindowIcon( "texture" );

		var screen = parent?.ScreenGeometry ?? new Rect( 0, 0, 1600, 900 );
		Window.Size = new Vector2( Math.Max( 1280f, screen.Width * 0.7f ), Math.Max( 720f, screen.Height * 0.7f ) );

		ScanLibrary();
		Build();

		bindings = ArchMapPalette.Resolve( document );
		selected = document.Surfaces.FirstOrDefault();

		ShowRoles();
		ShowSelected();
		ShowLibrary();
	}

	void ScanLibrary() {
		library.Clear();

		var wanted = ArchHost.Load().MaterialLibrary.Replace( '/', Path.DirectorySeparatorChar );
		var root = Path.Combine( Project.Current?.GetRootPath() ?? "", wanted );

		if ( !Directory.Exists( root ) ) {
			return;
		}

		foreach ( var categoryDir in Directory.GetDirectories( root ).OrderBy( p => p ) ) {
			var category = Path.GetFileName( categoryDir );

			foreach ( var materialDir in Directory.GetDirectories( categoryDir ).OrderBy( p => p ) ) {
				var name = Path.GetFileName( materialDir );

				if ( !File.Exists( Path.Combine( materialDir, $"{name}.vmat" ) ) ) {
					continue;
				}

				library.Add( new Entry {
					Name = name,
					Category = category,
					Id = SourceId( materialDir ),
					Kind = name.StartsWith( "map_trim_", StringComparison.OrdinalIgnoreCase ) ? "trim" : "tile"
				} );
			}
		}
	}

	// The importer stamps "// SOURCE megascans:<id>" into the vmat, or leaves _source.txt beside the pngs.
	static string SourceId( string materialDir ) {
		foreach ( var candidate in Directory.GetFiles( materialDir, "_source.txt" ).Concat( Directory.GetFiles( materialDir, "*.vmat" ) ) ) {
			try {
				var text = File.ReadAllText( candidate );
				var marker = text.IndexOf( "megascans:", StringComparison.OrdinalIgnoreCase );

				if ( marker < 0 ) {
					continue;
				}

				var id = new string( text.Skip( marker + 10 ).TakeWhile( char.IsLetterOrDigit ).ToArray() );

				if ( !string.IsNullOrWhiteSpace( id ) ) {
					return id;
				}
			} catch ( Exception ) {
			}
		}

		return null;
	}

	void Build() {
		Layout = Layout.Column();
		Layout.Margin = 0;

		var split = Layout.AddRow( 1 );

		var sidebar = new Widget( this ) { Layout = Layout.Column() };
		sidebar.MinimumWidth = 300;
		sidebar.MaximumWidth = 300;
		sidebar.Layout.Margin = 0;
		sidebar.Layout.Spacing = 0;

		var caption = new Label( "Palette roles", sidebar );
		caption.SetStyles( "font-weight: 700; font-size: 13px; margin: 10px 12px 6px 12px;" );
		sidebar.Layout.Add( caption );
		sidebar.Layout.AddSeparator();

		roles = new ArchTileGrid( sidebar, ArchTileSize.Role );
		sidebar.Layout.Add( roles, 1 );

		split.Add( sidebar );
		split.AddSeparator();

		var detail = new Widget( this ) { Layout = Layout.Column() };
		detail.Layout.Margin = 0;
		detail.Layout.Spacing = 0;

		var head = detail.Layout.AddColumn();
		head.Margin = new Margin( 12, 10, 12, 8 );
		head.Spacing = 3;

		roleHeading = new Label( "" );
		roleHeading.SetStyles( "font-size: 15px; font-weight: 700;" );
		head.Add( roleHeading );

		roleDetail = new Label( "" );
		roleDetail.SetStyles( "color: rgba(255,255,255,0.55); font-size: 11px;" );
		roleDetail.WordWrap = true;
		head.Add( roleDetail );

		var filters = detail.Layout.AddRow();
		filters.Margin = new Margin( 12, 0, 12, 8 );
		filters.Spacing = 6;

		search = new LineEdit( "" ) { PlaceholderText = "Filter materials..." };
		search.TextEdited += text => { query = text ?? ""; ShowLibrary(); };
		filters.Add( search, 1 );

		categories = new ComboBox();
		categories.AddItem( "All categories", null, () => { categoryFilter = ""; ShowLibrary(); } );

		foreach ( var category in library.Select( e => e.Category ).Distinct().OrderBy( c => c ) ) {
			var captured = category;
			categories.AddItem( captured, null, () => { categoryFilter = captured; ShowLibrary(); } );
		}

		filters.Add( categories );

		detail.Layout.AddSeparator();

		grid = new ArchTileGrid( detail, ArchTileSize.Preview );
		detail.Layout.Add( grid, 1 );

		detail.Layout.AddSeparator();

		var footer = detail.Layout.AddRow();
		footer.Margin = new Margin( 12, 8, 12, 10 );
		footer.Spacing = 6;

		footer.Add( new Button( "Rebuild Previews", "refresh" ) {
			ToolTip = "Wipe the cached thumbnails and 3D slot previews, then re-render them",
			Clicked = () => {
				ArchThumb.Forget();
				ArchSlotPreview.Invalidate();
				ShowRoles();
				ShowLibrary();
			}
		} );

		footer.AddStretchCell();
		footer.Add( new Button.Primary( "Done", "check" ) { Clicked = Close } );

		split.Add( detail, 1 );
	}

	void ShowRoles() {
		bindings = ArchMapPalette.Resolve( document );

		var bound = new HashSet<ArchMapPalette.Surface>( bindings.Values );

		var builder = new ArchTileSection {
			Title = $"BUILDER SLOTS - {bindings.Count}",
			Note = "Assigned automatically by the Architecture tool."
		};

		foreach ( var slot in ArchMapPalette.SlotOrder ) {
			if ( !bindings.TryGetValue( slot, out var surface ) ) {
				continue;
			}

			var captured = surface;
			var captureSlot = slot;

			builder.Tiles.Add( parent => RoleTile( parent, captured, captureSlot ) );
		}

		var extra = new ArchTileSection {
			Title = $"PALETTE ONLY - {document.Surfaces.Count - bound.Count}",
			Note = "Hand-painted per face in the Mapping tool."
		};

		foreach ( var surface in document.Surfaces.Where( s => !bound.Contains( s ) ) ) {
			var captured = surface;
			extra.Tiles.Add( parent => RoleTile( parent, captured, null ) );
		}

		roles.Set( new[] { builder, extra } );
	}

	ArchTile RoleTile( Widget parent, ArchMapPalette.Surface surface, ArchSurface? slot ) {
		var missing = !ArchMapPalette.IsImported( surface );

		var thumb = slot.HasValue && !missing
			? ArchSlotPreview.For( document.Map, slot.Value, bindings, 512 ) ?? ArchThumb.For( surface.Category, surface.Name, surface.Id )
			: ArchThumb.For( surface.Category, surface.Name, surface.Id );

		return new ArchTile( parent, thumb, surface.Role, missing ? "NOT IMPORTED" : surface.Category, missing, ArchTileSize.Role ) {
			Badge = slot?.ToString(),
			Glyph = slot.HasValue ? box => ArchSlotIcon.Draw( box, slot.Value, Color.White ) : null,
			Dim = slot.HasValue ? 1f : 0.7f,
			ToolTip = surface.Name,
			Clicked = () => { selected = surface; ShowSelected(); ShowLibrary(); Update(); },
			Current = () => ReferenceEquals( selected, surface )
		};
	}

	void ShowSelected() {
		if ( selected is null ) {
			roleHeading.Text = "No roles";
			roleDetail.Text = "";
			return;
		}

		var slots = ArchMapPalette.SlotsDrivenBy( bindings, selected );

		roleHeading.Text = selected.Role;

		roleDetail.Text = slots.Count > 0
			? $"Drives {string.Join( ", ", slots )}  -  currently {selected.Name} ({selected.Kind}, {selected.Category})"
			: $"Not auto-assigned  -  currently {selected.Name} ({selected.Kind}, {selected.Category})";
	}

	void ShowLibrary() {
		var matches = library.Where( Matches ).ToList();

		var section = new ArchTileSection {
			Title = $"MATERIAL LIBRARY - {matches.Count} of {library.Count}",
			Note = selected is null
				? "Select a role on the left first."
				: $"Click one to reassign '{selected.Role}'."
		};

		foreach ( var entry in matches ) {
			var captured = entry;

			section.Tiles.Add( parent => {
				var trim = captured.Kind == "trim";

				return new ArchTile( parent, ArchThumb.For( captured.Category, captured.Name, captured.Id ),
					Shorten( captured.Name ), trim ? $"{captured.Category} - trim" : captured.Category, trim, grid.TileSize ) {
					ToolTip = $"{captured.Category}/{captured.Name}",
					Current = () => selected is not null && selected.Name == captured.Name && selected.Category == captured.Category,
					Clicked = () => Reassign( captured )
				};
			} );
		}

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

	bool Matches( Entry entry ) {
		if ( !string.IsNullOrEmpty( categoryFilter ) && entry.Category != categoryFilter ) {
			return false;
		}

		if ( string.IsNullOrWhiteSpace( query ) ) {
			return true;
		}

		return entry.Name.Contains( query, StringComparison.OrdinalIgnoreCase )
			|| entry.Category.Contains( query, StringComparison.OrdinalIgnoreCase );
	}

	static string Shorten( string name ) {
		if ( name.StartsWith( "map_seamless_", StringComparison.OrdinalIgnoreCase ) ) {
			return name.Substring( 13 );
		}

		if ( name.StartsWith( "map_trim_", StringComparison.OrdinalIgnoreCase ) ) {
			return name.Substring( 9 );
		}

		return name;
	}

	void Reassign( Entry entry ) {
		if ( selected is null ) {
			return;
		}

		var replacement = new ArchMapPalette.Surface {
			Role = selected.Role,
			Kind = entry.Kind,
			Id = entry.Id,
			Name = entry.Name,
			Category = entry.Category,
			Use = selected.Use
		};

		if ( !ArchMapPalette.Reassign( document.File, selected.Role, replacement ) ) {
			return;
		}

		selected.Kind = replacement.Kind;
		selected.Id = replacement.Id;
		selected.Name = replacement.Name;
		selected.Category = replacement.Category;

		ArchSlotPreview.Invalidate();

		ShowRoles();
		ShowSelected();
		ShowLibrary();

		changed?.Invoke();
	}
}