Editor/Data/ArchStyle.cs
using System.Collections.Generic;
using Sandbox;

namespace Sunless.Architecture;

public sealed class ArchStyle {
	static readonly Dictionary<string, Material> Cache = new( System.StringComparer.OrdinalIgnoreCase );

	readonly ArchKit kit;

	public ArchStyle( ArchKit kit ) {
		this.kit = kit;
	}

	public static void InvalidateCache() => Cache.Clear();

	// Glass can't come from a scan library, so it has a built-in.
	const string GlassMaterial = "environment/materials/blockout/glass.vmat";

	// One resolver, and the short chains get their own overloads so the common ask allocates no array at all -
	// a brush is looked up per face group of every part of every build.
	public ArchBrush Brush( ArchSurface surface ) => Resolved( surface, null, null, null );

	public ArchBrush Brush( ArchSurface surface, ArchPalette one ) => Resolved( surface, one, null, null );

	public ArchBrush Brush( ArchSurface surface, ArchPalette one, ArchPalette two ) => Resolved( surface, one, two, null );

	public ArchBrush Brush( ArchSurface surface, params ArchPalette[] chain ) => Resolved( surface, null, null, chain );

	ArchBrush Resolved( ArchSurface surface, ArchPalette one, ArchPalette two, ArchPalette[] chain ) {
		foreach ( var role in Fallbacks( surface ) ) {
			if ( Taken( one, role, out var first ) ) {
				return first;
			}

			if ( Taken( two, role, out var second ) ) {
				return second;
			}

			if ( chain is not null ) {
				foreach ( var palette in chain ) {
					if ( Taken( palette, role, out var found ) ) {
						return found;
					}
				}
			}

			if ( Taken( kit?.Palette, role, out var fallback ) ) {
				return fallback;
			}
		}

		if ( surface == ArchSurface.Glass ) {
			return new ArchBrush { Material = Load( GlassMaterial ) };
		}

		// Nothing painted this role, so the blockout stands in for it - down the same fallback chain, because a
		// role the grid has no hue for still reads through the one it borrows from.
		foreach ( var role in Fallbacks( surface ) ) {
			if ( ArchBlockout.Paints( role ) ) {
				return ArchBlockout.Brush( role );
			}
		}

		return new ArchBrush { Material = ArchBrush.Missing };
	}

	bool Taken( ArchPalette palette, ArchSurface role, out ArchBrush brush ) {
		brush = default;

		if ( palette is null || !palette.TryGet( role, out var path ) ) {
			return false;
		}

		brush = new ArchBrush {
			Material = Load( path ),
			TexelScale = palette.ScaleFor( role ),
			Shift = palette.OffsetFor( role )
		};

		return true;
	}

	public static Material Load( string path ) {
		if ( string.IsNullOrWhiteSpace( path ) ) {
			return ArchBrush.Missing;
		}

		var normalized = Normalize( path );

		if ( Cache.TryGetValue( normalized, out var cached ) ) {
			return cached;
		}

		var material = Material.Load( normalized ) ?? ArchBrush.Missing;
		Cache[normalized] = material;

		return material;
	}

	static string Normalize( string path ) {
		path = path.Trim().Replace( '\\', '/' ).TrimStart( '/' );

		if ( path.StartsWith( "assets/", System.StringComparison.OrdinalIgnoreCase ) ) {
			path = path[7..];
		}

		return path.EndsWith( ".vmat_c", System.StringComparison.OrdinalIgnoreCase ) ? path[..^2] : path;
	}

	// Built once. Handing back a fresh array per lookup allocated one per face group of every part.
	static readonly Dictionary<ArchSurface, ArchSurface[]> chains = Chains();

	// Topped up on a miss rather than indexed. ArchSurface is append-only and hotload carries this table over, so
	// a role added in the same edit is absent from it until the editor restarts - and an absent role threw where
	// it should simply have skinned itself.
	static ArchSurface[] Fallbacks( ArchSurface surface ) {
		if ( chains.TryGetValue( surface, out var chain ) ) {
			return chain;
		}

		return chains[surface] = Chain( surface );
	}

	static Dictionary<ArchSurface, ArchSurface[]> Chains() {
		var built = new Dictionary<ArchSurface, ArchSurface[]>();

		foreach ( var surface in System.Enum.GetValues<ArchSurface>() ) {
			built[surface] = Chain( surface );
		}

		return built;
	}

	static ArchSurface[] Chain( ArchSurface surface ) {
		return surface switch {
			ArchSurface.WallInterior => new[] { ArchSurface.WallInterior, ArchSurface.WallExterior },
			ArchSurface.Reveal => new[] { ArchSurface.Reveal, ArchSurface.WallInterior, ArchSurface.WallExterior },
			ArchSurface.WallCap => new[] { ArchSurface.WallCap, ArchSurface.Trim, ArchSurface.WallExterior },
			ArchSurface.Sill => new[] { ArchSurface.Sill, ArchSurface.Trim },
			ArchSurface.Threshold => new[] { ArchSurface.Threshold, ArchSurface.Sill, ArchSurface.Trim },
			ArchSurface.Baseboard => new[] { ArchSurface.Baseboard, ArchSurface.Trim },
			ArchSurface.Foundation => new[] { ArchSurface.Foundation, ArchSurface.Pillar, ArchSurface.WallExterior },
			ArchSurface.Gutter => new[] { ArchSurface.Gutter, ArchSurface.Trim },
			ArchSurface.RoofEdge => new[] { ArchSurface.RoofEdge, ArchSurface.Trim, ArchSurface.Roof },
			ArchSurface.RoofFrame => new[] { ArchSurface.RoofFrame, ArchSurface.Railing, ArchSurface.Trim },
			ArchSurface.Soffit => new[] { ArchSurface.Soffit, ArchSurface.RoofEdge, ArchSurface.Trim },
			ArchSurface.Ceiling => new[] { ArchSurface.Ceiling, ArchSurface.Floor },
			ArchSurface.PillarCap => new[] { ArchSurface.PillarCap, ArchSurface.Trim, ArchSurface.Pillar },
			ArchSurface.Pillar => new[] { ArchSurface.Pillar, ArchSurface.WallExterior },
			ArchSurface.StairRiser => new[] { ArchSurface.StairRiser, ArchSurface.StairTread, ArchSurface.Floor },
			ArchSurface.StairStringer => new[] { ArchSurface.StairStringer, ArchSurface.StairTread, ArchSurface.Floor },
			ArchSurface.StairTread => new[] { ArchSurface.StairTread, ArchSurface.Floor },
			ArchSurface.DoorLeaf => new[] { ArchSurface.DoorLeaf, ArchSurface.Trim },
			ArchSurface.Siding => new[] { ArchSurface.Siding, ArchSurface.WallExterior },
			ArchSurface.Wainscot => new[] { ArchSurface.Wainscot, ArchSurface.Trim, ArchSurface.WallInterior },
			ArchSurface.WindowFrame => new[] { ArchSurface.WindowFrame, ArchSurface.Trim },
			ArchSurface.WindowSash => new[] { ArchSurface.WindowSash, ArchSurface.WindowFrame, ArchSurface.Trim },
			ArchSurface.Deck => new[] { ArchSurface.Deck, ArchSurface.StairTread, ArchSurface.Floor },
			ArchSurface.Railing => new[] { ArchSurface.Railing, ArchSurface.WindowFrame, ArchSurface.Trim },
			ArchSurface.Road => new[] { ArchSurface.Road, ArchSurface.Floor },
			ArchSurface.Pavement => new[] { ArchSurface.Pavement, ArchSurface.Deck, ArchSurface.Road, ArchSurface.Floor },
			ArchSurface.Kerb => new[] { ArchSurface.Kerb, ArchSurface.Foundation, ArchSurface.Trim },
			ArchSurface.RoadLine => new[] { ArchSurface.RoadLine, ArchSurface.Trim },
			ArchSurface.Panel => new[] { ArchSurface.Panel, ArchSurface.Foundation, ArchSurface.WallExterior },
			ArchSurface.Post => new[] { ArchSurface.Post, ArchSurface.Pillar, ArchSurface.Foundation, ArchSurface.WallExterior },
			ArchSurface.Wire => new[] { ArchSurface.Wire, ArchSurface.Gutter, ArchSurface.Trim },
			ArchSurface.Grille => new[] { ArchSurface.Grille, ArchSurface.Railing, ArchSurface.WindowFrame, ArchSurface.Trim },
			ArchSurface.Shutter => new[] { ArchSurface.Shutter, ArchSurface.DoorLeaf, ArchSurface.Panel, ArchSurface.Trim },
			ArchSurface.Sign => new[] { ArchSurface.Sign, ArchSurface.Panel, ArchSurface.Trim },
			ArchSurface.Plant => new[] { ArchSurface.Plant, ArchSurface.Gutter, ArchSurface.Foundation, ArchSurface.WallExterior },
			ArchSurface.Pipework => new[] { ArchSurface.Pipework, ArchSurface.Plant, ArchSurface.Gutter, ArchSurface.Trim },
			ArchSurface.Cabling => new[] { ArchSurface.Cabling, ArchSurface.Wire, ArchSurface.Plant, ArchSurface.Gutter, ArchSurface.Trim },
			ArchSurface.Bracket => new[] { ArchSurface.Bracket, ArchSurface.Post, ArchSurface.Plant, ArchSurface.Gutter, ArchSurface.Trim },
			ArchSurface.Bridge => new[] { ArchSurface.Bridge, ArchSurface.Foundation, ArchSurface.Pillar, ArchSurface.WallExterior },
			ArchSurface.Pier => new[] { ArchSurface.Pier, ArchSurface.Bridge, ArchSurface.Foundation, ArchSurface.Pillar },
			ArchSurface.Lining => new[] { ArchSurface.Lining, ArchSurface.Bridge, ArchSurface.Foundation, ArchSurface.WallInterior },
			ArchSurface.Portal => new[] { ArchSurface.Portal, ArchSurface.Lining, ArchSurface.Bridge, ArchSurface.Foundation },
			ArchSurface.Awning => new[] { ArchSurface.Awning, ArchSurface.Shutter, ArchSurface.Panel, ArchSurface.Trim },
			_ => new[] { surface }
		};
	}
}