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

namespace ReusableRoomLayout;

public sealed partial class RoomLayoutGeometryBuilder
{
	private static void BuildCorridor(
		Scene scene,
		GameObject frameParent,
		RoomLayoutDocument document,
		RoomLayoutCorridor corridor,
		ICollection<RoomLayoutFloorSlab> floorSlabs,
		ICollection<RoomLayoutFloorSlab> roofSlabs,
		ICollection<RoomLayoutThresholdSlab> thresholdSlabs,
		ICollection<RoomLayoutSolidSlab> solidSlabs )
	{
		if ( !document.CorridorDoorsAreOnFloor( corridor ) )
		{
			return;
		}

		if ( !TryGetDoorPoint( document, corridor.StartDoorId, out var start ) ||
			!TryGetDoorPoint( document, corridor.EndDoorId, out var end ) )
		{
			return;
		}

		var settings = document.Settings;
		var width = CorridorClearWidth( settings, corridor, start, end );
		var floorMaterialPath = CorridorMaterialPath( settings, corridor, RoomLayoutSurface.CorridorFloor );
		var outerWallMaterialPath = CorridorOuterWallMaterialPath( settings, corridor );
		var innerWallMaterialPath = CorridorInnerWallMaterialPath( settings, corridor );
		var capMaterialPath = CorridorMaterialPath( settings, corridor, RoomLayoutSurface.Cap );
		var baseboardMaterialPath = CorridorMaterialPath( settings, corridor, RoomLayoutSurface.Baseboard );
		var roofMaterialPath = CorridorMaterialPath( settings, corridor, RoomLayoutSurface.Roof );
		var floorMaterialScale = CorridorMaterialScale( settings, corridor, RoomLayoutSurface.CorridorFloor );
		var outerWallMaterialScale = CorridorOuterWallMaterialScale( settings, corridor );
		var innerWallMaterialScale = CorridorInnerWallMaterialScale( settings, corridor );
		var capMaterialScale = CorridorMaterialScale( settings, corridor, RoomLayoutSurface.Cap );
		var baseboardMaterialScale = CorridorMaterialScale( settings, corridor, RoomLayoutSurface.Baseboard );
		var roofMaterialScale = CorridorMaterialScale( settings, corridor, RoomLayoutSurface.Roof );
		var footprintRects = new List<RoomLayoutRect>();

		BuildDoorThreshold(
			floorSlabs,
			roofSlabs,
			thresholdSlabs,
			settings,
			corridor.Id,
			"Start",
			start,
			width,
			floorMaterialPath,
			floorMaterialScale,
			roofMaterialPath,
			roofMaterialScale,
			ThresholdMaterialPath( document, corridor, start ),
			ThresholdMaterialScale( document, corridor, start ) );
		BuildDoorThreshold(
			floorSlabs,
			roofSlabs,
			thresholdSlabs,
			settings,
			corridor.Id,
			"End",
			end,
			width,
			floorMaterialPath,
			floorMaterialScale,
			roofMaterialPath,
			roofMaterialScale,
			ThresholdMaterialPath( document, corridor, end ),
			ThresholdMaterialScale( document, corridor, end ) );

		var points = CorridorPath( settings, corridor, start, end, width );

		for ( var i = 0; i < points.Count - 1; i++ )
		{
			BuildCorridorSegmentFloor(
				floorSlabs,
				roofSlabs,
				settings,
				footprintRects,
				corridor.Id,
				i,
				points[i],
				points[i + 1],
				width,
				floorMaterialPath,
				floorMaterialScale,
				roofMaterialPath,
				roofMaterialScale );
		}

		BuildCorridorBendFloors( floorSlabs, roofSlabs, settings, footprintRects, corridor.Id, points, width, floorMaterialPath, floorMaterialScale, roofMaterialPath, roofMaterialScale );
		BuildCorridorOpeningFloors( floorSlabs, roofSlabs, settings, document, corridor, points, width, floorMaterialPath, floorMaterialScale, roofMaterialPath, roofMaterialScale );
		BuildCorridorBoundaryWalls(
			scene,
			frameParent,
			document,
			solidSlabs,
			settings,
			corridor,
			points,
			footprintRects,
			start,
			end,
			width,
			outerWallMaterialPath,
			innerWallMaterialPath,
			capMaterialPath,
			baseboardMaterialPath,
			outerWallMaterialScale,
			innerWallMaterialScale,
			capMaterialScale,
			baseboardMaterialScale );
	}

	private static List<Vector2> CorridorPath(
		RoomLayoutSettings settings,
		RoomLayoutCorridor corridor,
		DoorPoint start,
		DoorPoint end,
		float width )
	{
		var startPortal = CorridorPortalPoint( start );
		var endPortal = CorridorPortalPoint( end );
		var startApproach = CorridorApproachPoint( settings, start, width );
		var endApproach = CorridorApproachPoint( settings, end, width );
		var points = new List<Vector2>();

		if ( corridor.ManualPath )
		{
			AddCorridorPoint( points, startPortal );
			AddCorridorPoint( points, startApproach );
			foreach ( var bend in corridor.BendPoints )
			{
				AddCorridorPoint( points, bend.Vector );
			}

			AddCorridorPoint( points, endApproach );
			AddCorridorPoint( points, endPortal );
			SimplifyCorridorPath( points );
			return points;
		}

		AddCorridorPoint( points, startPortal );
		AddCorridorPoint( points, startApproach );
		foreach ( var bend in corridor.BendPoints )
		{
			AddCorridorPoint( points, bend.Vector );
		}

		if ( corridor.BendPoints.Count == 0 && !startApproach.x.AlmostEqual( endApproach.x ) && !startApproach.y.AlmostEqual( endApproach.y ) )
		{
			AddCorridorPoint( points, new Vector2( endApproach.x, startApproach.y ) );
		}

		AddCorridorPoint( points, endApproach );
		AddCorridorPoint( points, endPortal );
		SimplifyCorridorPath( points );
		return points;
	}

	private static Vector2 CorridorPortalPoint( DoorPoint door )
	{
		return door.Point;
	}

	private static Vector2 CorridorApproachPoint( RoomLayoutSettings settings, DoorPoint door, float width )
	{
		return door.Point + door.Normal * (width * 0.5f + settings.WallThickness);
	}

	private static void AddCorridorPoint( List<Vector2> points, Vector2 point )
	{
		if ( points.Count == 0 || Vector2.DistanceBetween( points[^1], point ) > 0.5f )
		{
			points.Add( point );
		}
	}

	private static void SimplifyCorridorPath( List<Vector2> points )
	{
		for ( var i = 1; i < points.Count - 1; )
		{
			if ( IsRedundantCorridorPoint( points[i - 1], points[i], points[i + 1] ) )
			{
				points.RemoveAt( i );
				continue;
			}

			i++;
		}
	}

	private static bool IsRedundantCorridorPoint( Vector2 a, Vector2 b, Vector2 c )
	{
		if ( a.x.AlmostEqual( b.x ) && b.x.AlmostEqual( c.x ) )
		{
			return IsBetween( b.y, a.y, c.y );
		}

		return a.y.AlmostEqual( b.y ) && b.y.AlmostEqual( c.y ) && IsBetween( b.x, a.x, c.x );
	}

	private static bool IsBetween( float value, float a, float b )
	{
		return value >= MathF.Min( a, b ) - 0.5f && value <= MathF.Max( a, b ) + 0.5f;
	}

	private static void BuildCorridorSegmentFloor(
		ICollection<RoomLayoutFloorSlab> floorSlabs,
		ICollection<RoomLayoutFloorSlab> roofSlabs,
		RoomLayoutSettings settings,
		ICollection<RoomLayoutRect> footprintRects,
		int corridorId,
		int segmentIndex,
		Vector2 a,
		Vector2 b,
		float width,
		string floorMaterialPath,
		float floorMaterialScale,
		string roofMaterialPath,
		float roofMaterialScale )
	{
		if ( !TryGetCorridorSegmentRect( a, b, width, out var floorRect, out var horizontal ) )
		{
			return;
		}

		var structuralRect = CorridorSegmentStructuralRect( floorRect, settings, horizontal );
		AddCorridorFloorSlab(
			floorSlabs,
			roofSlabs,
			settings,
			footprintRects,
			$"Corridor {corridorId:00} Segment {segmentIndex:00} Floor",
			floorRect,
			structuralRect,
			floorMaterialPath,
			floorMaterialScale,
			$"Corridor {corridorId:00} Segment {segmentIndex:00} Roof",
			roofMaterialPath,
			roofMaterialScale );
	}

	private static bool TryGetCorridorSegmentRect( Vector2 a, Vector2 b, float width, out RoomLayoutRect rect, out bool horizontal )
	{
		rect = default;
		horizontal = false;
		var delta = b - a;
		if ( delta.Length < 1.0f )
		{
			return false;
		}

		horizontal = MathF.Abs( delta.x ) >= MathF.Abs( delta.y );
		var length = horizontal ? MathF.Abs( delta.x ) : MathF.Abs( delta.y );
		var center = (a + b) * 0.5f;
		rect = horizontal
			? new RoomLayoutRect( MathF.Min( a.x, b.x ), center.y - width * 0.5f, length, width )
			: new RoomLayoutRect( center.x - width * 0.5f, MathF.Min( a.y, b.y ), width, length );
		return rect.Width >= 1.0f && rect.Height >= 1.0f;
	}

	private static void BuildDoorThreshold(
		ICollection<RoomLayoutFloorSlab> floorSlabs,
		ICollection<RoomLayoutFloorSlab> roofSlabs,
		ICollection<RoomLayoutThresholdSlab> thresholdSlabs,
		RoomLayoutSettings settings,
		int corridorId,
		string suffix,
		DoorPoint door,
		float width,
		string floorMaterialPath,
		float floorMaterialScale,
		string roofMaterialPath,
		float roofMaterialScale,
		string thresholdMaterialPath,
		float thresholdMaterialScale )
	{
		var center = door.Point + door.Normal * (settings.WallThickness * 0.5f);
		var verticalWall = MathF.Abs( door.Normal.y ) > MathF.Abs( door.Normal.x );
		var floorRect = verticalWall
			? new RoomLayoutRect( center.x - width * 0.5f, center.y - settings.WallThickness * 0.5f, width, settings.WallThickness )
			: new RoomLayoutRect( center.x - settings.WallThickness * 0.5f, center.y - width * 0.5f, settings.WallThickness, width );

		AddFloorSlab( floorSlabs, $"Corridor {corridorId:00} {suffix} Threshold Floor", floorRect, floorMaterialPath, floorMaterialScale );
		if ( settings.RoofEnabled )
		{
			AddRoofSlab( roofSlabs, $"Corridor {corridorId:00} {suffix} Threshold Roof", floorRect, roofMaterialPath, roofMaterialScale );
		}

		if ( settings.ThresholdsEnabled )
		{
			var thresholdDepth = Math.Clamp( settings.ThresholdDepth, 0.5f, 512.0f );
			var thresholdRect = verticalWall
				? new RoomLayoutRect( center.x - width * 0.5f, center.y - thresholdDepth * 0.5f, width, thresholdDepth )
				: new RoomLayoutRect( center.x - thresholdDepth * 0.5f, center.y - width * 0.5f, thresholdDepth, width );
			AddThresholdSlab( thresholdSlabs, $"Corridor {corridorId:00} {suffix} Threshold", thresholdRect, thresholdMaterialPath, thresholdMaterialScale );
		}
	}

	private static void BuildCorridorBendFloors(
		ICollection<RoomLayoutFloorSlab> floorSlabs,
		ICollection<RoomLayoutFloorSlab> roofSlabs,
		RoomLayoutSettings settings,
		ICollection<RoomLayoutRect> footprintRects,
		int corridorId,
		IReadOnlyList<Vector2> points,
		float width,
		string floorMaterialPath,
		float floorMaterialScale,
		string roofMaterialPath,
		float roofMaterialScale )
	{
		for ( var i = 1; i < points.Count - 1; i++ )
		{
			var rect = new RoomLayoutRect( points[i].x - width * 0.5f, points[i].y - width * 0.5f, width, width );
			AddCorridorFloorSlab(
				floorSlabs,
				roofSlabs,
				settings,
				footprintRects,
				$"Corridor {corridorId:00} Bend {i:00} Floor",
				rect,
				StructuralFootprintRect( rect, settings ),
				floorMaterialPath,
				floorMaterialScale,
				$"Corridor {corridorId:00} Bend {i:00} Roof",
				roofMaterialPath,
				roofMaterialScale );
		}
	}

	private static void AddCorridorFloorSlab(
		ICollection<RoomLayoutFloorSlab> floorSlabs,
		ICollection<RoomLayoutFloorSlab> roofSlabs,
		RoomLayoutSettings settings,
		ICollection<RoomLayoutRect> footprintRects,
		string name,
		RoomLayoutRect rect,
		RoomLayoutRect structuralRect,
		string floorMaterialPath,
		float floorMaterialScale,
		string roofName,
		string roofMaterialPath,
		float roofMaterialScale )
	{
		AddFloorSlab( floorSlabs, name, structuralRect, floorMaterialPath, floorMaterialScale );
		if ( settings.RoofEnabled )
		{
			AddRoofSlab( roofSlabs, roofName, structuralRect, roofMaterialPath, roofMaterialScale );
		}

		footprintRects.Add( rect );
	}

	private static RoomLayoutRect CorridorSegmentStructuralRect( RoomLayoutRect rect, RoomLayoutSettings settings, bool horizontal )
	{
		var overlap = MathF.Max( 0.0f, settings.WallThickness );
		return horizontal
			? new RoomLayoutRect( rect.X, rect.Y - overlap, rect.Width, rect.Height + overlap * 2.0f )
			: new RoomLayoutRect( rect.X - overlap, rect.Y, rect.Width + overlap * 2.0f, rect.Height );
	}

	private static void BuildCorridorWallRun(
		ICollection<RoomLayoutSolidSlab> solidSlabs,
		RoomLayoutSettings settings,
		int corridorId,
		int segmentIndex,
		float centerX,
		float centerY,
		float length,
		bool horizontal,
		int sideIndex,
		string wallMaterialPath,
		string capMaterialPath )
	{
		var size = horizontal
			? new Vector3( length, settings.WallThickness, settings.WallHeight )
			: new Vector3( settings.WallThickness, length, settings.WallHeight );
		var capSize = horizontal
			? new Vector3( length, settings.WallThickness, settings.WallCapHeight )
			: new Vector3( settings.WallThickness, length, settings.WallCapHeight );

		AddSolidSlab(
			solidSlabs,
			$"Corridor {corridorId:00} Segment {segmentIndex:00} Wall {sideIndex}",
			new Vector3( centerX, centerY, settings.WallHeight * 0.5f ),
			size,
			RoomLayoutSurface.Wall,
			materialPath: wallMaterialPath );

		AddSolidSlab(
			solidSlabs,
			$"Corridor {corridorId:00} Segment {segmentIndex:00} Wall {sideIndex} Cap",
			new Vector3( centerX, centerY, settings.WallHeight + settings.WallCapHeight * 0.5f ),
			capSize,
			RoomLayoutSurface.Cap,
			materialPath: capMaterialPath );
	}

	private static void BuildCorridorBaseboardRun(
		ICollection<RoomLayoutSolidSlab> solidSlabs,
		RoomLayoutSettings settings,
		int corridorId,
		int segmentIndex,
		float centerX,
		float centerY,
		float length,
		bool horizontal,
		int direction,
		string materialPath,
		float textureWorldSize )
	{
		if ( !settings.BaseboardsEnabled ||
			length < 1.0f ||
			settings.BaseboardHeight < 0.5f ||
			settings.BaseboardDepth < 0.5f )
		{
			return;
		}

		var offset = -direction * settings.BaseboardDepth * 0.5f + direction * TrimEmbedForDepth( settings.BaseboardDepth );
		var center = horizontal
			? new Vector3( centerX, centerY + offset, settings.BaseboardHeight * 0.5f )
			: new Vector3( centerX + offset, centerY, settings.BaseboardHeight * 0.5f );
		var size = horizontal
			? new Vector3( length, settings.BaseboardDepth, settings.BaseboardHeight )
			: new Vector3( settings.BaseboardDepth, length, settings.BaseboardHeight );

		AddSolidSlab(
			solidSlabs,
			$"Corridor {corridorId:00} Segment {segmentIndex:00} Baseboard",
			center,
			size,
			RoomLayoutSurface.Baseboard,
			hasCollider: false,
			materialPath: materialPath,
			textureWorldSize: textureWorldSize );
	}

	private static bool TryGetDoorPoint( RoomLayoutDocument document, int doorId, out DoorPoint point )
	{
		point = default;
		var door = document.FindDoor( doorId );
		if ( door is null )
		{
			return false;
		}

		var room = document.FindRoom( door.RoomId );
		if ( room is null )
		{
			return false;
		}

		var bounds = room.Bounds;
		point = door.Side switch
		{
			RoomLayoutWallSide.North => new DoorPoint(
				new Vector2( bounds.X + door.Offset, bounds.Y + bounds.Height ),
				new Vector2( 0.0f, 1.0f ),
				door.Width,
				door.RoomId ),
			RoomLayoutWallSide.South => new DoorPoint(
				new Vector2( bounds.X + door.Offset, bounds.Y ),
				new Vector2( 0.0f, -1.0f ),
				door.Width,
				door.RoomId ),
			RoomLayoutWallSide.East => new DoorPoint(
				new Vector2( bounds.X + bounds.Width, bounds.Y + door.Offset ),
				new Vector2( 1.0f, 0.0f ),
				door.Width,
				door.RoomId ),
			_ => new DoorPoint(
				new Vector2( bounds.X, bounds.Y + door.Offset ),
				new Vector2( -1.0f, 0.0f ),
				door.Width,
				door.RoomId )
		};
		return true;
	}

	private static float CorridorClearWidth( RoomLayoutSettings settings, RoomLayoutCorridor corridor, DoorPoint start, DoorPoint end )
	{
		var width = corridor.Width <= 0.0f ? settings.DefaultCorridorWidth : corridor.Width;
		width = MathF.Max( 1.0f, width );

		if ( start.Width > 0.0f )
		{
			width = MathF.Min( width, start.Width );
		}

		if ( end.Width > 0.0f )
		{
			width = MathF.Min( width, end.Width );
		}

		return width;
	}

	private static float CorridorClearWidth( RoomLayoutDocument document, RoomLayoutCorridor corridor )
	{
		var width = corridor.Width <= 0.0f ? document.Settings.DefaultCorridorWidth : corridor.Width;
		width = MathF.Max( 1.0f, width );

		var startDoor = document.FindDoor( corridor.StartDoorId );
		if ( startDoor is not null && startDoor.Width > 0.0f )
		{
			width = MathF.Min( width, startDoor.Width );
		}

		var endDoor = document.FindDoor( corridor.EndDoorId );
		if ( endDoor is not null && endDoor.Width > 0.0f )
		{
			width = MathF.Min( width, endDoor.Width );
		}

		return width;
	}

	private readonly record struct DoorPoint( Vector2 Point, Vector2 Normal, float Width, int RoomId );
}