Editor/InteriorLayoutBuilder/RoomLayoutGeometryBuilder.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

namespace ReusableRoomLayout;

public sealed partial class RoomLayoutGeometryBuilder
{
	public const string GeneratedRootName = "Generated Interior Layout";
	private const string LegacyGeneratedRootName = "Generated Room Layout";
	private const float TrimSurfaceEmbed = 0.25f;

	public static IReadOnlyList<GameObject> FindGeneratedRoots( Scene scene )
	{
		return scene.GetAllObjects( true )
			.Where( gameObject => gameObject.Name == GeneratedRootName || gameObject.Name == LegacyGeneratedRootName )
			.ToArray();
	}

	public GameObject Build( Scene scene, RoomLayoutDocument document )
	{
		var root = CreateGroup( scene, null, GeneratedRootName );
		foreach ( var floor in document.UsedFloors() )
		{
			BuildFloor( scene, root, document, floor );
		}

		return root;
	}

	private void BuildFloor( Scene scene, GameObject root, RoomLayoutDocument document, int floor )
	{
		var rooms = document.Rooms
			.Where( room => document.FloorFor( room ) == floor )
			.OrderBy( room => room.Id )
			.ToArray();
		var corridors = document.Corridors
			.Where( corridor => document.FloorFor( corridor ) == floor && document.CorridorDoorsAreOnFloor( corridor ) )
			.OrderBy( corridor => corridor.Id )
			.ToArray();

		if ( rooms.Length == 0 && corridors.Length == 0 )
		{
			return;
		}

		var floorRoot = CreateGroup( scene, root, GeneratedFloorGroupName( floor ) );
		floorRoot.LocalPosition = new Vector3( 0.0f, 0.0f, document.Settings.EffectiveFloorSpacing * floor );

		var floors = CreateGroup( scene, floorRoot, "Generated Floors" );
		var walls = CreateGroup( scene, floorRoot, "Generated Walls" );
		var frames = CreateGroup( scene, floorRoot, "Generated Frames" );
		var floorSlabs = new List<RoomLayoutFloorSlab>();
		var roofSlabs = new List<RoomLayoutFloorSlab>();
		var thresholdSlabs = new List<RoomLayoutThresholdSlab>();
		var solidSlabs = new List<RoomLayoutSolidSlab>();

		foreach ( var room in rooms )
		{
			BuildRoomFloors( document.Settings, floorSlabs, roofSlabs, room );
			BuildRoomWalls( scene, frames, floorSlabs, solidSlabs, document, room );
		}

		foreach ( var corridor in corridors )
		{
			BuildCorridor( scene, frames, document, corridor, floorSlabs, roofSlabs, thresholdSlabs, solidSlabs );
		}

		var cutoutRects = document.FloorCutouts
			.Where( cutout => document.FloorFor( cutout ) == floor )
			.Select( cutout => cutout.Bounds )
			.ToArray();
		var clippedFloorSlabs = ClipFloorSlabs( floorSlabs, cutoutRects );
		var clippedThresholdSlabs = ClipThresholdSlabs( thresholdSlabs, cutoutRects );

		BuildSolidGeometry( scene, walls, document.Settings, solidSlabs );
		BuildFloorGeometry( scene, floors, document.Settings, clippedFloorSlabs );
		var exposedRoofSlabs = document.Settings.RoofEnabled
			? ExposedRoofSlabs( roofSlabs, HigherFloorCoverageRects( document, floor ) )
			: Array.Empty<RoomLayoutFloorSlab>();
		if ( exposedRoofSlabs.Count > 0 )
		{
			var roofs = CreateGroup( scene, floorRoot, "Generated Roofs" );
			BuildRoofGeometry( scene, roofs, document.Settings, exposedRoofSlabs );
		}

		BuildThresholdGeometry( scene, floors, document.Settings, clippedThresholdSlabs );
	}

	private static string GeneratedFloorGroupName( int floor )
	{
		return floor >= 0
			? $"Generated Floor {floor:00}"
			: $"Generated Basement {Math.Abs( floor ):00}";
	}

	private static void BuildRoomFloors(
		RoomLayoutSettings settings,
		ICollection<RoomLayoutFloorSlab> floorSlabs,
		ICollection<RoomLayoutFloorSlab> roofSlabs,
		RoomLayoutRoom room )
	{
		AddStructuralFloorSlab(
			floorSlabs,
			$"{RoomName( room )} Floor",
			room.Bounds,
			settings,
			RoomMaterialPath( settings, room, RoomLayoutSurface.Floor ),
			RoomMaterialScale( settings, room, RoomLayoutSurface.Floor ) );

		if ( settings.RoofEnabled )
		{
			AddStructuralRoofSlab(
				roofSlabs,
				$"{RoomName( room )} Roof",
				room.Bounds,
				settings,
				RoomMaterialPath( settings, room, RoomLayoutSurface.Roof ),
				RoomMaterialScale( settings, room, RoomLayoutSurface.Roof ) );
		}
	}

	private static void BuildRoomWalls(
		Scene scene,
		GameObject frameParent,
		ICollection<RoomLayoutFloorSlab> floorSlabs,
		ICollection<RoomLayoutSolidSlab> solidSlabs,
		RoomLayoutDocument document,
		RoomLayoutRoom room )
	{
		foreach ( var side in Enum.GetValues<RoomLayoutWallSide>() )
		{
			var doors = document.Doors
				.Where( door => door.RoomId == room.Id && door.Side == side )
				.OrderBy( door => door.Offset )
				.ToArray();
			var windows = document.Windows
				.Where( window => window.RoomId == room.Id && window.Side == side )
				.OrderBy( window => window.Offset )
				.ToArray();
			var openings = WallOpeningsForSide( document, room, side, doors );

			BuildWallSide( scene, frameParent, floorSlabs, solidSlabs, document, room, side, doors, windows, openings );
		}

		BuildRoomCorners( solidSlabs, document, room );
	}

	private static void BuildWallSide(
		Scene scene,
		GameObject frameParent,
		ICollection<RoomLayoutFloorSlab> floorSlabs,
		ICollection<RoomLayoutSolidSlab> solidSlabs,
		RoomLayoutDocument document,
		RoomLayoutRoom room,
		RoomLayoutWallSide side,
		IReadOnlyList<RoomLayoutDoor> doors,
		IReadOnlyList<RoomLayoutWindow> windows,
		IReadOnlyList<RoomLayoutWallOpening> openings )
	{
		var settings = document.Settings;
		var length = side is RoomLayoutWallSide.North or RoomLayoutWallSide.South
			? room.Bounds.Width
			: room.Bounds.Height;

		var spans = OpenWallSpans( settings, length, openings );
		var wallIndex = 0;

		foreach ( var span in spans )
		{
			if ( span.y - span.x < 1.0f )
			{
				continue;
			}

			wallIndex = BuildWallSpanWithWindows( scene, frameParent, solidSlabs, settings, room, side, windows, span.x, span.y, wallIndex );
		}

		if ( settings.BaseboardsEnabled )
		{
			var baseboardIndex = 0;
			foreach ( var span in spans )
			{
				var extendStart = span.x <= 0.5f || IsCorridorConnectedDoorOpeningEdge( length, openings, span.x );
				var extendEnd = span.y >= length - 0.5f || IsCorridorConnectedDoorOpeningEdge( length, openings, span.y );
				baseboardIndex = BuildRoomBaseboard( solidSlabs, settings, room, side, span.x, span.y, extendStart, extendEnd, baseboardIndex );
			}
		}

		foreach ( var door in doors )
		{
			if ( !IsDoorConnectedToCorridor( document, door.Id ) )
			{
				BuildStandaloneDoorFloor( floorSlabs, settings, room, side, door, length );
				wallIndex = BuildStandaloneDoorHeader( solidSlabs, settings, room, side, door, length, wallIndex );
				BuildDoorFrame( scene, frameParent, settings, room, door );
			}
		}
	}

	private static bool IsDoorConnectedToCorridor( RoomLayoutDocument document, int doorId )
	{
		return document.Corridors.Any( corridor => corridor.StartDoorId == doorId || corridor.EndDoorId == doorId );
	}

	private static bool IsCorridorConnectedDoorOpeningEdge(
		float length,
		IReadOnlyList<RoomLayoutWallOpening> openings,
		float offset )
	{
		foreach ( var opening in openings )
		{
			if ( !opening.IsConnectedToCorridor )
			{
				continue;
			}

			var openStart = Math.Clamp( opening.Start, 0.0f, length );
			var openEnd = Math.Clamp( opening.End, 0.0f, length );
			if ( MathF.Abs( offset - openStart ) <= 0.5f || MathF.Abs( offset - openEnd ) <= 0.5f )
			{
				return true;
			}
		}

		return false;
	}

	private static List<Vector2> OpenWallSpans( RoomLayoutSettings settings, float length, IReadOnlyList<RoomLayoutWallOpening> openings )
	{
		var spans = new List<Vector2>();
		var cursor = 0.0f;
		var minimumWallRun = MinimumWallRunLength( settings );

		foreach ( var opening in openings.OrderBy( opening => opening.Start ) )
		{
			var openStart = Math.Clamp( opening.Start, 0.0f, length );
			var openEnd = Math.Clamp( opening.End, 0.0f, length );
			SnapOpeningToWallEnds( length, minimumWallRun, ref openStart, ref openEnd );
			if ( openEnd - openStart < 1.0f )
			{
				continue;
			}

			if ( openStart - cursor >= minimumWallRun )
			{
				spans.Add( new Vector2( cursor, openStart ) );
			}

			cursor = MathF.Max( cursor, openEnd );
		}

		if ( length - cursor >= minimumWallRun )
		{
			spans.Add( new Vector2( cursor, length ) );
		}

		return spans;
	}

	private static IReadOnlyList<RoomLayoutWallOpening> WallOpeningsForSide(
		RoomLayoutDocument document,
		RoomLayoutRoom room,
		RoomLayoutWallSide side,
		IReadOnlyList<RoomLayoutDoor> ownDoors )
	{
		var openings = new List<RoomLayoutWallOpening>();
		foreach ( var door in ownDoors )
		{
			var halfWidth = DoorOpeningWidth( document, door ) * 0.5f;
			openings.Add( new RoomLayoutWallOpening(
				door.Offset - halfWidth,
				door.Offset + halfWidth,
				IsDoorConnectedToCorridor( document, door.Id ) ) );
		}

		foreach ( var otherRoom in document.Rooms )
		{
			if ( otherRoom.Id == room.Id ||
				document.FloorFor( otherRoom ) != document.FloorFor( room ) ||
				!TryGetSharedWall( room, side, otherRoom, out var otherSide, out var sharedStart, out var sharedEnd ) )
			{
				continue;
			}

			foreach ( var door in document.Doors.Where( door => door.RoomId == otherRoom.Id && door.Side == otherSide ) )
			{
				var offset = SharedWallDoorOffset( room, side, otherRoom, door.Offset );
				var halfWidth = DoorOpeningWidth( document, door ) * 0.5f;
				var start = MathF.Max( sharedStart, offset - halfWidth );
				var end = MathF.Min( sharedEnd, offset + halfWidth );
				if ( end - start < 1.0f )
				{
					continue;
				}

				openings.Add( new RoomLayoutWallOpening(
					start,
					end,
					IsDoorConnectedToCorridor( document, door.Id ) ) );
			}
		}

		return openings;
	}

	private static bool TryGetSharedWall(
		RoomLayoutRoom room,
		RoomLayoutWallSide side,
		RoomLayoutRoom otherRoom,
		out RoomLayoutWallSide otherSide,
		out float sharedStart,
		out float sharedEnd )
	{
		otherSide = OppositeSide( side );
		sharedStart = 0.0f;
		sharedEnd = 0.0f;

		var bounds = room.Bounds;
		var otherBounds = otherRoom.Bounds;
		var sharesWallLine = side switch
		{
			RoomLayoutWallSide.North => (bounds.Y + bounds.Height).AlmostEqual( otherBounds.Y ),
			RoomLayoutWallSide.South => bounds.Y.AlmostEqual( otherBounds.Y + otherBounds.Height ),
			RoomLayoutWallSide.East => (bounds.X + bounds.Width).AlmostEqual( otherBounds.X ),
			_ => bounds.X.AlmostEqual( otherBounds.X + otherBounds.Width )
		};

		if ( !sharesWallLine )
		{
			return false;
		}

		var roomMin = side is RoomLayoutWallSide.North or RoomLayoutWallSide.South ? bounds.X : bounds.Y;
		var roomMax = side is RoomLayoutWallSide.North or RoomLayoutWallSide.South ? bounds.X + bounds.Width : bounds.Y + bounds.Height;
		var otherMin = side is RoomLayoutWallSide.North or RoomLayoutWallSide.South ? otherBounds.X : otherBounds.Y;
		var otherMax = side is RoomLayoutWallSide.North or RoomLayoutWallSide.South ? otherBounds.X + otherBounds.Width : otherBounds.Y + otherBounds.Height;

		sharedStart = MathF.Max( roomMin, otherMin ) - roomMin;
		sharedEnd = MathF.Min( roomMax, otherMax ) - roomMin;
		return sharedEnd - sharedStart >= 1.0f;
	}

	private static float SharedWallDoorOffset( RoomLayoutRoom room, RoomLayoutWallSide side, RoomLayoutRoom otherRoom, float otherOffset )
	{
		return side is RoomLayoutWallSide.North or RoomLayoutWallSide.South
			? otherRoom.Bounds.X + otherOffset - room.Bounds.X
			: otherRoom.Bounds.Y + otherOffset - room.Bounds.Y;
	}

	private static RoomLayoutWallSide OppositeSide( RoomLayoutWallSide side )
	{
		return side switch
		{
			RoomLayoutWallSide.North => RoomLayoutWallSide.South,
			RoomLayoutWallSide.South => RoomLayoutWallSide.North,
			RoomLayoutWallSide.East => RoomLayoutWallSide.West,
			_ => RoomLayoutWallSide.East
		};
	}

	private static float DoorOpeningWidth( RoomLayoutDocument document, RoomLayoutDoor door )
	{
		var connectedWidth = 0.0f;
		foreach ( var corridor in document.Corridors )
		{
			if ( corridor.StartDoorId != door.Id && corridor.EndDoorId != door.Id )
			{
				continue;
			}

			connectedWidth = MathF.Max( connectedWidth, CorridorClearWidth( document, corridor ) );
		}

		return connectedWidth > 0.0f
			? MathF.Min( door.Width, connectedWidth )
			: door.Width;
	}

	private static DoorOpeningData DoorOpeningSpan( RoomLayoutSettings settings, RoomLayoutDoor door, float wallLength )
	{
		var minimumWallRun = MinimumWallRunLength( settings );
		var halfDoor = door.Width * 0.5f;
		var start = Math.Clamp( door.Offset - halfDoor, 0.0f, wallLength );
		var end = Math.Clamp( door.Offset + halfDoor, 0.0f, wallLength );
		SnapOpeningToWallEnds( wallLength, minimumWallRun, ref start, ref end );
		return new DoorOpeningData( start, end, end - start );
	}

	private static void SnapOpeningToWallEnds( float wallLength, float minimumWallRun, ref float start, ref float end )
	{
		if ( start < minimumWallRun )
		{
			start = 0.0f;
		}

		if ( wallLength - end < minimumWallRun )
		{
			end = wallLength;
		}
	}

	private static float MinimumWallRunLength( RoomLayoutSettings settings )
	{
		return MathF.Max( 1.0f, settings.WallThickness );
	}

	private static int BuildWallSpanWithWindows(
		Scene scene,
		GameObject frameParent,
		ICollection<RoomLayoutSolidSlab> solidSlabs,
		RoomLayoutSettings settings,
		RoomLayoutRoom room,
		RoomLayoutWallSide side,
		IReadOnlyList<RoomLayoutWindow> windows,
		float start,
		float end,
		int index )
	{
		var cursor = start;
		var cursorTouchesOpening = false;
		var minimumWallRun = MinimumWallRunLength( settings );
		foreach ( var window in windows )
		{
			var halfWidth = window.Width * 0.5f;
			var openStart = Math.Clamp( window.Offset - halfWidth, start, end );
			var openEnd = Math.Clamp( window.Offset + halfWidth, start, end );
			if ( openEnd - openStart < 1.0f )
			{
				continue;
			}

			if ( openStart - start < minimumWallRun )
			{
				openStart = start;
			}

			if ( end - openEnd < minimumWallRun )
			{
				openEnd = end;
			}

			if ( openStart - cursor >= minimumWallRun )
			{
				index = BuildWallModules( solidSlabs, settings, room, side, cursor, openStart, index, cursorTouchesOpening, true );
			}

			index = BuildWindowWallOpening(
				scene,
				frameParent,
				solidSlabs,
				settings,
				room,
				side,
				window,
				MathF.Max( cursor, openStart ),
				openEnd,
				index );
			cursor = MathF.Max( cursor, openEnd );
			cursorTouchesOpening = true;
		}

		if ( end - cursor >= minimumWallRun )
		{
			index = BuildWallModules( solidSlabs, settings, room, side, cursor, end, index, cursorTouchesOpening, false );
		}

		return index;
	}

	private static int BuildWindowWallOpening(
		Scene scene,
		GameObject frameParent,
		ICollection<RoomLayoutSolidSlab> solidSlabs,
		RoomLayoutSettings settings,
		RoomLayoutRoom room,
		RoomLayoutWallSide side,
		RoomLayoutWindow window,
		float start,
		float end,
		int index )
	{
		if ( end - start < 1.0f )
		{
			return index;
		}

		var sillHeight = EffectiveWindowSillHeight( settings, window );
		var openingHeight = EffectiveWindowHeight( settings, window, sillHeight );
		var windowTop = MathF.Min( settings.WallHeight, sillHeight + openingHeight );

		if ( sillHeight > 0.5f )
		{
			index = BuildWallBodySection( solidSlabs, settings, room, side, start, end, 0.0f, sillHeight, false, index, true, true );
		}

		if ( settings.WallHeight - windowTop > 0.5f )
		{
			index = BuildWallBodySection( solidSlabs, settings, room, side, start, end, windowTop, settings.WallHeight, true, index, true, true );
		}

		BuildWindowFrame( scene, frameParent, settings, room, window, start, end, sillHeight, openingHeight );
		return index;
	}

	private static int BuildWallModules(
		ICollection<RoomLayoutSolidSlab> solidSlabs,
		RoomLayoutSettings settings,
		RoomLayoutRoom room,
		RoomLayoutWallSide side,
		float start,
		float end,
		int index,
		bool revealStartOuter = false,
		bool revealEndOuter = false )
	{
		return BuildWallBodySection( solidSlabs, settings, room, side, start, end, 0.0f, settings.WallHeight, true, index, revealStartOuter, revealEndOuter );
	}

	private static int BuildStandaloneDoorHeader(
		ICollection<RoomLayoutSolidSlab> solidSlabs,
		RoomLayoutSettings settings,
		RoomLayoutRoom room,
		RoomLayoutWallSide side,
		RoomLayoutDoor door,
		float wallLength,
		int index )
	{
		var span = DoorOpeningSpan( settings, door, wallLength );
		if ( span.OpeningWidth < 1.0f )
		{
			return index;
		}

		var openingTop = EffectiveDoorHeight( settings, door );
		if ( settings.WallHeight - openingTop < 0.5f )
		{
			return index;
		}

		return BuildWallBodySection(
			solidSlabs,
			settings,
			room,
			side,
			span.Start,
			span.End,
			openingTop,
			settings.WallHeight,
			true,
			index,
			true,
			true );
	}

	private static void BuildStandaloneDoorFloor(
		ICollection<RoomLayoutFloorSlab> floorSlabs,
		RoomLayoutSettings settings,
		RoomLayoutRoom room,
		RoomLayoutWallSide side,
		RoomLayoutDoor door,
		float wallLength )
	{
		var span = DoorOpeningSpan( settings, door, wallLength );
		if ( span.OpeningWidth < 1.0f )
		{
			return;
		}

		var box = WallBox(
			room.Bounds,
			side,
			span.Start + span.OpeningWidth * 0.5f,
			span.OpeningWidth,
			settings.WallThickness,
			settings.FloorThickness,
			0.0f );
		var rect = new RoomLayoutRect(
			box.Center.x - box.Size.x * 0.5f,
			box.Center.y - box.Size.y * 0.5f,
			box.Size.x,
			box.Size.y );
		AddFloorSlab(
			floorSlabs,
			$"{RoomName( room )} Door {door.Id:00} Floor",
			rect,
			RoomMaterialPath( settings, room, RoomLayoutSurface.Floor ),
			RoomMaterialScale( settings, room, RoomLayoutSurface.Floor ) );
	}

	private static int BuildWallBodySection(
		ICollection<RoomLayoutSolidSlab> solidSlabs,
		RoomLayoutSettings settings,
		RoomLayoutRoom room,
		RoomLayoutWallSide side,
		float start,
		float end,
		float zMin,
		float zMax,
		bool includeTopCap,
		int index,
		bool revealStartOuter = false,
		bool revealEndOuter = false )
	{
		var length = end - start;
		var height = zMax - zMin;
		if ( length < 1.0f || height < 0.5f )
		{
			return index;
		}

		var centerAlong = start + length * 0.5f;
		var outerWallMaterialPath = RoomOuterWallMaterialPath( settings, room );
		var innerWallMaterialPath = RoomInnerWallMaterialPath( settings, room );
		var outerWallMaterialScale = RoomOuterWallMaterialScale( settings, room );
		var innerWallMaterialScale = RoomInnerWallMaterialScale( settings, room );
		var capMaterialPath = RoomMaterialPath( settings, room, RoomLayoutSurface.Cap );
		var capMaterialScale = RoomMaterialScale( settings, room, RoomLayoutSurface.Cap );
		var body = WallBox( room.Bounds, side, centerAlong, length, settings.WallThickness, height, zMin + height * 0.5f );
		AddRoomWallSolidSlab(
			solidSlabs,
			$"{RoomName( room )} Wall {side} {index:00}",
			body.Center,
			body.Size,
			side,
			outerWallMaterialPath,
			innerWallMaterialPath,
			outerWallMaterialScale,
			innerWallMaterialScale,
			revealStartOuter,
			revealEndOuter );

		if ( !includeTopCap )
		{
			return index + 1;
		}

		var cap = WallBox(
			room.Bounds,
			side,
			centerAlong,
			length,
			settings.WallThickness,
			settings.WallCapHeight,
			settings.WallHeight + settings.WallCapHeight * 0.5f );
		AddSolidSlab(
			solidSlabs,
			$"{RoomName( room )} Wall {side} {index:00} Cap",
			cap.Center,
			cap.Size,
			RoomLayoutSurface.Cap,
			materialPath: capMaterialPath,
			textureWorldSize: capMaterialScale );
		return index + 1;
	}

	private static void AddRoomWallSolidSlab(
		ICollection<RoomLayoutSolidSlab> solidSlabs,
		string name,
		Vector3 center,
		Vector3 size,
		RoomLayoutWallSide side,
		string outerMaterialPath,
		string innerMaterialPath,
		float outerMaterialScale,
		float innerMaterialScale,
		bool revealStartOuter,
		bool revealEndOuter )
	{
		AddSolidSlab(
			solidSlabs,
			name,
			center,
			size,
			RoomLayoutSurface.Wall,
			materialPath: outerMaterialPath,
			textureWorldSize: outerMaterialScale,
			northMaterialPath: RoomWallFaceMaterialPath( side, revealStartOuter, revealEndOuter, CardinalDirection.North, outerMaterialPath, innerMaterialPath ),
			southMaterialPath: RoomWallFaceMaterialPath( side, revealStartOuter, revealEndOuter, CardinalDirection.South, outerMaterialPath, innerMaterialPath ),
			eastMaterialPath: RoomWallFaceMaterialPath( side, revealStartOuter, revealEndOuter, CardinalDirection.East, outerMaterialPath, innerMaterialPath ),
			westMaterialPath: RoomWallFaceMaterialPath( side, revealStartOuter, revealEndOuter, CardinalDirection.West, outerMaterialPath, innerMaterialPath ),
			northTextureWorldSize: RoomWallFaceMaterialScale( side, revealStartOuter, revealEndOuter, CardinalDirection.North, outerMaterialScale, innerMaterialScale ),
			southTextureWorldSize: RoomWallFaceMaterialScale( side, revealStartOuter, revealEndOuter, CardinalDirection.South, outerMaterialScale, innerMaterialScale ),
			eastTextureWorldSize: RoomWallFaceMaterialScale( side, revealStartOuter, revealEndOuter, CardinalDirection.East, outerMaterialScale, innerMaterialScale ),
			westTextureWorldSize: RoomWallFaceMaterialScale( side, revealStartOuter, revealEndOuter, CardinalDirection.West, outerMaterialScale, innerMaterialScale ) );
	}

	private static string RoomWallFaceMaterialPath(
		RoomLayoutWallSide side,
		bool revealStartOuter,
		bool revealEndOuter,
		CardinalDirection face,
		string outerMaterialPath,
		string innerMaterialPath )
	{
		return IsRoomWallOuterFace( side, revealStartOuter, revealEndOuter, face )
			? outerMaterialPath
			: innerMaterialPath;
	}

	private static float RoomWallFaceMaterialScale(
		RoomLayoutWallSide side,
		bool revealStartOuter,
		bool revealEndOuter,
		CardinalDirection face,
		float outerMaterialScale,
		float innerMaterialScale )
	{
		return IsRoomWallOuterFace( side, revealStartOuter, revealEndOuter, face )
			? outerMaterialScale
			: innerMaterialScale;
	}

	private static bool IsRoomWallOuterFace(
		RoomLayoutWallSide side,
		bool revealStartOuter,
		bool revealEndOuter,
		CardinalDirection face )
	{
		return side switch
		{
			RoomLayoutWallSide.North => face switch
			{
				CardinalDirection.North => true,
				CardinalDirection.West => revealStartOuter,
				CardinalDirection.East => revealEndOuter,
				_ => false
			},
			RoomLayoutWallSide.South => face switch
			{
				CardinalDirection.South => true,
				CardinalDirection.West => revealStartOuter,
				CardinalDirection.East => revealEndOuter,
				_ => false
			},
			RoomLayoutWallSide.East => face switch
			{
				CardinalDirection.East => true,
				CardinalDirection.South => revealStartOuter,
				CardinalDirection.North => revealEndOuter,
				_ => false
			},
			_ => face switch
			{
				CardinalDirection.West => true,
				CardinalDirection.South => revealStartOuter,
				CardinalDirection.North => revealEndOuter,
				_ => false
			}
		};
	}

	private static void BuildDoorFrame(
		Scene scene,
		GameObject parent,
		RoomLayoutSettings settings,
		RoomLayoutRoom room,
		RoomLayoutDoor door )
	{
		var length = door.Side is RoomLayoutWallSide.North or RoomLayoutWallSide.South
			? room.Bounds.Width
			: room.Bounds.Height;

		var span = DoorOpeningSpan( settings, door, length );
		if ( span.OpeningWidth < 1.0f )
		{
			return;
		}

		var jamb = Math.Clamp( settings.DoorFrameThickness, 1.0f, MathF.Max( 1.0f, span.OpeningWidth * 0.45f ) );
		var height = EffectiveDoorHeight( settings, door );
		if ( height < 0.5f )
		{
			return;
		}

		var left = WallBox( room.Bounds, door.Side, span.Start + jamb * 0.5f, jamb, settings.WallThickness, height, height * 0.5f );
		var right = WallBox( room.Bounds, door.Side, span.End - jamb * 0.5f, jamb, settings.WallThickness, height, height * 0.5f );
		var railHeight = MathF.Min( height, Math.Clamp( settings.DoorFrameThickness, 1.0f, MathF.Max( 1.0f, height * 0.45f ) ) );
		var header = WallBox( room.Bounds, door.Side, span.Start + span.OpeningWidth * 0.5f, span.OpeningWidth, settings.WallThickness, railHeight, height - railHeight * 0.5f );
		var materialPath = FirstMaterialPath( door.DoorFrameMaterialPath, settings.DoorFrameMaterialPath );
		var textureWorldSize = MaterialScaleOrFallback( door.DoorFrameMaterialScale, MaterialScaleForSettings( settings, RoomLayoutSurface.DoorFrame ) );

		CreateBox( scene, parent, $"{RoomName( room )} Door {door.Id:00} Jamb A", left.Center, left.Size, settings, RoomLayoutSurface.DoorFrame, materialPath, textureWorldSize );
		CreateBox( scene, parent, $"{RoomName( room )} Door {door.Id:00} Jamb B", right.Center, right.Size, settings, RoomLayoutSurface.DoorFrame, materialPath, textureWorldSize );
		CreateBox( scene, parent, $"{RoomName( room )} Door {door.Id:00} Header", header.Center, header.Size, settings, RoomLayoutSurface.DoorFrame, materialPath, textureWorldSize );
	}

	private static void BuildWindowFrame(
		Scene scene,
		GameObject parent,
		RoomLayoutSettings settings,
		RoomLayoutRoom room,
		RoomLayoutWindow window,
		float start,
		float end,
		float sillHeight,
		float openingHeight )
	{
		var width = end - start;
		var jamb = Math.Clamp( settings.WindowFrameThickness, 0.1f, MathF.Max( 0.1f, width * 0.25f ) );
		var railHeight = Math.Clamp( settings.WindowFrameThickness, 0.1f, MathF.Max( 0.1f, openingHeight * 0.25f ) );
		var depth = TrimDepth( settings, settings.WindowFrameThickness );
		var center = (start + end) * 0.5f;
		var verticalCenter = sillHeight + openingHeight * 0.5f;
		var sillCenter = sillHeight + railHeight * 0.5f;
		var headerCenter = sillHeight + MathF.Max( railHeight * 0.5f, openingHeight - railHeight * 0.5f );
		var materialPath = FirstMaterialPath( window.WindowFrameMaterialPath, settings.WindowFrameMaterialPath );
		var textureWorldSize = MaterialScaleOrFallback( window.WindowFrameMaterialScale, MaterialScaleForSettings( settings, RoomLayoutSurface.WindowFrame ) );

		var left = InteriorTrimBox( room.Bounds, window.Side, start + jamb * 0.5f, jamb, depth, openingHeight, verticalCenter );
		var right = InteriorTrimBox( room.Bounds, window.Side, end - jamb * 0.5f, jamb, depth, openingHeight, verticalCenter );
		var sill = InteriorTrimBox( room.Bounds, window.Side, center, width, depth, railHeight, sillCenter );
		var header = InteriorTrimBox( room.Bounds, window.Side, center, width, depth, railHeight, headerCenter );

		CreateBox( scene, parent, $"{RoomName( room )} Window {window.Id:00} Jamb A", left.Center, left.Size, settings, RoomLayoutSurface.WindowFrame, materialPath, textureWorldSize );
		CreateBox( scene, parent, $"{RoomName( room )} Window {window.Id:00} Jamb B", right.Center, right.Size, settings, RoomLayoutSurface.WindowFrame, materialPath, textureWorldSize );
		CreateBox( scene, parent, $"{RoomName( room )} Window {window.Id:00} Sill", sill.Center, sill.Size, settings, RoomLayoutSurface.WindowFrame, materialPath, textureWorldSize );
		CreateBox( scene, parent, $"{RoomName( room )} Window {window.Id:00} Header", header.Center, header.Size, settings, RoomLayoutSurface.WindowFrame, materialPath, textureWorldSize );
	}

	private static float EffectiveDoorHeight( RoomLayoutSettings settings, RoomLayoutDoor door )
	{
		var height = door.Height > 0.0f ? door.Height : settings.DoorHeight;
		return Math.Clamp( height, 0.0f, settings.WallHeight );
	}

	private static float EffectiveWindowSillHeight( RoomLayoutSettings settings, RoomLayoutWindow window )
	{
		var sillHeight = window.SillHeight >= 0.0f ? window.SillHeight : settings.WindowSillHeight;
		return Math.Clamp( sillHeight, 0.0f, MathF.Max( 0.0f, settings.WallHeight - 1.0f ) );
	}

	private static float EffectiveWindowHeight( RoomLayoutSettings settings, RoomLayoutWindow window, float sillHeight )
	{
		var height = window.Height > 0.0f ? window.Height : settings.WindowHeight;
		return Math.Clamp( height, 1.0f, MathF.Max( 1.0f, settings.WallHeight - sillHeight ) );
	}

	private static int BuildRoomBaseboard(
		ICollection<RoomLayoutSolidSlab> solidSlabs,
		RoomLayoutSettings settings,
		RoomLayoutRoom room,
		RoomLayoutWallSide side,
		float start,
		float end,
		bool extendStart,
		bool extendEnd,
		int index )
	{
		var length = end - start;
		if ( length < 1.0f || settings.BaseboardHeight < 0.5f || settings.BaseboardDepth < 0.5f )
		{
			return index;
		}

		var startOverlap = extendStart ? BaseboardRunOverlap( settings ) : 0.0f;
		var endOverlap = extendEnd ? BaseboardRunOverlap( settings ) : 0.0f;
		var centerAlong = (start - startOverlap + end + endOverlap) * 0.5f;
		length += startOverlap + endOverlap;
		var materialPath = RoomMaterialPath( settings, room, RoomLayoutSurface.Baseboard );
		var textureWorldSize = RoomMaterialScale( settings, room, RoomLayoutSurface.Baseboard );
		var data = BaseboardBox( room.Bounds, side, centerAlong, length, settings.BaseboardDepth, settings.BaseboardHeight );
		AddSolidSlab(
			solidSlabs,
			$"{RoomName( room )} Baseboard {side} {index:00}",
			data.Center,
			data.Size,
			RoomLayoutSurface.Baseboard,
			hasCollider: false,
			materialPath: materialPath,
			textureWorldSize: textureWorldSize );
		return index + 1;
	}

	private static WallBoxData WallBox(
		RoomLayoutRect bounds,
		RoomLayoutWallSide side,
		float centerAlong,
		float length,
		float thickness,
		float height,
		float centerZ )
	{
		return side switch
		{
			RoomLayoutWallSide.North => new WallBoxData(
				new Vector3( bounds.X + centerAlong, bounds.Y + bounds.Height + thickness * 0.5f, centerZ ),
				new Vector3( length, thickness, height ) ),
			RoomLayoutWallSide.South => new WallBoxData(
				new Vector3( bounds.X + centerAlong, bounds.Y - thickness * 0.5f, centerZ ),
				new Vector3( length, thickness, height ) ),
			RoomLayoutWallSide.East => new WallBoxData(
				new Vector3( bounds.X + bounds.Width + thickness * 0.5f, bounds.Y + centerAlong, centerZ ),
				new Vector3( thickness, length, height ) ),
			_ => new WallBoxData(
				new Vector3( bounds.X - thickness * 0.5f, bounds.Y + centerAlong, centerZ ),
				new Vector3( thickness, length, height ) )
		};
	}

	private static WallBoxData BaseboardBox(
		RoomLayoutRect bounds,
		RoomLayoutWallSide side,
		float centerAlong,
		float length,
		float depth,
		float height )
	{
		var centerZ = height * 0.5f;
		var embed = TrimEmbedForDepth( depth );
		return side switch
		{
			RoomLayoutWallSide.North => new WallBoxData(
				new Vector3( bounds.X + centerAlong, bounds.Y + bounds.Height - depth * 0.5f + embed, centerZ ),
				new Vector3( length, depth, height ) ),
			RoomLayoutWallSide.South => new WallBoxData(
				new Vector3( bounds.X + centerAlong, bounds.Y + depth * 0.5f - embed, centerZ ),
				new Vector3( length, depth, height ) ),
			RoomLayoutWallSide.East => new WallBoxData(
				new Vector3( bounds.X + bounds.Width - depth * 0.5f + embed, bounds.Y + centerAlong, centerZ ),
				new Vector3( depth, length, height ) ),
			_ => new WallBoxData(
				new Vector3( bounds.X + depth * 0.5f - embed, bounds.Y + centerAlong, centerZ ),
				new Vector3( depth, length, height ) )
		};
	}

	private static WallBoxData InteriorTrimBox(
		RoomLayoutRect bounds,
		RoomLayoutWallSide side,
		float centerAlong,
		float length,
		float depth,
		float height,
		float centerZ )
	{
		var embed = TrimEmbedForDepth( depth );
		return side switch
		{
			RoomLayoutWallSide.North => new WallBoxData(
				new Vector3( bounds.X + centerAlong, bounds.Y + bounds.Height - depth * 0.5f + embed, centerZ ),
				new Vector3( length, depth, height ) ),
			RoomLayoutWallSide.South => new WallBoxData(
				new Vector3( bounds.X + centerAlong, bounds.Y + depth * 0.5f - embed, centerZ ),
				new Vector3( length, depth, height ) ),
			RoomLayoutWallSide.East => new WallBoxData(
				new Vector3( bounds.X + bounds.Width - depth * 0.5f + embed, bounds.Y + centerAlong, centerZ ),
				new Vector3( depth, length, height ) ),
			_ => new WallBoxData(
				new Vector3( bounds.X + depth * 0.5f - embed, bounds.Y + centerAlong, centerZ ),
				new Vector3( depth, length, height ) )
		};
	}

	private static float TrimDepth( RoomLayoutSettings settings, float preferred )
	{
		return Math.Clamp( preferred, 0.5f, MathF.Max( 0.5f, settings.WallThickness ) );
	}

	private static float TrimEmbedForDepth( float depth )
	{
		return MathF.Min( TrimSurfaceEmbed, depth * 0.25f );
	}

	private static float BaseboardRunOverlap( RoomLayoutSettings settings )
	{
		if ( settings.BaseboardDepth < 0.5f )
		{
			return 0.0f;
		}

		var overlap = settings.BaseboardDepth - TrimEmbedForDepth( settings.BaseboardDepth );
		return Math.Clamp( overlap, 0.0f, MathF.Max( 0.0f, settings.WallThickness ) );
	}

	private static GameObject CreateGroup( Scene scene, GameObject parent, string name )
	{
		var group = scene.CreateObject( true );
		group.Name = name;

		if ( parent is not null )
		{
			group.SetParent( parent, false );
		}

		return group;
	}

	private static string RoomName( RoomLayoutRoom room )
	{
		return string.IsNullOrWhiteSpace( room.Name ) ? $"Room {room.Id:00}" : room.Name;
	}

	private readonly record struct DoorOpeningData( float Start, float End, float OpeningWidth );

	private readonly record struct RoomLayoutWallOpening( float Start, float End, bool IsConnectedToCorridor );

	private readonly record struct WallBoxData( Vector3 Center, Vector3 Size );
}