Editor/Output/ArchLocate.Extents.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

namespace Sunless.Architecture;

public static partial class ArchLocate {
	static ArchExtent BuildingExtent( ArchBuilding building, ArchKit kit, int? level = null ) {
		var extent = new ArchExtent();

		foreach ( var room in building.Rooms.Where( room => !level.HasValue || room.Floor == level ) ) {
			extent.Add( RoomExtent( room, kit ) );
		}

		foreach ( var roof in building.Roofs.Where( roof => !level.HasValue || roof.Level == level ) ) {
			extent.Add( RoofExtent( roof ) );
		}

		foreach ( var fence in building.Fences ) {
			extent.Add( FenceExtent( fence ) );
		}

		return extent;
	}

	public static ArchExtent RoomExtent( ArchRoom room, ArchKit kit ) {
		var extent = new ArchExtent();
		var providers = ArchExtents.Load();
		var top = room.BaseHeight + ArchFloorGen.WallHeight( room, kit );

		extent.Add( ArchFloorGen.Footprint( room ), room.BaseHeight - kit.FloorThickness, top );

		foreach ( var wall in room.Walls ) {
			extent.Add( WallExtent( wall, room, kit ) );
		}

		foreach ( var stair in room.Stairs ) {
			extent.Add( providers.Measure( ArchKind.Stair, stair,
				new ArchExtentContext( null, kit, room, null ) ) );
		}

		foreach ( var porch in room.Porches ) {
			extent.Add( providers.Measure( ArchKind.Porch, porch,
				new ArchExtentContext( null, kit, room, null ) ) );
		}

		return extent;
	}

	static ArchExtent WallExtent( ArchWall wall, ArchRoom room, ArchKit kit ) {
		var extent = new ArchExtent();
		var half = WallThickness( wall, kit ) * 0.5f;
		var side = wall.Normal * half;
		var top = room.BaseHeight + WallHeight( wall, room, kit );

		extent.Add( wall.Start + side, room.BaseHeight, top );
		extent.Add( wall.Start - side, room.BaseHeight, top );
		extent.Add( wall.End + side, room.BaseHeight, top );
		extent.Add( wall.End - side, room.BaseHeight, top );

		return extent;
	}

	static ArchExtent OpeningExtent( ArchOpening opening, ArchWall wall, ArchRoom room, ArchKit kit ) {
		var extent = new ArchExtent();
		var half = WallThickness( wall, kit ) * 0.5f;
		var side = wall.Normal * half;
		var bottom = room.BaseHeight + opening.SillHeight;
		var top = room.BaseHeight + opening.Top;

		foreach ( var along in new[] { opening.Left, opening.Right } ) {
			var point = wall.PointAt( along );

			extent.Add( point + side, bottom, top );
			extent.Add( point - side, bottom, top );
		}

		return extent;
	}

	static ArchExtent RoofExtent( ArchRoofPart roof ) {
		var extent = new ArchExtent();
		var outline = roof.Outline();
		var span = MathF.Max( roof.Size.x, roof.Size.y ) * 0.5f;
		var rise = roof.Style is RoofStyle.Flat ? roof.ParapetHeight : span * ArchPitch.Slope( roof.Pitch );

		extent.Add( ArchFootprint.Grow( new List<List<Vector2>> { ArchFootprint.Wind( outline ) }, roof.Overhang )
			.SelectMany( loop => loop ), roof.BaseHeight - 24f, roof.BaseHeight + rise + roof.Thickness );

		return extent;
	}

	static ArchExtent PlatformExtent( ArchPlatformPart platform ) {
		var extent = new ArchExtent();

		// A ramp wears no coping, so counting one in leaves the audit reporting an envelope nothing ever fills.
		var band = ArchRamp.Rakes( platform ) ? 0f : platform.CopingHeight;

		extent.Add( platform.Outline(), platform.GradeHeight, platform.TopHeight + band );

		return extent;
	}

	static ArchExtent BeamExtent( ArchBeamPart beam ) {
		var extent = new ArchExtent();

		extent.Add( beam.Outline(), beam.Soffit, beam.TopHeight );

		return extent;
	}

	// Off the same resolve the generator reads, so a run trimmed to nothing at a corner reports the empty extent
	// it really has rather than the stretch it was drawn over.
	static ArchExtent CabinetExtent( ArchPlan plan, ArchKit kit, ArchRoom room, ArchBuilding building, ArchCabinetPart part ) {
		return ArchExtents.Load().Measure( ArchKind.Cabinet, part, new ArchExtentContext( plan, kit, room, building ) );
	}

	static ArchExtent CutExtent( ArchCutPart cut ) {
		var extent = new ArchExtent();

		foreach ( var segment in cut.Segments.Where( leg => leg.Length > 1f ) ) {
			extent.Add( segment.Outline(), segment.BaseHeight, segment.TopHeight );
		}

		return extent;
	}

	static ArchExtent DownpipeExtent( ArchDownpipePart pipe ) {
		var extent = new ArchExtent();

		extent.Add( pipe.Outlet, pipe.BaseHeight, pipe.TopHeight );
		extent.Add( pipe.Wall, pipe.BaseHeight, pipe.TopHeight );

		return extent.Grown( 6f );
	}

	// The line between the ends - the elevation a road is actually read from.
	static float RoadYaw( ArchRoadPart road ) {
		if ( road.Nodes.Count < 2 ) {
			return 0f;
		}

		var run = road.Nodes[^1].Position - road.Nodes[0].Position;

		return MathF.Atan2( run.y, run.x ).RadianToDegree();
	}

	static ArchExtent FenceExtent( ArchFencePart fence ) {
		var extent = new ArchExtent();
		var top = fence.Barrier.Height + fence.Barrier.PostOverhang + fence.Barrier.DropperLength;

		foreach ( var node in fence.Nodes ) {
			extent.Add( node.Position );
			extent.Add( node.Position + Vector3.Up * top );
		}

		return extent.Grown( 4f );
	}

	static ArchExtent TrimExtent( ArchTrimPart trim ) {
		var extent = new ArchExtent();

		foreach ( var point in trim.Path ) {
			extent.Add( point );
		}

		return extent.Grown( 8f );
	}
}