Editor/Platform/ArchPlatformGen.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

namespace Sunless.Architecture;

public static class ArchPlatformGen {
	public static void Build(
		ArchMesh canvas,
		ArchPlatformPart platform,
		ArchBuilding building,
		ArchPlan plan,
		ArchKit kit,
		ArchStyle style ) {
		var shape = Resolve( platform, building, plan, kit );

		if ( shape.Faces.Count == 0 ) {
			return;
		}

		var chain = new[] { platform.Palette, building.Palette };
		var skirt = style.Brush( ArchSurface.Foundation, chain );

		var brushes = new Dictionary<ArchCarveSide, ArchBrush> {
			[ArchCarveSide.Top] = style.Brush( ArchSurface.Deck, chain ),
			[ArchCarveSide.Sill] = style.Brush( ArchSurface.StairTread, chain ),
			[ArchCarveSide.Jamb] = style.Brush( ArchSurface.StairRiser, chain )
		};

		if ( platform.Hung ) {
			brushes[ArchCarveSide.Bottom] = style.Brush( ArchSurface.Soffit, chain );
		}

		Skin( canvas, shape, Base( platform, kit ), platform.Hung, brushes, skirt );

		using ( canvas.Part( ArchPieces.Coping ) ) {
			Coping( canvas, shape, platform, building, kit, style.Brush( ArchSurface.WallCap, chain ) );
		}

		using ( canvas.Part( ArchPieces.Guardrail ) ) {
			foreach ( var guard in Guardrails( platform, building, plan, kit ) ) {
				ArchBarrierGen.Build( canvas, guard.Shape, guard.Spec, kit, ArchBarrierBrushes.Of( style, chain ), guard.Doubled() );
			}
		}
	}

	public static List<ArchGuardRun> Guardrails( ArchPlatformPart platform, ArchBuilding building, ArchPlan plan, ArchKit kit ) {
		var guards = new List<ArchGuardRun>();

		if ( !platform.Guardrail || ArchRamp.Rakes( platform ) ) {
			return guards;
		}

		var seat = platform.TopHeight;
		var mouths = Mouths( platform, building, kit );
		var notches = Notches( platform, building, plan, kit, seat );

		bool Abuts( Vector2 point, Vector2 outward ) {
			var probe = point + outward * ArchProbe.Step;

			return mouths.Any( mouth => ArchFootprint.Contains( mouth, probe ) );
		}

		var region = notches.Count == 0
			? new List<List<Vector2>> { platform.Outline() }
			: ArchFootprint.Subtract( new List<List<Vector2>> { platform.Outline() }, notches );

		foreach ( var loop in ArchFootprint.Outer( region ) ) {
			foreach ( var run in ArchBrokenRun.Of( ArchFootprint.Wind( loop ), Abuts ).Level( seat ).Resolve() ) {
				var spec = ArchGuards.Spec( MathF.Max( 12f, platform.GuardHeight ), kit, run, platform.GuardStyle );
				var curve = ArchCurve.Polyline( run.Raised(), run.Closed );

				guards.Add( new ArchGuardRun {
					Shape = ArchBarrierShape.Resolve( curve, spec, 0f, 0f, kit ),
					Spec = spec,
					Closed = run.Closed,
					Length = curve.Length
				} );
			}
		}

		return guards;
	}

	static List<List<Vector2>> Notches( ArchPlatformPart platform, ArchBuilding building, ArchPlan plan, ArchKit kit, float seat ) {
		var notches = new List<List<Vector2>>();

		foreach ( var stair in ArchLayerGate.Enabled( platform.Stairs ) ) {
			if ( stair.Core is not { } core ) {
				continue;
			}

			foreach ( var lane in ArchStairShape.Standing( stair, core ) ) {
				notches.Add( lane.Loop( core ) );
			}
		}

		foreach ( var cut in ArchCut.Over( plan, platform.Level, platform.Outline(), building?.Id ?? 0, ArchCutAffects.Platforms, platform.Id ) ) {
			foreach ( var volume in ArchCut.Resolve( cut, kit ).Where( volume => ArchCut.Reaches( volume, seat - 1f, seat ) ) ) {
				notches.Add( volume.Footprint.ToList() );
			}
		}

		return notches;
	}

	static List<List<Vector2>> Mouths( ArchPlatformPart platform, ArchBuilding building, ArchKit kit ) {
		var mouths = new List<List<Vector2>>();

		foreach ( var stair in ArchLayerGate.Enabled( platform.Stairs ) ) {
			foreach ( var mouth in ArchHostCarvings.Mouths( platform, stair, building, kit ) ) {
				mouths.Add( mouth );
			}
		}

		return mouths;
	}

	// Sunk below grade so the slab edge doesn't show daylight
	public static float Base( ArchPlatformPart platform, ArchKit kit ) {
		return platform.Hung ? platform.GradeHeight : platform.GradeHeight - ArchGround.Embedment( kit );
	}

	public static ArchCarveShape Resolve( ArchPlatformPart platform, ArchBuilding building, ArchPlan plan, ArchKit kit ) {
		var outline = platform.Outline();
		var baseHeight = Base( platform, kit );
		var cuts = ArchCut.Over( plan, platform.Level, outline, building?.Id ?? 0, ArchCutAffects.Platforms, platform.Id )
			.Where( cut => ArchCut.Resolve( cut, kit ).Any( volume => ArchCut.Reaches( volume, baseHeight, platform.TopHeight ) ) )
			.ToList();
		var carve = Body( platform, outline, baseHeight ).In( Yaw( platform, cuts ) );

		foreach ( var stair in ArchLayerGate.Enabled( platform.Stairs ) ) {
			foreach ( var volume in ArchHostCarvings.Volumes( platform, stair, building, kit ) ) {
				carve.Less( volume );
			}
		}

		foreach ( var cut in cuts ) {
			foreach ( var volume in ArchCut.Resolve( cut, kit ) ) {
				carve.Less( volume );
			}
		}

		return carve.Resolve();
	}

	static ArchCarve Body( ArchPlatformPart platform, IReadOnlyList<Vector2> outline, float baseHeight ) {
		return ArchRamp.Rakes( platform )
			? ArchCarve.Wedge( outline, baseHeight, ArchRamp.Deck( platform ) )
			: ArchCarve.Prism( outline, baseHeight, platform.TopHeight );
	}

	static float Yaw( ArchPlatformPart platform, IReadOnlyList<ArchCutPart> cuts ) {
		var stepped = cuts.FirstOrDefault( cut => cut.Profile == CutProfile.Steps && cut.HasContent );

		if ( stepped is not null ) {
			return stepped.Segments[0].Yaw;
		}

		var flight = ArchLayerGate.Enabled( platform.Stairs ).FirstOrDefault( stair => MathF.Abs( stair.Yaw % 90f ) > 0.01f );

		return flight?.Yaw ?? 0f;
	}

	static void Skin(
		ArchMesh canvas,
		ArchCarveShape shape,
		float underside,
		bool hung,
		Dictionary<ArchCarveSide, ArchBrush> brushes,
		ArchBrush skirt ) {
		using var welding = canvas.Welding();

		foreach ( var face in shape.Faces ) {
			if ( !hung && face.Side == ArchCarveSide.Bottom && face.Lowest - underside < 0.05f ) {
				continue;
			}

			canvas.Polygon( face.Points, brushes.TryGetValue( face.Side, out var brush ) ? brush : skirt );
		}
	}

	static void Coping( ArchMesh canvas, ArchCarveShape shape, ArchPlatformPart platform, ArchBuilding building, ArchKit kit, ArchBrush brush ) {
		if ( !platform.Coping || platform.CopingHeight < 0.05f || ArchRamp.Rakes( platform ) ) {
			return;
		}

		var top = platform.TopHeight;

		// Bitten into the deck so it laps correctly
		var band = ArchBandSection.Between(
			-MathF.Max( 1f, platform.CopingWidth ),
			MathF.Max( 0f, platform.CopingOversail ),
			top - ArchLap.Bite( kit ),
			top + platform.CopingHeight );

		var mouths = ApproachMouths( platform, building, kit );

		foreach ( var loop in Coped( shape.Outline( top ), platform ) ) {
			foreach ( var run in Parted( loop, mouths, kit ) ) {
				run.Level( top ).Emit( canvas, band, brush );
			}
		}
	}

	// Coping breaks over approaches that land on this deck
	static List<IReadOnlyList<Vector2>> ApproachMouths( ArchPlatformPart platform, ArchBuilding building, ArchKit kit ) {
		var mouths = new List<IReadOnlyList<Vector2>>();
		var bite = ArchLap.Bite( kit );

		// Past the course both ways to avoid coplanar pairs at the podium edge
		var inward = MathF.Max( 1f, platform.CopingWidth ) + bite;
		var garden = MathF.Max( 0f, platform.CopingOversail ) + bite;

		foreach ( var room in building?.Rooms ?? Enumerable.Empty<ArchRoom>() ) {
			foreach ( var approach in room.Approaches ) {
				if ( approach.PlatformId != platform.Id || !ArchLayerGate.On( approach ) ) {
					continue;
				}

				mouths.Add( ArchApproachGen.Mouth( approach, kit, garden, inward, 0f ).Outline() );
			}
		}

		return mouths;
	}

	// Open runs parted at mouths, not re-routed around them
	static IEnumerable<ArchRun> Parted( List<Vector2> loop, List<IReadOnlyList<Vector2>> mouths, ArchKit kit ) {
		if ( mouths.Count == 0 ) {
			yield return ArchRun.Around( loop );

			yield break;
		}

		foreach ( var stretch in ArchRegion.Open( loop, mouths ) ) {
			if ( stretch.Count < 2 ) {
				continue;
			}

			if ( (stretch[0] - stretch[^1]).Length < 0.05f ) {
				yield return ArchRun.Around( stretch.Take( stretch.Count - 1 ).ToList() );

				continue;
			}

			yield return ArchRun.Along( Retracted( stretch, ArchLap.Bite( kit ) ) );
		}
	}

	// Open ends retract a bite inside the solid to avoid z-fighting
	static List<Vector2> Retracted( IReadOnlyList<Vector2> stretch, float bite ) {
		var ends = stretch.ToList();

		ends[0] = Inside( ends[0], ends[1], bite );
		ends[^1] = Inside( ends[^1], ends[^2], bite );

		return ends;
	}

	static Vector2 Inside( Vector2 end, Vector2 towards, float bite ) {
		var span = towards - end;
		var length = span.Length;

		return length < bite * 3f ? end : end + span / length * bite;
	}

	static IEnumerable<List<Vector2>> Coped( List<List<Vector2>> region, ArchPlatformPart platform ) {
		return platform.CopingInside ? region : ArchFootprint.Outer( region );
	}
}