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

namespace ReusableRoomLayout;

public sealed partial class RoomLayoutGeometryBuilder
{
	private static WallBoxData CorridorInteriorTrimBox(
		RoomLayoutSettings settings,
		CorridorBoundaryEdge edge,
		float length,
		float depth,
		float height,
		float centerZ )
	{
		var embed = TrimEmbedForDepth( depth );
		var inwardOffset = -edge.Direction * depth * 0.5f + edge.Direction * embed;
		return edge.Horizontal
			? new WallBoxData(
				new Vector3( edge.CenterAlong, edge.Fixed + inwardOffset, centerZ ),
				new Vector3( length, depth, height ) )
			: new WallBoxData(
				new Vector3( edge.Fixed + inwardOffset, edge.CenterAlong, centerZ ),
				new Vector3( depth, length, height ) );
	}

	private static IReadOnlyList<CorridorBoundaryOpening> CorridorBoundaryOpeningsForEdge(
		RoomLayoutDocument document,
		RoomLayoutCorridor corridor,
		IReadOnlyList<Vector2> points,
		CorridorBoundaryEdge edge,
		float corridorWidth )
	{
		var openings = new List<CorridorBoundaryOpening>();

		foreach ( var door in document.Doors.Where( door => door.CorridorId == corridor.Id ) )
		{
			if ( TryGetCorridorOpeningSpan(
				points,
				door.CorridorSegmentIndex,
				door.CorridorSide,
				door.Offset,
				door.Width,
				edge,
				corridorWidth,
				out var start,
				out var end ) )
			{
				openings.Add( new CorridorBoundaryOpening( CorridorBoundaryOpeningKind.Door, start, end, door, null ) );
			}
		}

		foreach ( var window in document.Windows.Where( window => window.CorridorId == corridor.Id ) )
		{
			if ( TryGetCorridorOpeningSpan(
				points,
				window.CorridorSegmentIndex,
				window.CorridorSide,
				window.Offset,
				window.Width,
				edge,
				corridorWidth,
				out var start,
				out var end ) )
			{
				openings.Add( new CorridorBoundaryOpening( CorridorBoundaryOpeningKind.Window, start, end, null, window ) );
			}
		}

		return openings;
	}

	private static bool TryGetCorridorOpeningSpan(
		IReadOnlyList<Vector2> points,
		int segmentIndex,
		int side,
		float offset,
		float openingWidth,
		CorridorBoundaryEdge edge,
		float corridorWidth,
		out float openStart,
		out float openEnd )
	{
		openStart = 0.0f;
		openEnd = 0.0f;

		if ( !TryGetCorridorOpeningEdge( points, segmentIndex, side, offset, openingWidth, corridorWidth, out var openingEdge ) ||
			openingEdge.Horizontal != edge.Horizontal ||
			openingEdge.Direction != edge.Direction ||
			!openingEdge.Fixed.AlmostEqual( edge.Fixed ) )
		{
			return false;
		}

		openStart = MathF.Max( edge.Start, openingEdge.Start );
		openEnd = MathF.Min( edge.End, openingEdge.End );
		return openEnd - openStart >= 1.0f;
	}

	private static bool TryGetCorridorOpeningEdge(
		IReadOnlyList<Vector2> points,
		int segmentIndex,
		int side,
		float offset,
		float openingWidth,
		float corridorWidth,
		out CorridorBoundaryEdge edge )
	{
		edge = default;

		if ( segmentIndex < 0 || segmentIndex >= points.Count - 1 || openingWidth < 1.0f )
		{
			return false;
		}

		var start = points[segmentIndex];
		var end = points[segmentIndex + 1];
		var delta = end - start;
		if ( delta.Length < 1.0f )
		{
			return false;
		}

		var horizontal = MathF.Abs( delta.x ) >= MathF.Abs( delta.y );
		var minAlong = horizontal ? MathF.Min( start.x, end.x ) : MathF.Min( start.y, end.y );
		var maxAlong = horizontal ? MathF.Max( start.x, end.x ) : MathF.Max( start.y, end.y );
		var halfWidth = openingWidth * 0.5f;
		var centerAlong = Math.Clamp( minAlong + offset, minAlong + halfWidth, MathF.Max( minAlong + halfWidth, maxAlong - halfWidth ) );
		var direction = Math.Sign( side == 0 ? 1 : side );
		var fixedCoordinate = (horizontal ? start.y : start.x) + direction * corridorWidth * 0.5f;

		edge = new CorridorBoundaryEdge( centerAlong - halfWidth, centerAlong + halfWidth, fixedCoordinate, horizontal, direction );
		return edge.Length >= 1.0f;
	}

	private static void BuildCorridorOpeningFrames(
		Scene scene,
		GameObject frameParent,
		RoomLayoutSettings settings,
		RoomLayoutDocument document,
		RoomLayoutCorridor corridor,
		IReadOnlyList<Vector2> points,
		float corridorWidth )
	{
		foreach ( var door in document.Doors.Where( door => door.CorridorId == corridor.Id ) )
		{
			if ( TryGetCorridorOpeningEdge(
				points,
				door.CorridorSegmentIndex,
				door.CorridorSide,
				door.Offset,
				door.Width,
				corridorWidth,
				out var edge ) )
			{
				BuildCorridorDoorFrame( scene, frameParent, settings, corridor.Id, door, edge );
			}
		}

		foreach ( var window in document.Windows.Where( window => window.CorridorId == corridor.Id ) )
		{
			if ( TryGetCorridorOpeningEdge(
				points,
				window.CorridorSegmentIndex,
				window.CorridorSide,
				window.Offset,
				window.Width,
				corridorWidth,
				out var edge ) )
			{
				BuildCorridorWindowFrame( scene, frameParent, settings, corridor.Id, window, edge );
			}
		}
	}

	private static void BuildCorridorOpeningFloors(
		ICollection<RoomLayoutFloorSlab> floorSlabs,
		ICollection<RoomLayoutFloorSlab> roofSlabs,
		RoomLayoutSettings settings,
		RoomLayoutDocument document,
		RoomLayoutCorridor corridor,
		IReadOnlyList<Vector2> points,
		float corridorWidth,
		string floorMaterialPath,
		float floorMaterialScale,
		string roofMaterialPath,
		float roofMaterialScale )
	{
		foreach ( var door in document.Doors.Where( door => door.CorridorId == corridor.Id ) )
		{
			if ( !TryGetCorridorOpeningEdge(
				points,
				door.CorridorSegmentIndex,
				door.CorridorSide,
				door.Offset,
				door.Width,
				corridorWidth,
				out var edge ) )
			{
				continue;
			}

			var rect = edge.Horizontal
				? new RoomLayoutRect(
					edge.Start,
					edge.Direction > 0 ? edge.Fixed : edge.Fixed - settings.WallThickness,
					edge.Length,
					settings.WallThickness )
				: new RoomLayoutRect(
					edge.Direction > 0 ? edge.Fixed : edge.Fixed - settings.WallThickness,
					edge.Start,
					settings.WallThickness,
					edge.Length );

			AddFloorSlab( floorSlabs, $"Corridor {corridor.Id:00} Door {door.Id:00} Floor", rect, floorMaterialPath, floorMaterialScale );
			if ( settings.RoofEnabled )
			{
				AddRoofSlab( roofSlabs, $"Corridor {corridor.Id:00} Door {door.Id:00} Roof", rect, roofMaterialPath, roofMaterialScale );
			}
		}
	}

	private static void BuildCorridorDoorFrame(
		Scene scene,
		GameObject frameParent,
		RoomLayoutSettings settings,
		int corridorId,
		RoomLayoutDoor door,
		CorridorBoundaryEdge edge )
	{
		if ( edge.Length < 1.0f )
		{
			return;
		}

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

		var railHeight = MathF.Min( height, Math.Clamp( settings.DoorFrameThickness, 1.0f, MathF.Max( 1.0f, height * 0.45f ) ) );
		var leftEdge = edge.WithSpan( edge.Start, edge.Start + jamb );
		var rightEdge = edge.WithSpan( edge.End - jamb, edge.End );
		var left = CorridorBoundaryBox( settings, leftEdge, leftEdge.Length, height, height * 0.5f );
		var right = CorridorBoundaryBox( settings, rightEdge, rightEdge.Length, height, height * 0.5f );
		var header = CorridorBoundaryBox( settings, edge, edge.Length, railHeight, height - railHeight * 0.5f );
		var materialPath = FirstMaterialPath( door.DoorFrameMaterialPath, settings.DoorFrameMaterialPath );
		var textureWorldSize = MaterialScaleOrFallback( door.DoorFrameMaterialScale, MaterialScaleForSettings( settings, RoomLayoutSurface.DoorFrame ) );

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

	private static void BuildCorridorWindowFrame(
		Scene scene,
		GameObject frameParent,
		RoomLayoutSettings settings,
		int corridorId,
		RoomLayoutWindow window,
		CorridorBoundaryEdge edge )
	{
		if ( edge.Length < 1.0f )
		{
			return;
		}

		var sillHeight = EffectiveWindowSillHeight( settings, window );
		var openingHeight = EffectiveWindowHeight( settings, window, sillHeight );
		var jamb = Math.Clamp( settings.WindowFrameThickness, 0.1f, MathF.Max( 0.1f, edge.Length * 0.25f ) );
		var railHeight = Math.Clamp( settings.WindowFrameThickness, 0.1f, MathF.Max( 0.1f, openingHeight * 0.25f ) );
		var depth = TrimDepth( settings, settings.WindowFrameThickness );
		var materialPath = FirstMaterialPath( window.WindowFrameMaterialPath, settings.WindowFrameMaterialPath );
		var textureWorldSize = MaterialScaleOrFallback( window.WindowFrameMaterialScale, MaterialScaleForSettings( settings, RoomLayoutSurface.WindowFrame ) );
		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 leftEdge = edge.WithSpan( edge.Start, edge.Start + jamb );
		var rightEdge = edge.WithSpan( edge.End - jamb, edge.End );
		var left = CorridorInteriorTrimBox( settings, leftEdge, leftEdge.Length, depth, openingHeight, verticalCenter );
		var right = CorridorInteriorTrimBox( settings, rightEdge, rightEdge.Length, depth, openingHeight, verticalCenter );
		var sill = CorridorInteriorTrimBox( settings, edge, edge.Length, depth, railHeight, sillCenter );
		var header = CorridorInteriorTrimBox( settings, edge, edge.Length, depth, railHeight, headerCenter );

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

	private enum CorridorBoundaryOpeningKind
	{
		Door,
		Window
	}

	private readonly record struct CorridorBoundaryOpening(
		CorridorBoundaryOpeningKind Kind,
		float Start,
		float End,
		RoomLayoutDoor Door,
		RoomLayoutWindow Window );
}