Editor/Output/ArchScene.cs
using System;
using System.Collections.Generic;
using System.Linq;
using MapDoor = Sandbox.Mapping.Door;
using Sandbox;

namespace Sunless.Architecture;

public static class ArchScene {
	public const string RootName = "Generated Architecture";
	public const string GeneratedTag = "arch_generated";

	public const string LeafName = "Leaf";

	public static GameObject FindRoot( Scene scene ) {
		if ( scene is null ) {
			return null;
		}

		return scene.Children.FirstOrDefault( child => child.Name == RootName );
	}

	public static List<GameObject> FindRoots( Scene scene ) {
		return scene?.Children.Where( child => child.Name == RootName ).ToList() ?? new List<GameObject>();
	}

	// Locked layers are preserved — their hand-edited faces must survive a cold rebuild.
	public static void Clear( Scene scene, ArchLayerTree layers ) {
		foreach ( var root in FindRoots( scene ) ) {
			if ( layers is null || !layers.AnyFrozen() ) {
				root.DestroyImmediate();

				continue;
			}

			Strip( root, layers );
		}
	}

	static void Strip( GameObject node, ArchLayerTree layers ) {
		foreach ( var child in node.Children.ToList() ) {
			if ( Frozen( child, layers ) ) {
				continue;
			}

			if ( Holds( child, layers ) ) {
				Strip( child, layers );

				continue;
			}

			child.DestroyImmediate();
		}
	}

	static bool Frozen( GameObject node, ArchLayerTree layers ) {
		return ArchNames.TrySourceId( node.Name, out var id ) && layers.Frozen( id );
	}

	static bool Holds( GameObject node, ArchLayerTree layers ) {
		return Descendants( node ).Any( child => Frozen( child, layers ) );
	}

	public static GameObject Generate( Scene scene, ArchPlan plan, ArchKit kit, ArchBuildContext context = null,
		ArchLayerTree layers = null, ArchBuildCache cache = null, ArchDoorFitters fitters = null,
		IReadOnlySet<int> rebuild = null ) {
		if ( scene is null || plan is null ) {
			return null;
		}

		fitters ??= ArchDoorFitters.Load();

		if ( cache is null ) {
			Clear( scene, layers );
		}

		var root = FindRoot( scene );

		if ( !root.IsValid() ) {
			root = scene.CreateObject();
			root.Name = RootName;
			root.Tags.Add( GeneratedTag );
		}

		ArchBuildGate gate;
		ArchArchitectureBuild build;

		using ( ArchBuildProfile.Stage( "Gate" ) ) {
			gate = cache?.Gate( plan, kit, ArchTerrain.HeightStamped, rebuild );
		}

		using ( ArchBuildProfile.Stage( "Generate" ) ) {
			build = Built( scene, plan, kit, context, layers, gate );
		}

		using ( ArchBuildProfile.Stage( "Settings" ) ) {
			foreach ( var part in build.Parts ) {
				part.Resolved = ArchCollision.Resolve( part, kit, layers );
				part.ResolvedShadows = ArchShadows.Resolve( part, plan, kit, layers );
			}
		}

		ArchBuildSettlement settled;

		using ( ArchBuildProfile.Stage( "Contact" ) ) {
			settled = ArchBuildCache.Settle( build, cache, gate );
		}

		if ( settled.Escaped ) {
			Clear( scene, layers );
			cache.Restart();

			return Generate( scene, plan, kit, context, layers, cache, fitters );
		}

		using ( ArchBuildProfile.Stage( "Rename" ) ) {
			Rename( root, build );
		}

		Dictionary<string, GameObject> nodes;
		bool vanished;

		using ( ArchBuildProfile.Stage( "Index" ) ) {
			nodes = Indexed( root );
			vanished = Vanished( nodes, build, settled );
		}

		if ( vanished ) {
			Clear( scene, layers );
			cache.Restart();

			return Generate( scene, plan, kit, context, layers, cache, fitters );
		}

		var live = Living( settled.Kept );

		var unresolved = new HashSet<string>( StringComparer.OrdinalIgnoreCase );

		using ( ArchBuildProfile.Stage( "Write" ) ) {
			for ( var index = 0; index < build.Parts.Count; index++ ) {
				var part = build.Parts[index];
				var node = EnsurePath( root, nodes, part.Path, live );

				if ( layers?.Frozen( part.Owner ) == true ) {
					Claim( live, part );
					continue;
				}

				if ( !settled.Write.Contains( index ) ) {
					if ( settled.Dressed.Contains( index ) ) {
						Dress( node, part );
					}

					Claim( live, part );
					continue;
				}

				if ( node.LocalTransform != part.NodeTransform ) {
					node.LocalTransform = part.NodeTransform;
				}

				Apply( node, part.Canvas, part.Resolved, part.ResolvedShadows );

				foreach ( var request in part.Doors ) {
					BuildDoor( node, request, fitters, unresolved );
				}

				foreach ( var request in part.Glass ) {
					var pane = Child( node, ArchNames.Glass( request.Opening ) );
					pane.Tags.Add( "glass" );

					Apply( pane, request.Canvas, ArchCollisionMode.Convex, part.ResolvedShadows );
				}

				Claim( live, part );
			}

			foreach ( var rootTransform in build.RootTransforms ) {
				var node = EnsurePath( root, nodes, rootTransform.Key, live );

				if ( node.LocalTransform != rootTransform.Value ) {
					node.LocalTransform = rootTransform.Value;
				}
			}
		}

		using ( ArchBuildProfile.Stage( "Fittings" ) ) {
			Hang( scene, nodes, build, settled, layers, unresolved );
		}

		using ( ArchBuildProfile.Stage( "Prune" ) ) {
			foreach ( var child in root.Children.ToList() ) {
				Prune( child, child.Name, live, settled.Kept );
			}
		}

		using ( ArchBuildProfile.Stage( "Interiors" ) ) {
			ArchCull.Lined( root, plan, kit, cache );
		}

		foreach ( var extension in ArchAddons.Load().Contributed<IArchSceneAddon>() ) {
			extension.Apply( scene, root, plan, kit, layers );
		}

		return root;
	}

	static ArchArchitectureBuild Built( Scene scene, ArchPlan plan, ArchKit kit, ArchBuildContext context,
		ArchLayerTree layers, ArchBuildGate gate ) {
		using ( ArchLayerGate.Begin( layers ) ) {
			var builder = new ArchArchitectureBuilder( plan, kit, scene, context ).Through( gate ).WithConnections();

			using ( ArchBuildMemo.Begin( plan ) ) {
				return builder.WithBuildings().WithRoads().WithUnits().Create();
			}
		}
	}

	static void Hang( Scene scene, Dictionary<string, GameObject> nodes, ArchArchitectureBuild build,
		ArchBuildSettlement settled, ArchLayerTree layers, HashSet<string> unresolved ) {
		for ( var index = 0; index < build.Parts.Count; index++ ) {
			var part = build.Parts[index];

			if ( part.Fittings.Count == 0 || !settled.Write.Contains( index ) || layers?.Frozen( part.Owner ) == true ) {
				continue;
			}

			if ( !nodes.TryGetValue( part.Path, out var node ) || !node.IsValid() ) {
				continue;
			}

			foreach ( var request in part.Fittings ) {
				Stand( scene, node, request, unresolved );
			}
		}
	}

	static void Stand( Scene scene, GameObject parent, ArchFittingRequest request, HashSet<string> unresolved ) {
		var file = PrefabFile.Load( request.Prefab );
		var source = file is null ? null : SceneUtility.GetPrefabScene( file );

		if ( source is null ) {
			if ( unresolved.Add( request.Prefab ?? "" ) ) {
				Log.Warning( $"Architecture: no prefab at '{request.Prefab}' - nothing hung on {parent.Name}/{request.Name}." );
			}

			return;
		}

		var local = request.Standing( source.GetBounds() );
		var standing = parent.Children.FirstOrDefault( child => child.Name == request.Name );

		if ( standing.IsValid() && Same( standing.PrefabInstanceSource, file.ResourcePath ) ) {
			if ( standing.LocalTransform != local ) {
				standing.LocalTransform = local;
			}

			Finished( standing, request.MaterialGroup );

			return;
		}

		standing?.DestroyImmediate();

		using ( scene.Push() ) {
			var clone = source.Clone( local, parent, true, request.Name );

			clone.LocalTransform = local;

			Finished( clone, request.MaterialGroup );
		}
	}

	static void Finished( GameObject standing, string group ) {
		if ( string.IsNullOrWhiteSpace( group ) || !standing.IsValid() ) {
			return;
		}

		foreach ( var renderer in standing.Components.GetAll<ModelRenderer>( FindMode.EverythingInSelfAndDescendants ) ) {
			if ( renderer.MaterialGroup != group ) {
				renderer.MaterialGroup = group;
			}
		}
	}

	static bool Same( string one, string other ) {
		return string.Equals( (one ?? "").TrimStart( '/' ), (other ?? "").TrimStart( '/' ), StringComparison.OrdinalIgnoreCase );
	}

	// Renames standing nodes to match current layer names so the cache still hits.
	static void Rename( GameObject root, ArchArchitectureBuild build ) {
		var wanted = new Dictionary<int, string>();

		foreach ( var path in build.Parts.Select( part => part.Path ).Concat( build.RootTransforms.Keys ) ) {
			foreach ( var segment in (path ?? "").Split( '/', StringSplitOptions.RemoveEmptyEntries ) ) {
				if ( ArchNames.TrySourceId( segment, out var id ) ) {
					wanted[id] = segment;
				}
			}
		}

		if ( wanted.Count == 0 ) {
			return;
		}

		foreach ( var node in Generated( root ) ) {
			if ( ArchNames.TrySourceId( node.Name, out var id )
				&& wanted.TryGetValue( id, out var name )
				&& node.Name != name ) {
				node.Name = name;
			}
		}
	}

	static IEnumerable<GameObject> Generated( GameObject root ) {
		if ( !root.IsValid() || root.IsDestroyed ) {
			yield break;
		}

		yield return root;

		if ( root.IsPrefabInstanceRoot ) {
			yield break;
		}

		foreach ( var child in root.Children ) {
			foreach ( var nested in Generated( child ) ) {
				yield return nested;
			}
		}
	}

	static Dictionary<string, GameObject> Indexed( GameObject root ) {
		var nodes = new Dictionary<string, GameObject>();

		void Walk( GameObject node, string path ) {
			nodes[path] = node;

			if ( node.IsPrefabInstanceRoot ) {
				return;
			}

			foreach ( var child in node.Children ) {
				Walk( child, path.Length == 0 ? child.Name : $"{path}/{child.Name}" );
			}
		}

		foreach ( var child in root.Children ) {
			Walk( child, child.Name );
		}

		return nodes;
	}

	static HashSet<string> Living( IReadOnlySet<string> kept ) {
		var live = new HashSet<string>();

		foreach ( var path in kept ) {
			var walked = "";

			foreach ( var name in path.Split( '/', StringSplitOptions.RemoveEmptyEntries ) ) {
				walked = walked.Length == 0 ? name : $"{walked}/{name}";

				live.Add( walked );
			}
		}

		return live;
	}

	static bool Vanished( Dictionary<string, GameObject> nodes, ArchArchitectureBuild build, ArchBuildSettlement settled ) {
		for ( var index = 0; index < build.Parts.Count; index++ ) {
			if ( !settled.Write.Contains( index ) && !Standing( nodes, build.Parts[index].Path ) ) {
				return true;
			}
		}

		return settled.Kept.Any( path => !Standing( nodes, path ) );
	}

	static bool Standing( Dictionary<string, GameObject> nodes, string path ) {
		return nodes.TryGetValue( path, out var node ) && node.IsValid() && !node.IsDestroyed
			&& node.Components.Get<MeshComponent>( FindMode.EverythingInSelf ) is { Mesh: not null };
	}

	static void Claim( HashSet<string> live, ArchBuiltPart part ) {
		foreach ( var request in part.Doors ) {
			var door = $"{part.Path}/{ArchNames.Door( request.Opening )}";

			live.Add( door );

			if ( request.Hung ) {
				live.Add( $"{door}/{LeafName}" );
			}
		}

		foreach ( var request in part.Glass ) {
			live.Add( $"{part.Path}/{ArchNames.Glass( request.Opening )}" );
		}

		foreach ( var request in part.Fittings ) {
			live.Add( $"{part.Path}/{request.Name}" );
		}
	}

	static GameObject EnsurePath( GameObject root, Dictionary<string, GameObject> nodes, string path, HashSet<string> live ) {
		var node = root;
		var walked = "";

		foreach ( var name in path.Split( '/', StringSplitOptions.RemoveEmptyEntries ) ) {
			walked = walked.Length == 0 ? name : $"{walked}/{name}";

			live.Add( walked );

			if ( nodes.TryGetValue( walked, out var held ) && held.IsValid() && !held.IsDestroyed ) {
				node = held;
				continue;
			}

			node = nodes[walked] = Child( node, name );
		}

		return node;
	}

	static void Prune( GameObject node, string path, HashSet<string> live, IReadOnlySet<string> kept ) {
		if ( Spared( path, kept ) ) {
			return;
		}

		if ( !node.IsPrefabInstanceRoot ) {
			foreach ( var child in node.Children.ToList() ) {
				Prune( child, $"{path}/{child.Name}", live, kept );
			}
		}

		if ( !live.Contains( path ) ) {
			node.DestroyImmediate();
		}
	}

	// Interiors are ArchCull.Lined's; a kept part's doors, panes and fittings were never built this pass to claim.
	public static bool Spared( string path, IReadOnlySet<string> kept ) {
		return path[(path.LastIndexOf( '/' ) + 1)..] == ArchPieces.Interior || kept.Contains( path );
	}

	public static IEnumerable<GameObject> Descendants( GameObject root ) {
		if ( !root.IsValid() || root.IsDestroyed ) {
			yield break;
		}

		yield return root;

		foreach ( var child in root.Children ) {
			foreach ( var nested in Descendants( child ) ) {
				yield return nested;
			}
		}
	}

	public static void LinkDoublePairs( GameObject root ) {
		var doors = root.Components.GetAll<MapDoor>( FindMode.EverythingInSelfAndDescendants ).ToList();

		foreach ( var group in doors.GroupBy( door => door.GameObject.Name ) ) {
			var pair = group.ToList();

			if ( pair.Count != 2 ) {
				continue;
			}

			pair[0].LinkedDoor = pair[1];
			pair[1].LinkedDoor = pair[0];
		}
	}

	static void BuildDoor( GameObject parent, ArchDoorRequest request, ArchDoorFitters fitters, HashSet<string> unresolved ) {
		var node = Child( parent, ArchNames.Door( request.Opening ) );
		node.LocalTransform = new Transform( request.HingeLocal, request.Rotation );

		if ( request.Hung ) {
			HangLeaf( node, request, unresolved );
		} else {
			Extrude( node, request );
		}

		var door = node.Components.GetOrCreate<MapDoor>();
		door.Speed = request.Opening.Kind == OpeningKind.Garage ? 90f : 220f;
		door.OpenAwayFromPlayer = true;
		door.IsUsable = true;
		door.StartOpen = request.Opening.StartOpen;

		if ( request.Opening.Kind == OpeningKind.Garage ) {
			door.Mode = MapDoor.DoorMode.Sliding;
			door.SlideOffset = Vector3.Up * (request.LeafHeight - 2f);
		} else {
			door.Mode = MapDoor.DoorMode.Rotating;
			door.TargetAngle = request.Mirrored ? -95f : 95f;
		}

		fitters.Fit( node, door );
	}

	static void Extrude( GameObject node, ArchDoorRequest request ) {
		var canvas = new ArchMesh( node.LocalTransform );
		var half = request.LeafThickness * 0.5f;
		var width = request.Mirrored ? -request.LeafWidth : request.LeafWidth;

		canvas.Box(
			new Vector3( MathF.Min( 0f, width ), -half, 0f ),
			new Vector3( MathF.Max( 0f, width ), half, request.LeafHeight ),
			request.Brush );

		Apply( node, canvas, ArchCollisionMode.Convex );
	}

	static void HangLeaf( GameObject node, ArchDoorRequest request, HashSet<string> unresolved ) {
		if ( node.Components.Get<MeshComponent>( FindMode.EverythingInSelf ) is { } extruded ) {
			extruded.Destroy();
		}

		Stand( node.Scene, node, Leaf( request ), unresolved );
	}

	// Mirrored by rotation, not negative scale — negative scale inverts winding.
	static ArchFittingRequest Leaf( ArchDoorRequest request ) {
		return new ArchFittingRequest {
			Name = LeafName,
			Prefab = request.Art.Prefab,
			Mount = new Transform( Vector3.Zero, request.Mirrored ? Rotation.FromYaw( 180f ) : Rotation.Identity ),
			Space = new Vector3( MathF.Max( 1f, request.LeafWidth ), MathF.Max( 0.25f, request.LeafThickness ),
				MathF.Max( 1f, request.LeafHeight ) ),
			Fits = request.Art.Scales,
			MaterialGroup = request.MaterialGroup,
			Anchor = new ArchCabinetAnchor( request.Art.Anchor, request.Art.AnchorAlong ),
			Turn = request.Art.AnchorTurn
		};
	}

	static GameObject Child( GameObject parent, string name ) {
		if ( parent.Children.FirstOrDefault( child => child.Name == name ) is { } standing ) {
			return standing;
		}

		var created = parent.Scene.CreateObject();
		created.Name = name;
		created.SetParent( parent, false );
		created.Tags.Add( GeneratedTag );

		return created;
	}

	// Writes flags only — no mesh assignment, avoiding an expensive hull/physics rebuild.
	static void Dress( GameObject node, ArchBuiltPart part ) {
		if ( node.Components.Get<MeshComponent>( FindMode.EverythingInSelf ) is not { } renderer ) {
			return;
		}

		ArchCollision.Write( node, renderer, part.Canvas, part.Resolved );
		ArchShadows.Write( renderer, part.ResolvedShadows );

		foreach ( var request in part.Glass ) {
			var pane = node.Children.FirstOrDefault( child => child.Name == ArchNames.Glass( request.Opening ) );

			if ( pane?.Components.Get<MeshComponent>( FindMode.EverythingInSelf ) is { } glazing ) {
				ArchShadows.Write( glazing, part.ResolvedShadows );
			}
		}
	}

	static void Apply( GameObject node, ArchMesh canvas, ArchCollisionMode collision, bool shadows = true ) {
		if ( canvas is null || canvas.IsEmpty ) {
			return;
		}

		var renderer = node.Components.GetOrCreate<MeshComponent>();

		renderer.Color = Color.White;
		renderer.SmoothingAngle = 0f;

		ArchCollision.Write( node, renderer, canvas, collision );
		ArchShadows.Write( renderer, shadows );

		renderer.Mesh = canvas.Finish();
	}

}

public static class ArchNames {
	public static string Building( ArchBuilding building ) => $"Building_{building.Id}_{building.Name}";

	public static string Unit( ArchUnit unit ) {
		var ident = unit.Kind.Ident;

		return $"{char.ToUpperInvariant( ident[0] )}{ident[1..]}_{unit.Id}_{unit.Name}";
	}

	// Second field is the plan id — except Floor_ which is numbered by level.
	public static bool TrySourceId( string name, out int id ) {
		id = 0;

		if ( name is null || name.StartsWith( "Floor_", StringComparison.Ordinal ) ) {
			return false;
		}

		var first = name.IndexOf( '_' );

		if ( first < 0 ) {
			return false;
		}

		var second = name.IndexOf( '_', first + 1 );
		var field = second < 0 ? name[(first + 1)..] : name[(first + 1)..second];

		return int.TryParse( field, out id );
	}

	public static string Room( ArchRoom room ) => $"Room_{room.Id}_{room.Name}";

	public static string Floor( ArchFloorGroup group ) => $"Floor_{group.Level}_{group.Lead.Name}";

	public static string Wall( ArchWall wall ) => $"Wall_{wall.Id}";

	public static string Door( ArchOpening opening ) => $"Door_{opening.Id}";

	public static string Glass( ArchOpening opening ) => $"Glass_{opening.Id}";

	public static string Roof( ArchRoofPart roof ) => $"Roof_{roof.Id}_{roof.Name}";

	public static string Stair( ArchStairPart stair ) => $"Stair_{stair.Id}_{stair.Name}";

	public static string Trim( ArchTrimPart trim ) => $"Trim_{trim.Id}_{trim.Name}";

	public static string Pillar( ArchPillarPart pillar ) => $"Pillar_{pillar.Id}_{pillar.Name}";

	public static string Span( ArchSpanPart span ) => $"Span_{span.Id}_{span.Name}";

	public static string Beam( ArchBeamPart beam ) => $"Beam_{beam.Id}_{beam.Name}";

	public static string Cabinets( ArchCabinetPart part ) => $"Cabinets_{part.Id}_{part.Name}";

	// Second field is a word, not a number — TrySourceId would misread it as a layer id.
	public static string Fitting( int cell, string mount ) => $"Fitting_{mount.Replace( " ", "" )}_{cell}";

	public static string Downpipe( ArchDownpipePart pipe ) => $"Downpipe_{pipe.Id}_{pipe.Name}";

	public static string Pipe( ArchPipePart run ) => $"PipeRun_{run.Id}_{run.Name}";

	public static string Bracket( ArchPipeBracketPart bracket ) => $"Brackets_{bracket.Id}_{bracket.Name}";

	public static string Ladder( ArchLadderPart ladder ) => $"Ladder_{ladder.Id}_{ladder.Name}";

	public static string Balcony( ArchBalconyPart balcony ) => $"Balcony_{balcony.Id}_{balcony.Name}";

	public static string ExteriorStair( ArchExteriorStairPart flight ) => $"Escape_{flight.Id}_{flight.Name}";


	public static string Porch( ArchPorchPart porch ) => $"Porch_{porch.Id}_{porch.Name}";

	public static string Fence( ArchFencePart fence ) => $"Fence_{fence.Id}_{fence.Name}";

	public static string Platform( ArchPlatformPart platform ) => $"Platform_{platform.Id}_{platform.Name}";

	public static string Cut( ArchCutPart cut ) => $"Cut_{cut.Id}_{cut.Name}";

	public static string Approach( ArchApproachPart approach ) => $"Approach_{approach.Id}_{approach.Name}";

	public static string Road( ArchRoadPart road ) => $"Road_{road.Id}_{road.Name}";

	public static string Bridge( ArchBridgePart bridge ) => $"Bridge_{bridge.Id}_{bridge.Name}";

	public static string Tunnel( ArchTunnelPart tunnel ) => $"Tunnel_{tunnel.Id}_{tunnel.Name}";

	// Strips the user-facing name suffix so cache keys survive renames.
	public static string Stable( string path ) {
		if ( string.IsNullOrEmpty( path ) ) {
			return path;
		}

		var segments = path.Split( '/', StringSplitOptions.RemoveEmptyEntries );

		for ( var index = 0; index < segments.Length; index++ ) {
			segments[index] = Trimmed( segments[index] );
		}

		return string.Join( '/', segments );
	}

	static string Trimmed( string name ) {
		var first = name.IndexOf( '_' );

		if ( first < 0 ) {
			return name;
		}

		var second = name.IndexOf( '_', first + 1 );

		return second > 0 && int.TryParse( name[(first + 1)..second], out _ ) ? name[..second] : name;
	}

	public static bool TryParseId( string name, string prefix, out int id ) {
		id = 0;

		if ( string.IsNullOrEmpty( name ) || !name.StartsWith( prefix + "_", StringComparison.Ordinal ) ) {
			return false;
		}

		var rest = name[(prefix.Length + 1)..];
		var end = rest.IndexOf( '_' );
		var token = end < 0 ? rest : rest[..end];

		return int.TryParse( token, out id );
	}
}