Editor/Roof/ArchRoofGen.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

namespace Sunless.Architecture;

public static partial class ArchRoofGen {
	public static void Build( ArchMesh canvas, ArchRoofPart roof, ArchBuilding building, ArchPlan plan, ArchKit kit, ArchStyle style, List<ArchRunPath> gutterRuns ) {
		foreach ( var region in new ArchRoofRegionService( building, roof ).Resolve() ) {
			BuildRegion( canvas, roof, building, plan, kit, style, gutterRuns, region );
		}
	}

	static void BuildRegion( ArchMesh canvas, ArchRoofPart roof, ArchBuilding building, ArchPlan plan, ArchKit kit, ArchStyle style, List<ArchRunPath> gutterRuns, ArchRoofRegion region ) {
		var outline = region.Outer;
		var shapeStyle = roof.Style;
		var chain = new[] { roof.Palette, building.Palette };
		var deck = style.Brush( ArchSurface.Roof, chain );
		var edge = style.Brush( ArchSurface.RoofEdge, chain );
		var kinds = ArchKinds.Load();

		if ( BuiltOver( roof, building, outline ) ) {
			Interior( canvas, roof, region.Loops, building, plan, kit, style, chain, kinds );
			return;
		}

		var abutments = Abutments( roof, building, kinds );
		var junctions = ArchCrossGableService.Resolve( building, kit, kinds );
		var fittingAbutments = FittingAbutments( roof, building, plan, kit, junctions, kinds );
		var overhang = ArchRoofPlane.Overhang( roof );
		var thickness = ArchRoofPlane.Thickness( roof, kit );

		bool Abuts( Vector2 point, Vector2 outward ) => Buried( abutments, point + outward * ArchProbe.Step );
		bool FittingAbuts( Vector2 point, Vector2 outward ) => Buried( fittingAbutments, point + outward * ArchProbe.Step );

		bool Bare( Vector2 point, Vector2 outward ) => FittingAbuts( point, outward ) || !Sheds( roof, shapeStyle, outward );

		// Only a taller roof removes the slot — a level deck laps into closure instead
		bool Slotless( Vector2 point, Vector2 outward ) => Abuts( point, outward ) || !Sheds( roof, shapeStyle, outward );

		var drip = ArchRoofPlane.Drip( roof, kit );
		var rafters = ArchFootprint.Subtract( ArchFootprint.Grow( region.Loops, overhang + drip ), abutments );
		var eaves = ArchFootprint.Subtract( ArchFootprint.Grow( region.Loops, overhang ), fittingAbutments );
		// Notched like eaves so partial abutments don't strip the whole run
		var soffits = ArchFootprint.Subtract( region.Loops, fittingAbutments );
		var plates = ArchFootprint.Subtract( region.Loops, abutments );

		if ( rafters.Count == 0 ) {
			return;
		}

		Bounds( rafters, out var deckMin, out var deckMax );

		switch ( shapeStyle ) {
			case RoofStyle.Flat:
				using ( canvas.Part( ArchPieces.Deck ) ) {
					Deck( canvas, roof, building, rafters, deckMin, deckMax, thickness, plan, kit, style, chain, deck );
				}

				if ( roof.RidgeCap ) {
					using ( canvas.Part( ArchPieces.Capping ) ) {
						Capping( canvas, Abutted( rafters, Abuts, roof.BaseHeight ), thickness, kit, edge, plan, roof.Level, building.Id );
					}
				}
				break;

			case RoofStyle.Hip:
				var shape = ArchRoofSkeleton.Shape( rafters, Abuts, roof.BaseHeight, ArchRoofPlane.Slope( roof ) );
				var faceted = LitFacets( roof );

				using ( canvas.Part( ArchPieces.Deck ) ) {
					JoinedSlab( canvas, Pierced( shape.Faces, roof, building, kit, thickness, plan, faceted ), Vector3.Up * thickness, deck );
				}

				if ( roof.RidgeCap ) {
					using ( canvas.Part( ArchPieces.Capping ) ) {
						Capping( canvas, shape.Creases, thickness, kit, edge, plan, roof.Level, building.Id );
						Capping( canvas, shape.Flashing, thickness, kit, edge, plan, roof.Level, building.Id );
					}
				}

				foreach ( var piece in faceted ) {
					using ( canvas.Part( piece.Light.Form.Piece() ) ) {
						Light( canvas, piece, thickness, kit, style, chain );
					}
				}
				break;

			case RoofStyle.Shed:
			case RoofStyle.Gable:
				using ( canvas.Part( ArchPieces.Deck ) ) {
					Deck( canvas, roof, building, rafters, deckMin, deckMax, thickness, plan, kit, style, chain, deck );
				}
				break;

			case RoofStyle.Sawtooth:
				using ( canvas.Part( ArchPieces.Deck ) ) {
					Sawtooth( canvas, roof, rafters, deckMin, deckMax, thickness, deck, edge );
				}
				break;
		}

		if ( roof.Frame ) {
			using ( canvas.Part( ArchPieces.Frame ) ) {
				Frame( canvas, roof, rafters, kit, style.Brush( ArchSurface.RoofFrame, chain ) );
			}
		}

		if ( roof.EndWalls ) {
			var walls = style.Brush( ArchSurface.WallExterior, chain );

			using ( canvas.Part( ArchPieces.EndWalls ) ) {
				EndWalls( canvas, roof, deckMin, deckMax, region.Loops, kit, walls, abutments, junctions );
				Closure( canvas, roof, plates, kit, walls, overhang + drip, Slotless, plan, building.Id );
			}
		}

		if ( roof.Soffit && overhang > 0.5f ) {
			using ( canvas.Part( ArchPieces.Gutters ) )
			using ( canvas.Part( ArchPieces.Soffit ) ) {
				Soffit( canvas, roof, soffits, kit, style.Brush( ArchSurface.Soffit, chain ), Bare, plan, building.Id );
			}
		}

		if ( roof.Fascia ) {
			using ( canvas.Part( ArchPieces.Gutters ) )
			using ( canvas.Part( ArchPieces.Fascia ) ) {
				Fascia( canvas, roof, eaves, kit, edge, Bare, plan, building.Id );
			}
		}

		if ( roof.Parapet ) {
			using ( canvas.Part( ArchPieces.Parapet ) ) {
				Parapet( canvas, roof, outline, kit, style.Brush( ArchSurface.WallExterior, chain ), style.Brush( ArchSurface.WallCap, chain ), Abuts, plan, building.Id );
			}
		}

		using ( canvas.Part( ArchPieces.Cornice ) ) {
			Crown( canvas, roof, outline, kit, style, chain, Abuts, plan, building.Id );
		}

		if ( roof.Guardrail ) {
			using ( canvas.Part( ArchPieces.Guardrail ) ) {
				Guardrail( canvas, roof, outline, kit, style, chain, Abuts );
			}
		}

		Interior( canvas, roof, region.Loops, building, plan, kit, style, chain, kinds );

		if ( gutterRuns is not null ) {
			foreach ( var run in GutterRuns( roof, building, kit, plan, region ) ) {
				gutterRuns.AddRange( ArchCut.Runs( plan, kit, roof.Level, building.Id, run, ArchCutAffects.Gutters ) );
			}
		}
	}

	static void Interior( ArchMesh canvas, ArchRoofPart roof, List<List<Vector2>> region, ArchBuilding building, ArchPlan plan, ArchKit kit, ArchStyle style, ArchPalette[] chain, ArchKinds kinds ) {
		if ( !roof.Ceiling && !roof.VaultedCeiling ) {
			return;
		}

		if ( roof.VaultedCeiling ) {
			using ( canvas.Part( ArchPieces.Ceiling ) ) {
				ArchVaultGen.Build( canvas, roof, kit, style );
			}

			return;
		}

		// Ceiling top laps into the deck to avoid a coplanar pair at the eave line
		var head = roof.BaseHeight + ArchLap.Bite( kit );
		var ceiling = roof.BaseHeight - kit.FloorThickness;
		var surface = roof.LoftFloor ? ArchSurface.Floor : ArchSurface.Ceiling;
		var (bitten, recesses) = ArchFloorGen.Bites( plan, roof.Level + 1, kit, ceiling, head, building.Id, ArchCutAffects.Roofs, roof.Id );

		var wells = ArchCut.Stored( building, roof.Level + 1 )
			.Concat( bitten )
			.Where( cutout => Reaches( region, cutout ) )
			.ToList();

		var handed = wells.Concat( HandedOver( roof, building ) ).ToList();

		using ( canvas.Part( ArchPieces.Ceiling ) ) {
			ArchFloorGen.Solid( canvas, region, handed, ceiling, head, style.Brush( surface, chain ), recesses );
			ArchFloorGen.WellTrim( canvas, wells, ceiling, head, kit, style.Brush( ArchSurface.Trim, chain ),
				ArchWellEdges.Trimmed( building, kit, wells, head ),
				building.Rooms
					.SelectMany( room => plan.Filed( ArchKind.Wall, room, kinds ) )
					.OfType<ArchWall>()
					.ToList(), region );
		}

		using ( canvas.Part( ArchPieces.Ceiling ) )
		using ( canvas.Part( ArchPieces.Cornice ) ) {
			foreach ( var outline in region ) {
				if ( roof.Walkway ) {
					WalkwayCornice( canvas, roof, wells, kit, ceiling, style.Brush( ArchSurface.Trim, chain ) );
					break;
				}

				ArchCornice.Around( canvas, outline, wells, kit, ceiling, style.Brush( ArchSurface.Trim, chain ) );
			}
		}
	}

	static IEnumerable<ArchFloorCutout> HandedOver( ArchRoofPart roof, ArchBuilding building ) {
		if ( ArchFloorGen.RoofKeepsCeiling( roof ) ) {
			yield break;
		}

		foreach ( var room in building.Rooms.Where( room => room.Floor == roof.Level && room.HasCeiling ) ) {
			var footprint = ArchFloorGen.Footprint( room );

			if ( footprint.Count < 3 ) {
				continue;
			}

			var handed = new ArchFloorCutout { Level = roof.Level, Name = room.Name };

			handed.Reshape( footprint );

			yield return handed;
		}
	}

	static bool Reaches( IReadOnlyList<List<Vector2>> region, ArchFloorCutout cutout ) {
		foreach ( var loop in region ) {
			ArchFootprint.Bounds( loop, out var min, out var max );

			if ( ArchFootprint.Overlaps(
					new ArchBox { Min = cutout.Min, Max = cutout.Max },
					new ArchBox { Min = min, Max = max } ) ) {
				return true;
			}
		}

		return false;
	}

	static bool BuiltOver( ArchRoofPart roof, ArchBuilding building, IReadOnlyList<Vector2> outline ) {
		return ArchRegion.Covers( ArchRegion.Storey( building, roof.Level + 1 ), outline );
	}

	static List<List<Vector2>> Abutments( ArchRoofPart roof, ArchBuilding building, ArchKinds kinds ) {
		return ArchPlanStore.FiledOn( ArchKind.Roof, building, kinds )
			.OfType<ArchRoofPart>()
			.Where( other => !ReferenceEquals( other, roof ) && other.BaseHeight > roof.BaseHeight + 1f )
			.Select( other => ArchFootprint.Wind( other.Outline() ) )
			.ToList();
	}

	// Level-or-higher decks block fittings even though the eave itself continues
	static List<List<Vector2>> Blocking( ArchRoofPart roof, ArchBuilding building, ArchPlan plan, ArchKinds kinds ) {
		// A ghosted roof stands on a building the plan does not hold, so its neighbours are that building's alone.
		var standing = plan is not null
			? plan.AllRoofs( kinds )
			: ArchPlanStore.FiledOn( ArchKind.Roof, building, kinds ).OfType<ArchRoofPart>();

		return standing
			.Where( other => !ReferenceEquals( other, roof ) && other.BaseHeight > roof.BaseHeight - 1f )
			.Select( other => ArchFootprint.Wind( other.Outline() ) )
			.ToList();
	}

	// Padded off centreline by half-thickness so fittings stop at the wall face, not behind it
	static List<List<Vector2>> FittingAbutments(
		ArchRoofPart roof,
		ArchBuilding building,
		ArchPlan plan,
		ArchKit kit,
		IReadOnlyList<ArchCrossGableJunction> junctions,
		ArchKinds kinds ) {
		return ArchFootprint.Grow(
			Blocking( roof, building, plan, kinds )
				.Concat( ArchCrossGableService.FittingAbutments( roof, kit, junctions ) )
				.ToList(),
			kit.WallThickness * 0.5f + ArchLap.Bite( kit ) );
	}

	static bool Buried( List<List<Vector2>> abutments, Vector2 point ) {
		return abutments.Any( loop => ArchFloorGen.Contains( loop, point ) );
	}

	static bool Sheds( ArchRoofPart roof, RoofStyle style, Vector2 outward ) {
		if ( style == RoofStyle.Gable ) {
			return MathF.Abs( roof.RidgeAlongX ? outward.y : outward.x ) > 0.5f;
		}

		if ( style != RoofStyle.Shed ) {
			return true;
		}

		var fall = roof.Reversed ? 1f : -1f;

		return (roof.RidgeAlongX ? outward.y * fall : outward.x * fall) > 0.5f;
	}

	static bool Rectangular( ArchRoofRegion region ) {
		return Rectangular( region.Loops );
	}

	static bool Rectangular( IReadOnlyList<List<Vector2>> loops ) {
		return loops.Count == 1 && loops[0].Count == 4;
	}

	static void Bounds( List<List<Vector2>> loops, out Vector2 min, out Vector2 max ) {
		ArchFootprint.Bounds( loops.SelectMany( loop => loop ).ToList(), out min, out max );
	}

	static Vector3 Plan( bool alongX, float span, float across, float height ) {
		return alongX ? new Vector3( across, span, height ) : new Vector3( span, across, height );
	}

	static Vector3 AcrossAxis( bool alongX ) {
		return alongX ? new Vector3( 1f, 0f, 0f ) : new Vector3( 0f, 1f, 0f );
	}

	static Vector3 SpanAxis( bool alongX ) {
		return alongX ? new Vector3( 0f, 1f, 0f ) : new Vector3( 1f, 0f, 0f );
	}

	static void JoinedSlab(
		ArchMesh canvas,
		IReadOnlyList<List<Vector3>> faces,
		Vector3 lift,
		ArchBrush brush,
		IReadOnlyList<(Vector3 From, Vector3 To)> openEdges = null ) {
		if ( lift.IsNearZeroLength ) {
			return;
		}

		var boundaries = new Dictionary<(long, long, long, long, long, long), (Vector3 From, Vector3 To, int Count)>();
		var openings = openEdges?
			.Select( edge => EdgeKey( edge.From, edge.To ) )
			.ToHashSet() ?? new HashSet<(long, long, long, long, long, long)>();

		using ( canvas.Welding() ) {
			foreach ( var face in faces ) {
				var corners = ArchSlab.Cleaned( face );

				if ( corners.Count < 3 ) {
					continue;
				}

				if ( Vector3.Dot( ArchMesh.Newell( corners ), lift ) < 0f ) {
					corners.Reverse();
				}

				canvas.Polygon( corners, brush, true );
				canvas.Polygon( corners.Select( point => point + lift ).ToList(), brush );

				for ( var index = 0; index < corners.Count; index++ ) {
					var from = corners[index];
					var to = corners[(index + 1) % corners.Count];
					var key = EdgeKey( from, to );

					if ( boundaries.TryGetValue( key, out var boundary ) ) {
						boundaries[key] = (boundary.From, boundary.To, boundary.Count + 1);
						continue;
					}

					boundaries[key] = (from, to, 1);
				}
			}

			foreach ( var boundary in boundaries.Values.Where( boundary => boundary.Count == 1 ) ) {
				if ( openings.Contains( EdgeKey( boundary.From, boundary.To ) ) ) {
					continue;
				}

				canvas.Quad( boundary.From, boundary.To, boundary.To + lift, boundary.From + lift, brush );
			}
		}
	}

	static (long, long, long, long, long, long) EdgeKey( Vector3 first, Vector3 second ) {
		var a = PointKey( first );
		var b = PointKey( second );

		if ( ComesAfter( a, b ) ) {
			(a, b) = (b, a);
		}

		return (a.X, a.Y, a.Z, b.X, b.Y, b.Z);
	}

	static (long X, long Y, long Z) PointKey( Vector3 point ) {
		var size = ArchGridService.FinestSize;

		return (
			(long)MathF.Round( point.x / size ),
			(long)MathF.Round( point.y / size ),
			(long)MathF.Round( point.z / size ));
	}

	static bool ComesAfter( (long X, long Y, long Z) first, (long X, long Y, long Z) second ) {
		if ( first.X != second.X ) {
			return first.X > second.X;
		}

		if ( first.Y != second.Y ) {
			return first.Y > second.Y;
		}

		return first.Z > second.Z;
	}
}