Editor/Floor/ArchFloorGen.Storey.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using Sandbox;

namespace Sunless.Architecture;

public static partial class ArchFloorGen {
	public static void Storey( ArchMesh canvas, ArchFloorGroup group, ArchBuilding building, ArchPlan plan, ArchKit kit, ArchStyle style ) {
		var lead = group.Lead;
		var chain = new[] { lead.Palette, building.Palette };
		var thickness = kit.FloorThickness;
		var trim = style.Brush( ArchSurface.Trim, chain );

		var cutouts = ArchCut.Stored( building, group.Level ).ToList();
		var walls = building.Rooms.SelectMany( room => room.Walls ).ToList();
		var above = ArchCut.Stored( building, group.Level + 1 ).ToList();

		if ( lead.HasFloor ) {
			var top = lead.BaseHeight;
			var structure = SeatedShell( group.Region, kit );

			cutouts.AddRange( ArchCut.Holes( plan, group.Level, kit, top - thickness, top, building.Id, ArchCutAffects.Floors, lead.Id ) );

			Solid( canvas, structure, cutouts, top - thickness, top, style.Brush( ArchSurface.Floor, chain ) );
			WellTrim( canvas, cutouts, top - thickness, top, kit, trim, ArchWellEdges.Trimmed( building, kit, cutouts, top ), walls, group.Region );

			if ( lead.FloorBoards ) {
				Boards( canvas, group, cutouts, kit, top, style.Brush( ArchSurface.Deck, chain ) );
			}
		}

		foreach ( var ceiling in Ceilings( group, building, kit, style ).Where( Builds( building ) ) ) {
			Ceiling( canvas, ceiling, building, plan, kit, style, above, walls );
		}
	}

	static Func<ArchFloorGroup, bool> Builds( ArchBuilding building ) {
		return group => group.Lead.HasCeiling && !CeilingHeldAbove( building, group.Lead );
	}

	static void Ceiling(
		ArchMesh canvas,
		ArchFloorGroup ceiling,
		ArchBuilding building,
		ArchPlan plan,
		ArchKit kit,
		ArchStyle style,
		List<ArchFloorCutout> above,
		List<ArchWall> walls ) {
		var lead = ceiling.Lead;
		var chain = new[] { lead.Palette, building.Palette };
		var plate = lead.BaseHeight + ceiling.WallHeight;
		var soffit = plate - CeilingDepth( lead, kit );
		var trim = style.Brush( ArchSurface.Trim, chain );

		// Lapped into whatever stands over the plate to avoid z-fighting
		var head = plate + ArchLap.Bite( kit );
		var (wells, recesses) = Bites( plan, ceiling.Level, kit, soffit, head, building.Id, ArchCutAffects.Ceilings, lead.Id );
		var body = Body( ceiling, kit );
		var ceilingBrush = style.Brush( ArchSurface.Ceiling, chain );
		var damage = ArchCut.Damage( plan, ceiling.Level, kit, soffit, head, building.Id, ArchCutAffects.Ceilings, lead.Id )
			.Where( found => found.Cut.ResolvedDamage == ArchDamageKind.SurfaceSpall || ArchDamage.Panels( found.Cut.ResolvedDamage ) )
			.ToList();

		wells.AddRange( above );

		foreach ( var (cut, volume) in damage ) {
			var damageTop = ArchDamage.Panels( cut.ResolvedDamage )
				? head + ArchLap.Bite( kit )
				: MathF.Min( head, soffit + MathF.Max( 0.5f, cut.DamageDepth ) );

			recesses.Add( ArchCarveVolume.Over( volume.Footprint, soffit - ArchLap.Bite( kit ), damageTop ) );
		}

		Solid( canvas, body, wells, soffit, head, ceilingBrush, recesses );

		foreach ( var (cut, volume) in damage ) {
			if ( ArchDamage.Displaced( cut, volume ) ) {
				ArchDamage.DisplacedPanel( canvas, cut, volume, soffit, ceilingBrush );
			}
		}

		WellTrim( canvas, wells, soffit, head, kit, trim, ArchWellEdges.Trimmed( building, kit, above, head ), walls, body );
		Cornice( canvas, ceiling, wells, kit, soffit, trim );
	}

	// Passage ceiling body held a bite short at each mouth to avoid z-fighting with the host wall
	static List<List<Vector2>> Body( ArchFloorGroup ceiling, ArchKit kit ) {
		if ( !ceiling.Lead.Spans ) {
			return SeatedShell( ceiling.Region, kit );
		}

		ArchFootprint.Bounds( Footprint( ceiling.Lead ), out var min, out var max );

		var bite = ArchLap.Bite( kit );
		var over = new Vector2( kit.WallThickness, kit.WallThickness );
		var mouths = ArchAsks.WalkwayRunsAlongX( ceiling.Lead )
			? new[]
			{
				ArchFootprint.Rect( min - over, new Vector2( min.x + bite, max.y + over.y ) ),
				ArchFootprint.Rect( new Vector2( max.x - bite, min.y - over.y ), max + over )
			}
			: new[]
			{
				ArchFootprint.Rect( min - over, new Vector2( max.x + over.x, min.y + bite ) ),
				ArchFootprint.Rect( new Vector2( min.x - over.x, max.y - bite ), max + over )
			};

		return ArchFootprint.Subtract( ceiling.Region, mouths );
	}

	static List<List<Vector2>> SeatedShell( IReadOnlyList<List<Vector2>> region, ArchKit kit ) {
		return ArchRegion.Shell( region, MathF.Max( 0f, kit.WallThickness - ArchLap.Bite( kit ) * 2f ) );
	}

	static void Cornice( ArchMesh canvas, ArchFloorGroup ceiling, List<ArchFloorCutout> wells, ArchKit kit, float soffit, ArchBrush trim ) {
		if ( !ceiling.Lead.Spans ) {
			ArchCornice.Around( canvas, ceiling.Region, wells, kit, soffit, trim );

			return;
		}

		ArchCornice.AlongPassage( canvas, ceiling.Region, ArchAsks.WalkwayRunsAlongX( ceiling.Lead ), wells, kit, soffit, trim );
	}

	static void Boards( ArchMesh canvas, ArchFloorGroup group, List<ArchFloorCutout> cutouts, ArchKit kit, float floor, ArchBrush brush ) {
		var spec = new ArchPlankSpec {
			Width = kit.FloorBoardWidth,
			Gap = kit.FloorBoardGap,
			Thickness = kit.FloorBoardThickness,
			Length = kit.FloorBoardLength,
			Top = floor + kit.FloorBoardThickness,
			Yaw = group.Lead.FloorBoardYaw
		};

		ArchPlanks.Fill( canvas, group.Region, cutouts, spec, brush );
	}

	public static void Solid(
		ArchMesh canvas,
		IReadOnlyList<List<Vector2>> region,
		IReadOnlyList<ArchFloorCutout> cutouts,
		float bottom,
		float top,
		ArchBrush brush,
		IReadOnlyList<ArchCarveVolume> recesses = null ) {
		var holes = Holes( region );

		if ( cutouts is { Count: > 0 } ) {
			holes.AddRange( cutouts );
		}

		foreach ( var loop in ArchFootprint.Outer( region ) ) {
			Slab( canvas, loop, holes, bottom, top, brush, recesses );
		}
	}

	// Splits cuts into full-through wells vs partial recesses (coffers)
	public static (List<ArchFloorCutout> Wells, List<ArchCarveVolume> Recesses) Bites(
		ArchPlan plan,
		int level,
		ArchKit kit,
		float bottom,
		float top,
		int hostId,
		ArchCutAffects target,
		int standing = 0 ) {
		var wells = new List<ArchFloorCutout>();
		var recesses = new List<ArchCarveVolume>();

		foreach ( var volume in ArchCut.Volumes( plan, level, kit, bottom, top, hostId, target, standing ) ) {
			if ( !ArchCut.Pierces( volume, bottom, top ) ) {
				recesses.Add( volume );

				continue;
			}

			var well = new ArchFloorCutout { Level = level, Break = volume.Break };

			well.Reshape( volume.Footprint.ToList() );
			wells.Add( well );
		}

		return (wells, recesses);
	}

	static List<ArchFloorCutout> Holes( IReadOnlyList<List<Vector2>> region ) {
		var holes = new List<ArchFloorCutout>();

		foreach ( var loop in region.Where( loop => loop.Count >= 3 && ArchFootprint.SignedArea( loop ) < 0f ) ) {
			var hole = new ArchFloorCutout();
			hole.Reshape( loop );

			holes.Add( hole );
		}

		return holes;
	}

	// Runs along the union boundary of holes to avoid doubled edges
	public static void WellTrim(
		ArchMesh canvas,
		IReadOnlyList<ArchFloorCutout> cutouts,
		float bottom,
		float top,
		ArchKit kit,
		ArchBrush brush,
		Func<Vector2, bool> handedOver = null,
		IReadOnlyList<ArchWall> walls = null,
		IReadOnlyList<IReadOnlyList<Vector2>> region = null ) {
		var bite = ArchLap.Bite( kit );
		var proud = ArchLap.Proud( kit );
		var depth = MathF.Max( 0f, kit.WellTrimDepth );

		if ( cutouts is null || depth < 0.05f ) {
			return;
		}

		foreach ( var loop in Boundary( cutouts ) ) {
			var slab = ArchFootprint.FloorSide( loop );

			for ( var index = 0; index < loop.Count; index++ ) {
				var a = loop[index];
				var b = loop[(index + 1) % loop.Count];
				var run = b - a;

				if ( run.Length < 0.5f ) {
					continue;
				}

				var unit = run.Normal;
				var outward = new Vector2( -unit.y, unit.x ) * slab;

				// Folded onto bisector at corners for continuous ring
				var opening = ArchBandGen.Fold( Reaching( loop, index, -1, unit ), unit, slab );
				var closing = ArchBandGen.Fold( unit, Reaching( loop, index, 1, unit ), slab );

				// Clipped to the slab boundary, not sampled at midpoint
				foreach ( var (from, to) in Dressed( region, a, b, outward, bite ) ) {
					var start = Vector2.Lerp( a, b, from );
					var finish = Vector2.Lerp( a, b, to );
					var middle = (start + finish) * 0.5f;
					var foldFrom = from < 0.001f ? opening : 0f;
					var foldTo = to > 0.999f ? closing : 0f;

					if ( ArchProbe.Against( walls, kit, middle, outward ) ) {
						continue;
					}

					if ( handedOver?.Invoke( middle ) != true ) {
						Board( canvas, start, finish, outward, -proud, depth, top - bite, top + proud, brush, foldFrom, foldTo );
					}

					Board( canvas, start, finish, outward, -proud * 0.5f, bite, bottom, top - bite, brush, foldFrom, foldTo );
				}
			}
		}
	}

	public static bool Cutting( IReadOnlyList<IReadOnlyList<Vector2>> region, Vector2 at, Vector2 outward, float bite ) {
		return region is null || ArchFootprint.Encloses( region, at + outward * MathF.Max( bite, 0.05f ) );
	}

	public static IEnumerable<(float From, float To)> Dressed(
		IReadOnlyList<IReadOnlyList<Vector2>> region,
		Vector2 a,
		Vector2 b,
		Vector2 outward,
		float bite ) {
		if ( region is null ) {
			yield return (0f, 1f);

			yield break;
		}

		var step = outward * MathF.Max( bite, 0.05f );
		var cuts = new List<float> { 0f, 1f };

		foreach ( var loop in region ) {
			cuts.AddRange( ArchFootprint.Crossings( loop, a + step, b + step ) );
		}

		cuts.Sort();

		for ( var index = 0; index + 1 < cuts.Count; index++ ) {
			var from = cuts[index];
			var to = cuts[index + 1];

			if ( to - from > 0.001f && Cutting( region, Vector2.Lerp( a, b, (from + to) * 0.5f ), outward, bite ) ) {
				yield return (from, to);
			}
		}
	}

	static IEnumerable<List<Vector2>> Boundary( IReadOnlyList<ArchFloorCutout> cutouts ) {
		return ArchFootprint.Union( cutouts.Select( cutout => cutout.Outline() ) );
	}

	// Skips zero-length edges from traced boundary
	static Vector2 Reaching( IReadOnlyList<Vector2> loop, int from, int step, Vector2 fallback ) {
		for ( var walked = 1; walked <= loop.Count; walked++ ) {
			var at = ((from + step * walked) % loop.Count + loop.Count) % loop.Count;
			var run = loop[(at + 1) % loop.Count] - loop[at];

			if ( run.Length > 0.5f ) {
				return run.Normal;
			}
		}

		return fallback;
	}

	static void Board( ArchMesh canvas, Vector2 a, Vector2 b, Vector2 outward, float near, float far, float bottom, float top, ArchBrush brush, float foldFrom = 0f, float foldTo = 0f ) {
		if ( top - bottom < 0.05f || far - near < 0.05f ) {
			return;
		}

		var unit = (b - a).Normal;

		Vector3 At( Vector2 point, float offset, float height, float fold ) {
			var along = unit * (fold * offset);
			var across = outward * offset;

			return new Vector3( point.x + along.x + across.x, point.y + along.y + across.y, height );
		}

		var lower = new List<Vector3>
		{
			At( a, near, bottom, foldFrom ),
			At( b, near, bottom, -foldTo ),
			At( b, far, bottom, -foldTo ),
			At( a, far, bottom, foldFrom )
		};

		var upper = new List<Vector3>();

		foreach ( var point in lower ) {
			upper.Add( point.WithZ( top ) );
		}

		canvas.Prism( lower, upper, brush );
	}
}