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

namespace ReusableRoomLayout;

public sealed partial class RoomLayoutGeometryBuilder
{
	private static int BuildCorridorBoundaryWall(
		ICollection<RoomLayoutSolidSlab> solidSlabs,
		RoomLayoutDocument document,
		RoomLayoutSettings settings,
		RoomLayoutCorridor corridor,
		IReadOnlyList<Vector2> points,
		int wallIndex,
		CorridorBoundaryEdge edge,
		DoorPoint start,
		DoorPoint end,
		float width,
		string outerWallMaterialPath,
		string innerWallMaterialPath,
		string capMaterialPath,
		string baseboardMaterialPath,
		float outerWallMaterialScale,
		float innerWallMaterialScale,
		float capMaterialScale,
		float baseboardMaterialScale )
	{
		if ( edge.Length < 1.0f )
		{
			return wallIndex;
		}

		var clippedEdges = new List<CorridorBoundaryEdge> { edge };
		ClipCorridorDoorOpening( clippedEdges, start, width );
		ClipCorridorDoorOpening( clippedEdges, end, width );

		foreach ( var clippedEdge in clippedEdges )
		{
			var openings = CorridorBoundaryOpeningsForEdge( document, corridor, points, clippedEdge, width );
			wallIndex = BuildCorridorBoundaryWallWithOpenings(
				solidSlabs,
				settings,
				corridor.Id,
				wallIndex,
				clippedEdge,
				openings,
				outerWallMaterialPath,
				innerWallMaterialPath,
				capMaterialPath,
				baseboardMaterialPath,
				outerWallMaterialScale,
				innerWallMaterialScale,
				capMaterialScale,
				baseboardMaterialScale );
		}

		return wallIndex;
	}

	private static int BuildCorridorBoundaryWallWithOpenings(
		ICollection<RoomLayoutSolidSlab> solidSlabs,
		RoomLayoutSettings settings,
		int corridorId,
		int wallIndex,
		CorridorBoundaryEdge edge,
		IReadOnlyList<CorridorBoundaryOpening> openings,
		string outerWallMaterialPath,
		string innerWallMaterialPath,
		string capMaterialPath,
		string baseboardMaterialPath,
		float outerWallMaterialScale,
		float innerWallMaterialScale,
		float capMaterialScale,
		float baseboardMaterialScale )
	{
		var cursor = edge.Start;
		var cursorTouchesOpening = false;
		foreach ( var opening in openings.OrderBy( opening => opening.Start ) )
		{
			if ( opening.End <= cursor + 0.5f )
			{
				continue;
			}

			var openingStart = Math.Clamp( opening.Start, edge.Start, edge.End );
			var openingEnd = Math.Clamp( opening.End, edge.Start, edge.End );
			openingStart = MathF.Max( openingStart, cursor );
			if ( openingStart > cursor + 0.5f )
			{
				wallIndex = BuildCorridorBoundaryWallRun(
					solidSlabs,
					settings,
					corridorId,
					wallIndex,
					edge.WithSpan( cursor, openingStart ),
					cursorTouchesOpening,
					true,
					outerWallMaterialPath,
					innerWallMaterialPath,
					capMaterialPath,
					baseboardMaterialPath,
					outerWallMaterialScale,
					innerWallMaterialScale,
					capMaterialScale,
					baseboardMaterialScale );
			}

			if ( openingEnd - openingStart >= 1.0f )
			{
				wallIndex = BuildCorridorBoundaryOpening(
					solidSlabs,
					settings,
					corridorId,
					wallIndex,
					edge.WithSpan( openingStart, openingEnd ),
					opening,
					outerWallMaterialPath,
					innerWallMaterialPath,
					capMaterialPath,
					baseboardMaterialPath,
					outerWallMaterialScale,
					innerWallMaterialScale,
					capMaterialScale,
					baseboardMaterialScale );
			}

			cursor = MathF.Max( cursor, openingEnd );
			cursorTouchesOpening = true;
		}

		if ( edge.End > cursor + 0.5f )
		{
			wallIndex = BuildCorridorBoundaryWallRun(
				solidSlabs,
				settings,
				corridorId,
				wallIndex,
				edge.WithSpan( cursor, edge.End ),
				cursorTouchesOpening,
				false,
				outerWallMaterialPath,
				innerWallMaterialPath,
				capMaterialPath,
				baseboardMaterialPath,
				outerWallMaterialScale,
				innerWallMaterialScale,
				capMaterialScale,
				baseboardMaterialScale );
		}

		return wallIndex;
	}

	private static int BuildCorridorBoundaryWallRun(
		ICollection<RoomLayoutSolidSlab> solidSlabs,
		RoomLayoutSettings settings,
		int corridorId,
		int wallIndex,
		CorridorBoundaryEdge edge,
		bool revealStartOuter,
		bool revealEndOuter,
		string outerWallMaterialPath,
		string innerWallMaterialPath,
		string capMaterialPath,
		string baseboardMaterialPath,
		float outerWallMaterialScale,
		float innerWallMaterialScale,
		float capMaterialScale,
		float baseboardMaterialScale )
	{
		BuildCorridorBoundaryWallSection(
			solidSlabs,
			settings,
			corridorId,
			wallIndex,
			edge,
			0.0f,
			settings.WallHeight,
			true,
			revealStartOuter,
			revealEndOuter,
			outerWallMaterialPath,
			innerWallMaterialPath,
			capMaterialPath,
			outerWallMaterialScale,
			innerWallMaterialScale,
			capMaterialScale );
		BuildCorridorBoundaryBaseboardRun( solidSlabs, settings, corridorId, wallIndex, edge, baseboardMaterialPath, baseboardMaterialScale );
		return wallIndex + 1;
	}

	private static int BuildCorridorBoundaryOpening(
		ICollection<RoomLayoutSolidSlab> solidSlabs,
		RoomLayoutSettings settings,
		int corridorId,
		int wallIndex,
		CorridorBoundaryEdge edge,
		CorridorBoundaryOpening opening,
		string outerWallMaterialPath,
		string innerWallMaterialPath,
		string capMaterialPath,
		string baseboardMaterialPath,
		float outerWallMaterialScale,
		float innerWallMaterialScale,
		float capMaterialScale,
		float baseboardMaterialScale )
	{
		if ( opening.Kind == CorridorBoundaryOpeningKind.Door )
		{
			var openingTop = EffectiveDoorHeight( settings, opening.Door );
			if ( settings.WallHeight - openingTop > 0.5f )
			{
				BuildCorridorBoundaryWallSection(
					solidSlabs,
					settings,
					corridorId,
					wallIndex,
					edge,
					openingTop,
					settings.WallHeight,
					true,
					true,
					true,
					outerWallMaterialPath,
					innerWallMaterialPath,
					capMaterialPath,
					outerWallMaterialScale,
					innerWallMaterialScale,
					capMaterialScale );
				return wallIndex + 1;
			}

			return wallIndex;
		}

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

		if ( sillHeight > 0.5f )
		{
			BuildCorridorBoundaryWallSection(
				solidSlabs,
				settings,
				corridorId,
				wallIndex,
				edge,
				0.0f,
				sillHeight,
				false,
				true,
				true,
				outerWallMaterialPath,
				innerWallMaterialPath,
				capMaterialPath,
				outerWallMaterialScale,
				innerWallMaterialScale,
				capMaterialScale );
		}

		if ( settings.WallHeight - windowTop > 0.5f )
		{
			BuildCorridorBoundaryWallSection(
				solidSlabs,
				settings,
				corridorId,
				wallIndex,
				edge,
				windowTop,
				settings.WallHeight,
				true,
				true,
				true,
				outerWallMaterialPath,
				innerWallMaterialPath,
				capMaterialPath,
				outerWallMaterialScale,
				innerWallMaterialScale,
				capMaterialScale );
		}

		BuildCorridorBoundaryBaseboardRun( solidSlabs, settings, corridorId, wallIndex, edge, baseboardMaterialPath, baseboardMaterialScale );
		return wallIndex + 1;
	}

	private static void BuildCorridorBoundaryWallSection(
		ICollection<RoomLayoutSolidSlab> solidSlabs,
		RoomLayoutSettings settings,
		int corridorId,
		int wallIndex,
		CorridorBoundaryEdge edge,
		float zMin,
		float zMax,
		bool includeTopCap,
		bool revealStartOuter,
		bool revealEndOuter,
		string outerWallMaterialPath,
		string innerWallMaterialPath,
		string capMaterialPath,
		float outerWallMaterialScale,
		float innerWallMaterialScale,
		float capMaterialScale )
	{
		var height = zMax - zMin;
		if ( edge.Length < 1.0f || height < 0.5f )
		{
			return;
		}

		var wall = CorridorBoundaryBox( settings, edge, edge.Length, height, zMin + height * 0.5f );
		AddSolidSlab(
			solidSlabs,
			$"Corridor {corridorId:00} Boundary {wallIndex:00} Wall",
			wall.Center,
			wall.Size,
			RoomLayoutSurface.Wall,
			materialPath: outerWallMaterialPath,
			textureWorldSize: outerWallMaterialScale,
			northMaterialPath: CorridorBoundaryFaceMaterialPath( edge, revealStartOuter, revealEndOuter, CardinalDirection.North, outerWallMaterialPath, innerWallMaterialPath ),
			southMaterialPath: CorridorBoundaryFaceMaterialPath( edge, revealStartOuter, revealEndOuter, CardinalDirection.South, outerWallMaterialPath, innerWallMaterialPath ),
			eastMaterialPath: CorridorBoundaryFaceMaterialPath( edge, revealStartOuter, revealEndOuter, CardinalDirection.East, outerWallMaterialPath, innerWallMaterialPath ),
			westMaterialPath: CorridorBoundaryFaceMaterialPath( edge, revealStartOuter, revealEndOuter, CardinalDirection.West, outerWallMaterialPath, innerWallMaterialPath ),
			northTextureWorldSize: CorridorBoundaryFaceMaterialScale( edge, revealStartOuter, revealEndOuter, CardinalDirection.North, outerWallMaterialScale, innerWallMaterialScale ),
			southTextureWorldSize: CorridorBoundaryFaceMaterialScale( edge, revealStartOuter, revealEndOuter, CardinalDirection.South, outerWallMaterialScale, innerWallMaterialScale ),
			eastTextureWorldSize: CorridorBoundaryFaceMaterialScale( edge, revealStartOuter, revealEndOuter, CardinalDirection.East, outerWallMaterialScale, innerWallMaterialScale ),
			westTextureWorldSize: CorridorBoundaryFaceMaterialScale( edge, revealStartOuter, revealEndOuter, CardinalDirection.West, outerWallMaterialScale, innerWallMaterialScale ) );

		if ( !includeTopCap )
		{
			return;
		}

		var cap = CorridorBoundaryBox( settings, edge, edge.Length, settings.WallCapHeight, settings.WallHeight + settings.WallCapHeight * 0.5f );
		AddSolidSlab(
			solidSlabs,
			$"Corridor {corridorId:00} Boundary {wallIndex:00} Wall Cap",
			cap.Center,
			cap.Size,
			RoomLayoutSurface.Cap,
			materialPath: capMaterialPath,
			textureWorldSize: capMaterialScale );
	}

	private static string CorridorBoundaryFaceMaterialPath(
		CorridorBoundaryEdge edge,
		bool revealStartOuter,
		bool revealEndOuter,
		CardinalDirection face,
		string outerWallMaterialPath,
		string innerWallMaterialPath )
	{
		return IsCorridorBoundaryOuterFace( edge, revealStartOuter, revealEndOuter, face )
			? outerWallMaterialPath
			: innerWallMaterialPath;
	}

	private static float CorridorBoundaryFaceMaterialScale(
		CorridorBoundaryEdge edge,
		bool revealStartOuter,
		bool revealEndOuter,
		CardinalDirection face,
		float outerWallMaterialScale,
		float innerWallMaterialScale )
	{
		return IsCorridorBoundaryOuterFace( edge, revealStartOuter, revealEndOuter, face )
			? outerWallMaterialScale
			: innerWallMaterialScale;
	}

	private static bool IsCorridorBoundaryOuterFace(
		CorridorBoundaryEdge edge,
		bool revealStartOuter,
		bool revealEndOuter,
		CardinalDirection face )
	{
		return face switch
		{
			CardinalDirection.North => edge.Horizontal
				? edge.Direction > 0
				: revealEndOuter,
			CardinalDirection.South => edge.Horizontal
				? edge.Direction < 0
				: revealStartOuter,
			CardinalDirection.East => edge.Horizontal
				? revealEndOuter
				: edge.Direction > 0,
			_ => edge.Horizontal
				? revealStartOuter
				: edge.Direction < 0
		};
	}

	private static void BuildCorridorBoundaryBaseboardRun(
		ICollection<RoomLayoutSolidSlab> solidSlabs,
		RoomLayoutSettings settings,
		int corridorId,
		int wallIndex,
		CorridorBoundaryEdge edge,
		string materialPath,
		float textureWorldSize )
	{
		BuildCorridorBaseboardRun(
			solidSlabs,
			settings,
			corridorId,
			wallIndex,
			edge.Horizontal ? edge.CenterAlong : edge.Fixed,
			edge.Horizontal ? edge.Fixed : edge.CenterAlong,
			edge.Length,
			edge.Horizontal,
			edge.Direction,
			materialPath,
			textureWorldSize );
	}

	private static WallBoxData CorridorBoundaryBox(
		RoomLayoutSettings settings,
		CorridorBoundaryEdge edge,
		float length,
		float height,
		float centerZ )
	{
		var wallOffset = settings.WallThickness * 0.5f * edge.Direction;
		return edge.Horizontal
			? new WallBoxData(
				new Vector3( edge.CenterAlong, edge.Fixed + wallOffset, centerZ ),
				new Vector3( length, settings.WallThickness, height ) )
			: new WallBoxData(
				new Vector3( edge.Fixed + wallOffset, edge.CenterAlong, centerZ ),
				new Vector3( settings.WallThickness, length, height ) );
	}

	private static void ClipCorridorDoorOpening( List<CorridorBoundaryEdge> edges, DoorPoint door, float width )
	{
		var clipped = new List<CorridorBoundaryEdge>();
		foreach ( var edge in edges )
		{
			if ( !TryGetCorridorDoorOpeningSpan( edge, door, width, out var openStart, out var openEnd ) ||
				openEnd <= edge.Start + 0.5f ||
				openStart >= edge.End - 0.5f )
			{
				clipped.Add( edge );
				continue;
			}

			var beforeEnd = MathF.Min( openStart, edge.End );
			if ( beforeEnd - edge.Start >= 1.0f )
			{
				clipped.Add( edge.WithSpan( edge.Start, beforeEnd ) );
			}

			var afterStart = MathF.Max( openEnd, edge.Start );
			if ( edge.End - afterStart >= 1.0f )
			{
				clipped.Add( edge.WithSpan( afterStart, edge.End ) );
			}
		}

		edges.Clear();
		edges.AddRange( clipped );
	}

	private static bool TryGetCorridorDoorOpeningSpan(
		CorridorBoundaryEdge edge,
		DoorPoint door,
		float width,
		out float openStart,
		out float openEnd )
	{
		openStart = 0.0f;
		openEnd = 0.0f;

		var verticalDoorWall = MathF.Abs( door.Normal.y ) > MathF.Abs( door.Normal.x );
		var halfWidth = width * 0.5f;

		if ( verticalDoorWall )
		{
			if ( !edge.Horizontal ||
				!edge.Fixed.AlmostEqual( door.Point.y ) ||
				edge.Direction != -MathF.Sign( door.Normal.y ) )
			{
				return false;
			}

			openStart = door.Point.x - halfWidth;
			openEnd = door.Point.x + halfWidth;
			return true;
		}

		if ( edge.Horizontal ||
			!edge.Fixed.AlmostEqual( door.Point.x ) ||
			edge.Direction != -MathF.Sign( door.Normal.x ) )
		{
			return false;
		}

		openStart = door.Point.y - halfWidth;
		openEnd = door.Point.y + halfWidth;
		return true;
	}

	private enum CardinalDirection
	{
		North,
		South,
		East,
		West
	}

	private readonly record struct CorridorBoundaryEdge( float Start, float End, float Fixed, bool Horizontal, int Direction )
	{
		public float Length => End - Start;
		public float CenterAlong => (Start + End) * 0.5f;
		public CorridorBoundaryEdge WithSpan( float start, float end ) => new( start, end, Fixed, Horizontal, Direction );
	}
}