Editor/Layers/ArchLayerTree.cs
using System;
using System.Collections.Generic;
using System.Linq;

namespace Sunless.Architecture;

public sealed class ArchLayerNode {
	public ArchLayerRef? Ref { get; init; }
	public ArchKind Kind { get; init; }
	public ArchLayerStage Stage { get; init; }
	public ArchLayerDomain Domain { get; init; }
	public object Payload { get; init; }
	public string Name { get; init; } = "";
	public bool Enabled { get; init; } = true;
	public bool Locked { get; init; }
	public int Order { get; init; }
	public int Floor { get; init; } = int.MinValue;
	public ArchLayerNode Parent { get; internal set; }
	public List<ArchLayerNode> Children { get; } = new();
	public ArchBuilding Building { get; init; }
	public ArchRoom Room { get; init; }

	public bool Virtual => Payload is null;

	public string DisplayName => ArchLayerNames.DisplayName( this );
}

public sealed class ArchLayerDomainGroup {
	public ArchLayerDomain Domain { get; init; }
	public string Name { get; init; } = "";
	public List<ArchLayerNode> Children { get; } = new();
}

public sealed class ArchLayerTree {
	public List<ArchLayerDomainGroup> Domains { get; } = new();

	readonly Dictionary<int, ArchLayerNode> byId = new();
	readonly Dictionary<object, ArchLayerNode> byPayload = new();
	readonly Dictionary<(int Building, int Floor), ArchLayerNode> stories = new();

	public IReadOnlyList<ArchLayerLink> Links { get; private set; } = Array.Empty<ArchLayerLink>();

	public ArchLayerNode Find( int id ) => byId.TryGetValue( id, out var node ) ? node : null;

	public ArchLayerNode Find( object payload ) {
		return payload is null ? null : byPayload.TryGetValue( payload, out var node ) ? node : null;
	}

	public object Resolve( ArchLayerRef layer ) => byId.TryGetValue( layer.ItemId, out var node ) ? node.Payload : null;

	public string Breadcrumb( ArchLayerNode node ) {
		if ( node is null ) {
			return "";
		}

		var parts = new List<string>();

		for ( var current = node; current is not null; current = current.Parent ) {
			parts.Add( current.DisplayName );
		}

		parts.Reverse();

		return string.Join( " / ", parts );
	}

	public object StableKey( ArchLayerNode node ) {
		return node.Payload ?? $"story:{node.Building?.Id}:{node.Floor}";
	}

	public ArchLayerRecord Record( ArchPlan plan, ArchLayerNode node ) {
		if ( node?.Ref is not { } layer ) {
			return null;
		}

		var record = plan.Layers.FirstOrDefault( entry => entry.ItemId == layer.ItemId );

		if ( record is null ) {
			record = new ArchLayerRecord {
				ItemId = layer.ItemId,
				ParentId = layer.ParentId,
				Kind = layer.Kind,
				Stage = node.Stage,
				Order = node.Order,
				Enabled = node.Enabled,
				Locked = node.Locked,
			};

			plan.Layers.Add( record );
		}

		return record;
	}

	public bool Reorder( ArchPlan plan, ArchLayerRef moving, int anchorId, bool below ) {
		if ( !byId.TryGetValue( moving.ItemId, out var node ) || !byId.TryGetValue( anchorId, out var anchor ) ) {
			return false;
		}

		if ( ReferenceEquals( node, anchor ) || !ReferenceEquals( node.Parent, anchor.Parent ) ) {
			return false;
		}

		var siblings = (node.Parent?.Children ?? Domains.FirstOrDefault( domain => domain.Domain == node.Domain )?.Children)
			?.Where( child => child.Ref is not null )
			.ToList();

		if ( siblings is null ) {
			return false;
		}

		siblings.Remove( node );

		var at = siblings.IndexOf( anchor );

		if ( at < 0 ) {
			return false;
		}

		siblings.Insert( below ? at + 1 : at, node );

		for ( var index = 0; index < siblings.Count; index++ ) {
			Record( plan, siblings[index] ).Order = index;
		}

		return true;
	}

	public bool Reparent( ArchPlan plan, ArchLayerRef layer, int newParentId ) {
		if ( !byId.TryGetValue( layer.ItemId, out var node ) || node.Payload is null ) {
			return false;
		}

		if ( newParentId == 0 ) {
			ArchLayerGroups.Leave( plan, layer.ItemId );

			return true;
		}

		if ( layer.ItemId == newParentId || !byId.TryGetValue( newParentId, out var parentNode ) || parentNode.Payload is null ) {
			return false;
		}

		if ( parentNode.Payload is ArchSiteAssembly group ) {
			ArchLayerGroups.Join( plan, group, layer.ItemId );

			return true;
		}

		if ( !ArchLayerRules.CanParent( parentNode.Kind, layer.Kind ).Allowed ) {
			return false;
		}

		ArchLayerGroups.Leave( plan, layer.ItemId );

		if ( !MovePayload( plan, node.Payload, parentNode.Payload ) ) {
			return false;
		}

		var record = plan.Layers.FirstOrDefault( entry => entry.ItemId == layer.ItemId );

		if ( record is null ) {
			record = new ArchLayerRecord { ItemId = layer.ItemId, ParentId = newParentId, Kind = layer.Kind, Stage = node.Stage };
			plan.Layers.Add( record );
		}

		record.ParentId = newParentId;

		return true;
	}

	static bool Unfile( ArchPlan plan, ArchWall wall ) {
		foreach ( var room in plan.AllRooms() ) {
			if ( room.Walls.Remove( wall ) ) {
				return true;
			}
		}

		foreach ( var roof in plan.AllRoofs() ) {
			if ( roof.Walls.Remove( wall ) ) {
				return true;
			}
		}

		return false;
	}

	static bool Unfile( ArchPlan plan, ArchStairPart stair ) {
		foreach ( var room in plan.AllRooms() ) {
			if ( room.Stairs.Remove( stair ) || room.Porches.Any( porch => porch.Stairs.Remove( stair ) ) ) {
				return true;
			}
		}

		return plan.Buildings.SelectMany( building => building.Platforms ).Any( platform => platform.Stairs.Remove( stair ) );
	}

	static bool Unfile( ArchPlan plan, ArchPillarPart pillar ) {
		return plan.AllRooms().Any( room => room.Pillars.Remove( pillar ) || room.Porches.Any( porch => porch.Pillars.Remove( pillar ) ) );
	}

	static bool Unfile( ArchPlan plan, ArchTrimPart trim ) {
		return plan.AllRooms().Any( room => room.Trims.Remove( trim ) || room.Porches.Any( porch => porch.Trims.Remove( trim ) ) );
	}

	static bool MovePayload( ArchPlan plan, object payload, object newParent ) {
		switch (payload, newParent) {
			case (ArchRoom room, ArchBuilding building ):
				if ( plan.OwnerOf( room ) is not { } fromRoom ) return false;
				fromRoom.Rooms.Remove( room );
				building.Rooms.Add( room );
				return true;

			case (ArchWall wall, ArchRoom room ):
				if ( !Unfile( plan, wall ) ) return false;
				room.Walls.Add( wall );
				return true;

			case (ArchWall wall, ArchRoofPart roof ):
				if ( !Unfile( plan, wall ) ) return false;
				roof.Walls.Add( wall );
				return true;

			case (ArchOpening opening, ArchWall wall ):
				if ( plan.AllWalls().FirstOrDefault( candidate => candidate.Openings.Contains( opening ) ) is not { } fromOpening ) return false;
				fromOpening.Openings.Remove( opening );
				wall.Openings.Add( opening );
				return true;

			case (ArchWallModPart modifier, ArchWall host ):
				if ( plan.AllWalls().FirstOrDefault( candidate => candidate.Modifiers.Contains( modifier ) ) is not { } fromModifier ) return false;
				fromModifier.Modifiers.Remove( modifier );
				host.Modifiers.Add( modifier );
				return true;

			case (ArchStairPart stair, ArchRoom room ):
				if ( !Unfile( plan, stair ) ) return false;
				room.Stairs.Add( stair );
				return true;

			case (ArchTrimPart trim, ArchRoom room ):
				if ( !Unfile( plan, trim ) ) return false;
				room.Trims.Add( trim );
				return true;

			case (ArchPillarPart pillar, ArchRoom room ):
				if ( !Unfile( plan, pillar ) ) return false;
				room.Pillars.Add( pillar );
				return true;

			case (ArchStairPart stair, ArchPorchPart porch ):
				if ( !Unfile( plan, stair ) ) return false;
				porch.Stairs.Add( stair );
				return true;

			case (ArchPillarPart pillar, ArchPorchPart porch ):
				if ( !Unfile( plan, pillar ) ) return false;
				porch.Pillars.Add( pillar );
				return true;

			case (ArchTrimPart trim, ArchPorchPart porch ):
				if ( !Unfile( plan, trim ) ) return false;
				porch.Trims.Add( trim );
				return true;

			case (ArchCutPart, ArchPorchPart ):
				return true;

			case (ArchBeamPart beam, ArchRoom room ):
				if ( plan.AllRooms().FirstOrDefault( candidate => candidate.Beams.Contains( beam ) ) is not { } fromBeam ) return false;
				fromBeam.Beams.Remove( beam );
				room.Beams.Add( beam );
				return true;

			case (ArchPorchPart porch, ArchRoom room ):
				if ( plan.AllRooms().FirstOrDefault( candidate => candidate.Porches.Contains( porch ) ) is not { } fromPorch ) return false;
				fromPorch.Porches.Remove( porch );
				room.Porches.Add( porch );
				return true;

			case (ArchApproachPart approach, ArchRoom room ):
				if ( plan.AllRooms().FirstOrDefault( candidate => candidate.Approaches.Contains( approach ) ) is not { } fromApproach ) return false;
				fromApproach.Approaches.Remove( approach );
				room.Approaches.Add( approach );
				return true;

			case (ArchRoofPart roof, ArchBuilding building ):
				if ( plan.Buildings.FirstOrDefault( candidate => candidate.Roofs.Contains( roof ) ) is not { } fromRoof ) return false;
				fromRoof.Roofs.Remove( roof );
				building.Roofs.Add( roof );
				return true;

			case (ArchRoofLightPart light, ArchRoofPart roof ):
				if ( plan.Buildings.SelectMany( candidate => candidate.Roofs ).FirstOrDefault( candidate => candidate.Lights.Contains( light ) ) is not { } fromLight ) return false;
				fromLight.Lights.Remove( light );
				roof.Lights.Add( light );
				return true;

			case (ArchPlatformPart platform, ArchBuilding building ):
				if ( plan.Buildings.FirstOrDefault( candidate => candidate.Platforms.Contains( platform ) ) is not { } fromPlatform ) return false;
				fromPlatform.Platforms.Remove( platform );
				building.Platforms.Add( platform );
				return true;

			case (ArchStairPart stair, ArchPlatformPart platform ):
				if ( !Unfile( plan, stair ) ) return false;
				platform.Stairs.Add( stair );
				return true;

			case (ArchDownpipePart pipe, ArchBuilding building ):
				if ( plan.Buildings.FirstOrDefault( candidate => candidate.Downpipes.Contains( pipe ) ) is not { } fromPipe ) return false;
				fromPipe.Downpipes.Remove( pipe );
				building.Downpipes.Add( pipe );
				return true;

			case (ArchPipePart run, ArchBuilding building ):
				if ( plan.Buildings.FirstOrDefault( candidate => candidate.Pipes.Contains( run ) ) is not { } fromRun ) return false;
				fromRun.Pipes.Remove( run );
				building.Pipes.Add( run );
				return true;

			case (ArchPipeBracketPart bracket, ArchBuilding building ):
				if ( plan.Buildings.FirstOrDefault( candidate => candidate.Brackets.Contains( bracket ) ) is not { } fromBracket ) return false;
				fromBracket.Brackets.Remove( bracket );
				building.Brackets.Add( bracket );
				return true;

			case (ArchFencePart fence, ArchBuilding building ):
				if ( plan.Buildings.FirstOrDefault( candidate => candidate.Fences.Contains( fence ) ) is not { } fromFence ) return false;
				fromFence.Fences.Remove( fence );
				building.Fences.Add( fence );
				return true;

			case (ArchLadderPart ladder, ArchBuilding building ):
				if ( plan.Buildings.FirstOrDefault( candidate => candidate.Ladders.Contains( ladder ) ) is not { } fromLadder ) return false;
				fromLadder.Ladders.Remove( ladder );
				building.Ladders.Add( ladder );
				return true;

			case (ArchBalconyPart balcony, ArchBuilding building ):
				if ( plan.Buildings.FirstOrDefault( candidate => candidate.Balconies.Contains( balcony ) ) is not { } fromBalcony ) return false;
				fromBalcony.Balconies.Remove( balcony );
				building.Balconies.Add( balcony );
				return true;

			case (ArchExteriorStairPart flight, ArchBuilding building ):
				if ( plan.Buildings.FirstOrDefault( candidate => candidate.ExteriorStairs.Contains( flight ) ) is not { } fromFlight ) return false;
				fromFlight.ExteriorStairs.Remove( flight );
				building.ExteriorStairs.Add( flight );
				return true;

			case (ArchCutPart cut, ArchBuilding building ):
				if ( plan.Buildings.FirstOrDefault( candidate => candidate.Cuts.Contains( cut ) ) is not { } fromCut ) return false;
				fromCut.Cuts.Remove( cut );
				building.Cuts.Add( cut );
				return true;

			case (ArchBridgePart bridge, ArchRoadPart road ):
				if ( plan.Roads().FirstOrDefault( candidate => candidate.Bridges.Contains( bridge ) ) is not { } fromBridge ) return false;
				fromBridge.Bridges.Remove( bridge );
				road.Bridges.Add( bridge );
				return true;

			case (ArchTunnelPart tunnel, ArchRoadPart road ):
				if ( plan.Roads().FirstOrDefault( candidate => candidate.Tunnels.Contains( tunnel ) ) is not { } fromTunnel ) return false;
				fromTunnel.Tunnels.Remove( tunnel );
				road.Tunnels.Add( tunnel );
				return true;

			default:
				return false;
		}
	}

	public bool IsEnabled( object payload ) {
		return payload is null || byPayload.TryGetValue( payload, out var node ) && node.Enabled;
	}

	public bool IsEnabled( int id ) {
		return byId.TryGetValue( id, out var node ) && node.Enabled;
	}

	public bool IsLocked( int id ) {
		return byId.TryGetValue( id, out var node ) && node.Locked;
	}

	public bool IsLocked( object payload ) {
		return payload is not null && byPayload.TryGetValue( payload, out var node ) && node.Locked;
	}

	// Walks up — locking a parent freezes all children
	public bool Frozen( int itemId ) {
		if ( itemId == 0 || !byId.TryGetValue( itemId, out var found ) ) {
			return false;
		}

		for ( var node = found; node is not null; node = node.Parent ) {
			if ( node.Locked ) {
				return true;
			}
		}

		return false;
	}

	public bool AnyFrozen() => byId.Values.Any( node => node.Locked );

	public IReadOnlyList<string> Problems( ArchLayerNode node ) {
		var problems = new List<string>();

		if ( node?.Ref is not { } nodeRef ) {
			return problems;
		}

		foreach ( var link in Links.Where( link => link.Required && link.SourceId == nodeRef.ItemId ) ) {
			if ( !byId.ContainsKey( link.TargetId ) ) {
				problems.Add( $"Required link {link.SourcePort} → {link.TargetPort} (id {link.TargetId}) does not resolve." );
			}
		}

		return problems;
	}

	public IReadOnlySet<int> DirtyClosure( int itemId ) {
		var dirty = new HashSet<int> { itemId };

		if ( byId.TryGetValue( itemId, out var node ) ) {
			CollectDescendants( node, dirty );
		}

		foreach ( var link in Links.Where( link => link.Required && link.TargetId == itemId ) ) {
			dirty.Add( link.SourceId );
		}

		foreach ( var entry in byId.Values ) {
			if ( OwnsEffectsOn( entry.Payload, itemId ) && entry.Ref is { } entryRef ) {
				dirty.Add( entryRef.ItemId );
			}
		}

		return dirty;
	}

	static void CollectDescendants( ArchLayerNode node, HashSet<int> into ) {
		foreach ( var child in node.Children ) {
			if ( child.Ref is { } childRef ) {
				into.Add( childRef.ItemId );
			}

			CollectDescendants( child, into );
		}
	}

	static bool OwnsEffectsOn( object payload, int ownerId ) {
		return payload switch {
			ArchBuilding building => building.Cutouts.Any( cutout => cutout.OwnerId == ownerId ),
			ArchRoom room => room.Walls.SelectMany( wall => wall.Openings ).Any( opening => opening.OwnerId == ownerId ),
			_ => false
		};
	}

	public static ArchLayerTree Project( ArchPlan plan ) {
		var tree = new ArchLayerTree();

		if ( plan is null ) {
			return tree;
		}

		var kinds = ArchKinds.Load();
		var entries = new List<Entry>();
		var byId = new Dictionary<int, Entry>();

		void Register( object payload, int id, int parent, ArchKind kind, ArchBuilding building, ArchRoom room,
			int floor = int.MinValue, ArchLayerStage? stage = null ) {
			var entry = new Entry {
				Payload = payload,
				Id = id,
				ParentId = parent,
				Kind = kind,
				Stage = stage,
				Name = kinds.NameOf( kind, payload, id ),
				Building = building,
				Room = room,
				Floor = floor,
			};

			entries.Add( entry );
			byId[id] = entry;
		}

		foreach ( var building in plan.Buildings ) {
			Register( building, building.Id, 0, ArchKind.Building, building, null );

			foreach ( var room in building.Rooms ) {
				var kind = room.Spans ? ArchKind.Walkway : ArchKind.Room;
				Register( room, room.Id, building.Id, kind, building, room, room.Floor );

				foreach ( var wall in room.Walls ) {
					Register( wall, wall.Id, room.Id, ArchKind.Wall, building, room );

					foreach ( var opening in wall.Openings ) {
						Register( opening, opening.Id, wall.Id, ArchKind.Opening, building, room );
					}

					foreach ( var modifier in wall.Modifiers ) {
						Register( modifier, modifier.Id, wall.Id, ArchKind.WallMod, building, room,
							stage: modifier.Carves ? ArchLayerStage.Void : ArchLayerStage.Structure );
					}
				}

				foreach ( var stair in room.Stairs ) {
					Register( stair, stair.Id, room.Id, ArchKind.Stair, building, room );
				}

				foreach ( var trim in room.Trims ) {
					Register( trim, trim.Id, room.Id, ArchKind.Trim, building, room );
				}

				foreach ( var pillar in room.Pillars ) {
					Register( pillar, pillar.Id, room.Id, ArchKind.Pillar, building, room );
				}

				foreach ( var span in room.PierSpans ) {
					Register( span, span.Id, room.Id, ArchKind.Span, building, room );
				}

				foreach ( var beam in room.Beams ) {
					Register( beam, beam.Id, room.Id, ArchKind.Beam, building, room );
				}

				foreach ( var cabinets in room.Cabinets ) {
					Register( cabinets, cabinets.Id, room.Id, ArchKind.Cabinet, building, room );
				}

				foreach ( var porch in room.Porches ) {
					Register( porch, porch.Id, room.Id, ArchKind.Porch, building, room );

					foreach ( var stair in porch.Stairs ) {
						Register( stair, stair.Id, porch.Id, ArchKind.Stair, building, room );
					}

					foreach ( var pillar in porch.Pillars ) {
						Register( pillar, pillar.Id, porch.Id, ArchKind.Pillar, building, room );
					}

					foreach ( var trim in porch.Trims ) {
						Register( trim, trim.Id, porch.Id, ArchKind.Trim, building, room );
					}
				}

				foreach ( var approach in room.Approaches ) {
					Register( approach, approach.Id, room.Id, ArchKind.Approach, building, room );
				}
			}

			foreach ( var roof in building.Roofs ) {
				Register( roof, roof.Id, building.Id, ArchKind.Roof, building, null );

				foreach ( var light in roof.Lights ) {
					Register( light, light.Id, roof.Id, ArchKind.RoofLight, building, null );
				}

				foreach ( var wall in roof.Walls ) {
					Register( wall, wall.Id, roof.Id, ArchKind.Wall, building, null );

					foreach ( var opening in wall.Openings ) {
						Register( opening, opening.Id, wall.Id, ArchKind.Opening, building, null );
					}

					foreach ( var modifier in wall.Modifiers ) {
						Register( modifier, modifier.Id, wall.Id, ArchKind.WallMod, building, null,
							stage: modifier.Carves ? ArchLayerStage.Void : ArchLayerStage.Structure );
					}
				}
			}

			foreach ( var pipe in building.Downpipes ) {
				Register( pipe, pipe.Id, building.Id, ArchKind.Downpipe, building, null );
			}

			foreach ( var run in building.Pipes ) {
				Register( run, run.Id, building.Id, ArchKind.Pipe, building, null );
			}

			foreach ( var bracket in building.Brackets ) {
				Register( bracket, bracket.Id, building.Id, ArchKind.Bracket, building, null );
			}

			foreach ( var fence in building.Fences ) {
				Register( fence, fence.Id, building.Id, ArchKind.Fence, building, null );
			}

			foreach ( var platform in building.Platforms ) {
				Register( platform, platform.Id, building.Id, ArchKind.Platform, building, null );

				foreach ( var stair in platform.Stairs ) {
					Register( stair, stair.Id, platform.Id, ArchKind.CarvedStair, building, null );
				}
			}

			foreach ( var ladder in building.Ladders ) {
				Register( ladder, ladder.Id, building.Id, ArchKind.Ladder, building, null );
			}

			foreach ( var balcony in building.Balconies ) {
				Register( balcony, balcony.Id, building.Id, ArchKind.Balcony, building, null );
			}

			foreach ( var flight in building.ExteriorStairs ) {
				Register( flight, flight.Id, building.Id, ArchKind.ExteriorStair, building, null );
			}

			foreach ( var cut in building.Cuts ) {
				Register( cut, cut.Id, building.Id, ArchKind.Cut, building, null,
					stage: cut.Mode != ArchZoneMode.Carve ? ArchLayerStage.Finish : ArchLayerStage.Void );
			}
		}

		foreach ( var road in plan.Roads() ) {
			Register( road, road.Id, 0, ArchKind.Road, null, null );

			foreach ( var crossing in road.Crossings ) {
				Register( crossing, crossing.Id, road.Id, ArchKind.Crossing, null, null );
			}

			foreach ( var bridge in road.Bridges ) {
				Register( bridge, bridge.Id, road.Id, ArchKind.Bridge, null, null );
			}

			foreach ( var tunnel in road.Tunnels ) {
				Register( tunnel, tunnel.Id, road.Id, ArchKind.Tunnel, null, null );
			}


			foreach ( var cut in road.Cuts ) {
				Register( cut, cut.Id, road.Id, ArchKind.Cut, null, null,
					stage: cut.Mode != ArchZoneMode.Carve ? ArchLayerStage.Finish : ArchLayerStage.Void );
			}
		}

		foreach ( var unit in plan.Units ) {
			if ( unit is ArchBuilding or ArchRoadPart ) {
				continue;
			}

			var owner = kinds.Parent( unit.Kind, unit );

			Register( unit, unit.Id, owner, unit.Kind, plan.FindBuilding( owner ), null,
				stage: kinds.Stage( unit.Kind, unit ) );

			foreach ( var cut in unit.Cuts ) {
				Register( cut, cut.Id, unit.Id, ArchKind.Cut, null, null,
					stage: cut.Mode != ArchZoneMode.Carve ? ArchLayerStage.Finish : ArchLayerStage.Void );
			}
		}

		foreach ( var record in plan.Layers ) {
			if ( byId.TryGetValue( record.ItemId, out var entry ) && record.ParentId != record.ItemId ) {
				entry.ParentId = record.ParentId;
				entry.Kind = record.Kind;
				entry.Stage = record.Stage;
				entry.Enabled = record.Enabled;
				entry.Locked = record.Locked;
				entry.Order = record.Order;
			}
		}

		var nodes = new Dictionary<int, ArchLayerNode>();

		foreach ( var entry in entries ) {
			var node = new ArchLayerNode {
				Ref = new ArchLayerRef { ItemId = entry.Id, Kind = entry.Kind, ParentId = entry.ParentId },
				Kind = entry.Kind,
				Stage = entry.Stage ?? kinds.Stage( entry.Kind ),
				Domain = kinds.Domain( entry.Kind ),
				Payload = entry.Payload,
				Name = entry.Name,
				Enabled = entry.Enabled,
				Locked = entry.Locked,
				Order = entry.Order,
				Floor = entry.Floor,
				Building = entry.Building,
				Room = entry.Room,
			};

			nodes[entry.Id] = node;
			tree.byId[entry.Id] = node;
			tree.byPayload[entry.Payload] = node;
		}

		var memberOf = new Dictionary<int, ArchLayerNode>();

		foreach ( var assembly in plan.Assemblies ) {
			var node = new ArchLayerNode {
				Ref = new ArchLayerRef { ItemId = assembly.Id, Kind = ArchKind.Assembly, ParentId = 0 },
				Kind = ArchKind.Assembly,
				Stage = kinds.Stage( ArchKind.Assembly ),
				Domain = DomainOf( assembly, byId, kinds ),
				Payload = assembly,
				Name = assembly.Name,
			};

			nodes[assembly.Id] = node;
			tree.byId[assembly.Id] = node;
			tree.byPayload[assembly] = node;

			foreach ( var childId in assembly.Children.Where( childId => childId != assembly.Id ) ) {
				memberOf[childId] = node;
			}
		}

		foreach ( var instance in plan.Instances ) {
			var node = new ArchLayerNode {
				Ref = new ArchLayerRef { ItemId = instance.Id, Kind = ArchKind.AssetInstance, ParentId = 0 },
				Kind = ArchKind.AssetInstance,
				Stage = kinds.Stage( ArchKind.AssetInstance ),
				Domain = ArchLayerDomain.Connections,
				Payload = instance,
				Name = instance.Name,
			};

			nodes[instance.Id] = node;
			tree.byId[instance.Id] = node;
			tree.byPayload[instance] = node;
			tree.AddRoot( node );
		}

		foreach ( var building in plan.Buildings ) {
			if ( !nodes.TryGetValue( building.Id, out var buildingNode ) ) {
				continue;
			}

			foreach ( var floor in entries
				.Where( entry => entry.ParentId == building.Id && (entry.Kind == ArchKind.Room || entry.Kind == ArchKind.Walkway) )
				.Select( entry => entry.Floor )
				.Distinct()
				.OrderBy( floor => floor ) ) {
				var story = new ArchLayerNode {
					Kind = ArchKind.Story,
					Stage = ArchLayerStage.Shape,
					Domain = ArchLayerDomain.Buildings,
					Name = $"Level {floor}",
					Floor = floor,
					Building = building,
				};

				story.Parent = buildingNode;
				buildingNode.Children.Add( story );
				tree.stories[(building.Id, floor)] = story;
			}
		}

		var typedHost = new Dictionary<int, ArchLayerNode>();

		foreach ( var entry in entries ) {
			var node = nodes[entry.Id];
			var host = entry.ParentId != 0 && nodes.TryGetValue( entry.ParentId, out var parent ) ? parent : null;

			if ( host is not null
				&& host.Kind == ArchKind.Building
				&& (node.Kind == ArchKind.Room || node.Kind == ArchKind.Walkway)
				&& tree.stories.TryGetValue( (entry.ParentId, entry.Floor), out var story ) ) {
				host = story;
			}

			if ( host is not null ) {
				typedHost[entry.Id] = host;
			}

			if ( memberOf.TryGetValue( entry.Id, out var group ) ) {
				group.Children.Add( node );
				node.Parent = group;
				continue;
			}

			if ( host is null ) {
				tree.AddRoot( node );
				continue;
			}

			host.Children.Add( node );
			node.Parent = host;
		}

		foreach ( var assembly in plan.Assemblies ) {
			foreach ( var nested in plan.Assemblies.Where( inner => inner.Id != assembly.Id && assembly.Children.Contains( inner.Id ) ) ) {
				var child = nodes[nested.Id];

				nodes[assembly.Id].Children.Add( child );
				child.Parent = nodes[assembly.Id];
			}
		}

		foreach ( var assembly in plan.Assemblies ) {
			var node = nodes[assembly.Id];

			var member = node.Children.OrderBy( child => Membership( assembly, child ) ).ToList();

			node.Children.Clear();
			node.Children.AddRange( member );

			foreach ( var link in plan.Links.Where( link => link.SourceId == assembly.Id ) ) {
				tree.byId.TryGetValue( link.TargetId, out var targetNode );

				if ( targetNode is null || Within( targetNode, node ) ) {
					continue;
				}

				node.Children.Add( new ArchLayerNode {
					Kind = ArchKind.Assembly,
					Stage = ArchLayerStage.Reference,
					Domain = node.Domain,
					Payload = new ArchLayerReference { SourcePort = link.SourcePort, TargetId = link.TargetId, TargetPort = link.TargetPort },
					Name = $"{link.SourcePort} → {targetNode.Name}",
					Parent = node,
				} );
			}

			if ( node.Parent is null && Homed( assembly, typedHost ) is { } home ) {
				home.Children.Add( node );
				node.Parent = home;
			}

			if ( node.Parent is null ) {
				tree.AddRoot( node );
			}
		}

		foreach ( var group in tree.Domains ) {
			Sort( group.Children );
		}

		tree.Links = plan.Links;

		return tree;
	}

	static void Sort( List<ArchLayerNode> children ) {
		if ( children.Count > 1 ) {
			var ordered = children.OrderBy( child => child.Order ).ToList();

			children.Clear();
			children.AddRange( ordered );
		}

		foreach ( var child in children ) {
			Sort( child.Children );
		}
	}

	static bool Within( ArchLayerNode node, ArchLayerNode ancestor ) {
		for ( var current = node; current is not null; current = current.Parent ) {
			if ( ReferenceEquals( current, ancestor ) ) {
				return true;
			}
		}

		return false;
	}

	// Group hangs where its members came from; mixed hosts or self-containing groups root at domain
	static ArchLayerNode Homed( ArchSiteAssembly assembly, Dictionary<int, ArchLayerNode> typedHost ) {
		ArchLayerNode home = null;

		foreach ( var childId in assembly.Children ) {
			if ( !typedHost.TryGetValue( childId, out var host ) ) {
				return null;
			}

			home ??= host;

			if ( !ReferenceEquals( home, host ) ) {
				return null;
			}
		}

		for ( var walk = home; walk is not null; walk = walk.Parent ) {
			if ( walk.Ref is { } layer && assembly.Children.Contains( layer.ItemId ) ) {
				return null;
			}
		}

		return home;
	}

	static int Membership( ArchSiteAssembly assembly, ArchLayerNode child ) {
		var at = child.Ref is { } layer ? assembly.Children.IndexOf( layer.ItemId ) : -1;

		return at < 0 ? int.MaxValue : at;
	}

	static ArchLayerDomain DomainOf( ArchSiteAssembly assembly, Dictionary<int, Entry> byId, ArchKinds kinds ) {
		foreach ( var childId in assembly.Children ) {
			if ( byId.TryGetValue( childId, out var entry ) ) {
				return kinds.Domain( entry.Kind );
			}
		}

		return ArchLayerDomain.Connections;
	}

	void AddRoot( ArchLayerNode node ) {
		var group = Domains.FirstOrDefault( domain => domain.Domain == node.Domain );

		if ( group is null ) {
			group = new ArchLayerDomainGroup {
				Domain = node.Domain,
				Name = node.Domain switch {
					ArchLayerDomain.Buildings => "Buildings",
					ArchLayerDomain.Connections => "Connections",
					_ => "Infrastructure"
				}
			};

			Domains.Add( group );
		}

		group.Children.Add( node );
	}

	sealed class Entry {
		public object Payload;
		public int Id;
		public int ParentId;
		public ArchKind Kind;
		public ArchLayerStage? Stage;
		public bool Enabled = true;
		public bool Locked;
		public int Order;
		public string Name;
		public int Floor = int.MinValue;
		public ArchBuilding Building;
		public ArchRoom Room;
	}
}