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

namespace Sunless.Architecture;

public static partial class ArchRoofGen {
	// What a roof light is cut out of and what roof plant stands ON, so neither ever takes an authored z: the
	// deck's own SURFACE under a point, read off the patch the generator lofts there rather than a second formula,
	// which is why a plant item follows the pitch the moment it changes. A skeleton deck has no patches and falls
	// back to its eave datum.
	public static float Seat( ArchRoofPart roof, ArchKit kit, Vector2 at ) {
		var thickness = ArchRoofPlane.Thickness( roof, kit );

		if ( !ArchRoofPlane.Planar( roof ) ) {
			return ArchRoofPlane.At( roof, at ) + thickness;
		}

		var rafters = ArchFootprint.Grow(
			new List<List<Vector2>> { ArchFootprint.Wind( roof.Outline() ) },
			ArchRoofPlane.Overhang( roof ) + ArchRoofPlane.Drip( roof, kit ) );

		ArchFootprint.Bounds( rafters.SelectMany( loop => loop ).ToList(), out var min, out var max );

		foreach ( var patch in Patches( roof, rafters, min, max ) ) {
			if ( Covers( patch, at ) ) {
				return patch.Plane.At( at ) + thickness;
			}
		}

		return ArchRoofPlane.At( roof, at ) + thickness;
	}

	// Where a light or a plant item seats on that deck. Everything it puts on the roof is its footprint GROWN by
	// its own reach, and the seat is the LOWEST corner of that: taken over the bare hole instead, a plinth or a
	// flashing hangs off the downhill side by its own oversail, and taken over the highest corner it floats.
	public static float Seat( ArchRoofPart roof, ArchRoofLightPart light, ArchKit kit ) {
		return Seat( roof, kit, light.Min, light.Max, light.CurbWidth );
	}

	public static float Seat( ArchRoofPart roof, ArchKit kit, Vector2 min, Vector2 max, float reach ) {
		var grown = new Vector2( MathF.Max( 1f, reach ), MathF.Max( 1f, reach ) );

		return Seat( roof, kit, ArchFootprint.Rect( min - grown, max + grown ) );
	}

	static float Seat( ArchRoofPart roof, ArchKit kit, IReadOnlyList<Vector2> footprint ) {
		var lowest = float.MaxValue;

		foreach ( var corner in footprint ) {
			lowest = MathF.Min( lowest, Seat( roof, kit, corner ) );
		}

		return lowest;
	}

	// WHERE A RAY MEETS THE DECK, so a gesture can start on a roof from any angle rather than only from straight
	// overhead - off-axis, a ray taken down to the storey's own plane crosses it out past the building and the drag
	// lands in the yard behind the house. Exact on a patch: a raked plane and a ray are both linear, so the crossing
	// solves in closed form. A skeleton deck has no patches and is met on its eave plane, then read again at where
	// that landed - two passes, because a hip is a cone and one plane cannot be its surface.
	public static bool Struck( ArchRoofPart roof, ArchKit kit, Ray ray, out Vector3 hit ) {
		hit = default;

		if ( roof is null ) {
			return false;
		}

		var thickness = ArchRoofPlane.Thickness( roof, kit );

		if ( !ArchRoofPlane.Planar( roof ) ) {
			return Skeleton( roof, kit, ray, out hit );
		}

		var rafters = ArchFootprint.Grow(
			new List<List<Vector2>> { ArchFootprint.Wind( roof.Outline() ) },
			ArchRoofPlane.Overhang( roof ) + ArchRoofPlane.Drip( roof, kit ) );

		ArchFootprint.Bounds( rafters.SelectMany( loop => loop ).ToList(), out var min, out var max );

		var nearest = float.MaxValue;

		foreach ( var patch in Patches( roof, rafters, min, max ) ) {
			if ( !patch.Plane.Raised( thickness ).Crosses( ray, out var reach, out var struck )
				|| reach >= nearest
				|| !Covers( patch, new Vector2( struck.x, struck.y ) ) ) {
				continue;
			}

			nearest = reach;
			hit = struck;
		}

		return nearest < float.MaxValue;
	}

	// The nearest deck on the storey a ray meets at all, so the work plane can offer it as somewhere to place.
	// Met on the FLATTENED ray, because a section is authored flat and the deck the author can see rides its
	// building's grade lift - crossed against the authored height, the plane met is a lift below the one being
	// pointed at, and at a shallow angle that is feet of error across the plan.
	public static bool Struck( ArchPlan plan, int level, ArchKit kit, Ray ray, out Vector3 hit, out ArchRoofPart on ) {
		hit = default;
		on = null;

		var answers = ArchAnswers.Load();
		var nearest = float.MaxValue;

		foreach ( var (building, roof) in ArchRoofLight.Owned( plan, level ) ) {
			var flattened = answers.Flattened( ray, plan, building, kit );

			if ( !Struck( roof, kit, flattened, out var struck ) ) {
				continue;
			}

			var reach = Vector3.Dot( struck - flattened.Position, flattened.Forward );

			if ( reach >= nearest ) {
				continue;
			}

			nearest = reach;
			hit = struck;
			on = roof;
		}

		return on is not null;
	}

	static bool Skeleton( ArchRoofPart roof, ArchKit kit, Ray ray, out Vector3 hit ) {
		hit = default;

		if ( !ArchCarvePlane.Level( roof.BaseHeight + ArchRoofPlane.Thickness( roof, kit ) ).Crosses( ray, out _, out var eave ) ) {
			return false;
		}

		var at = new Vector2( eave.x, eave.y );

		if ( !ArchCarvePlane.Level( Seat( roof, kit, at ) ).Crosses( ray, out _, out hit ) ) {
			return false;
		}

		return ArchFootprint.Encloses( new[] { (IReadOnlyList<Vector2>)roof.Outline() }, new Vector2( hit.x, hit.y ) );
	}

	static bool Covers( ArchDeckPatch patch, Vector2 at ) {
		return at.x >= patch.Min.x - ArchCarve.Grain && at.x <= patch.Max.x + ArchCarve.Grain
			&& at.y >= patch.Min.y - ArchCarve.Grain && at.y <= patch.Max.y + ArchCarve.Grain;
	}

	// One plane, but only the CELLS the region covers: a single min/max quad roofs an L's notch too.
	static void Shed( ArchMesh canvas, ArchRoofPart roof, IReadOnlyList<List<Vector2>> region, Vector2 min, Vector2 max, float thickness, ArchBrush brush ) {
		var plane = Plane( roof, min, max );
		var faces = new List<List<Vector3>>();

		foreach ( var cell in ArchFootprint.Cells( region, null ) ) {
			var corners = new[] { cell.Min, new Vector2( cell.Max.x, cell.Min.y ), cell.Max, new Vector2( cell.Min.x, cell.Max.y ) };

			faces.Add( corners.Select( corner => new Vector3( corner.x, corner.y, plane.At( corner ) ) ).ToList() );
		}

		JoinedSlab( canvas, faces, Vector3.Up * thickness, brush );
	}

	static void Gable(
		ArchMesh canvas,
		ArchRoofPart roof,
		ArchBuilding building,
		ArchKit kit,
		IReadOnlyList<List<Vector2>> region,
		Vector2 min,
		Vector2 max,
		float thickness,
		ArchBrush brush ) {
		var junctions = ArchCrossGableService.Resolve( building, kit );
		var hosted = junctions.Where( junction => ReferenceEquals( junction.Host, roof ) ).ToList();
		var branched = junctions.Where( junction => ReferenceEquals( junction.Branch, roof ) ).ToList();

		if ( Rectangular( region ) && (hosted.Count > 0 || branched.Count > 0) ) {
			CrossGable( canvas, roof, min, max, thickness, brush, hosted, branched );
			return;
		}

		var acrossMin = roof.RidgeAlongX ? min.y : min.x;
		var acrossMax = roof.RidgeAlongX ? max.y : max.x;
		var faces = new List<List<Vector3>>();

		// Each cell eaves off the wing it stands in, so an L's narrow leg gets its own ridge at its own centre
		// rather than floating a pitch-times-inset above its walls.
		foreach ( var cell in ArchFootprint.Cells( region, null ) ) {
			var centre = (cell.Min + cell.Max) * 0.5f;
			var (footing, head) = ArchRoofPlane.AcrossSpan( roof.RidgeAlongX, region, centre, acrossMin, acrossMax );
			var mid = (footing + head) * 0.5f;

			if ( roof.RidgeAlongX && cell.Min.y < mid - ArchCarve.Grain && cell.Max.y > mid + ArchCarve.Grain ) {
				AddGableFace( faces, roof, new ArchBox { Min = cell.Min, Max = new Vector2( cell.Max.x, mid ) }, footing, head );
				AddGableFace( faces, roof, new ArchBox { Min = new Vector2( cell.Min.x, mid ), Max = cell.Max }, footing, head );
				continue;
			}

			if ( !roof.RidgeAlongX && cell.Min.x < mid - ArchCarve.Grain && cell.Max.x > mid + ArchCarve.Grain ) {
				AddGableFace( faces, roof, new ArchBox { Min = cell.Min, Max = new Vector2( mid, cell.Max.y ) }, footing, head );
				AddGableFace( faces, roof, new ArchBox { Min = new Vector2( mid, cell.Min.y ), Max = cell.Max }, footing, head );
				continue;
			}

			AddGableFace( faces, roof, cell, footing, head );
		}

		JoinedSlab( canvas, faces, Vector3.Up * thickness, brush );
	}

	static void CrossGable(
		ArchMesh canvas,
		ArchRoofPart roof,
		Vector2 min,
		Vector2 max,
		float thickness,
		ArchBrush brush,
		List<ArchCrossGableJunction> hosted,
		List<ArchCrossGableJunction> branched ) {
		var alongMin = roof.RidgeAlongX ? min.x : min.y;
		var alongMax = roof.RidgeAlongX ? max.x : max.y;
		var acrossMin = roof.RidgeAlongX ? min.y : min.x;
		var acrossMax = roof.RidgeAlongX ? max.y : max.x;
		var ridge = (acrossMin + acrossMax) * 0.5f;
		var slope = ArchRoofPlane.Slope( roof );
		var ridgeHeight = roof.BaseHeight + slope * (acrossMax - acrossMin) * 0.5f;
		var faces = new List<List<Vector3>>();
		var valleys = new List<(Vector3 From, Vector3 To)>();
		var branchMinimum = branched.FirstOrDefault( junction => junction.BranchAtMinimum );
		var branchMaximum = branched.FirstOrDefault( junction => !junction.BranchAtMinimum );
		var builtAlongMin = branchMinimum?.EaveAcross ?? alongMin;
		var builtAlongMax = branchMaximum?.EaveAcross ?? alongMax;

		AddCrossGableSlope( roof, faces, hosted.Where( junction => junction.HostAtMinimum ).ToList(),
			builtAlongMin, builtAlongMax, acrossMin, ridge, acrossMin, ridgeHeight, slope, true );
		AddCrossGableSlope( roof, faces, hosted.Where( junction => !junction.HostAtMinimum ).ToList(),
			builtAlongMin, builtAlongMax, ridge, acrossMax, acrossMax, ridgeHeight, slope, false );

		foreach ( var junction in branched ) {
			var eaveHeight = roof.BaseHeight;
			var left = junction.BranchPoint( junction.EaveAcross, junction.AlongMin, eaveHeight );
			var centre = junction.BranchPoint( junction.EaveAcross, junction.AlongMid, ridgeHeight );
			var apex = junction.BranchPoint( junction.ApexAcross, junction.AlongMid, ridgeHeight );
			var right = junction.BranchPoint( junction.EaveAcross, junction.AlongMax, eaveHeight );

			faces.Add( new List<Vector3> { left, centre, apex } );
			faces.Add( new List<Vector3> { centre, right, apex } );
			valleys.Add( (left, apex) );
			valleys.Add( (apex, right) );
		}

		foreach ( var junction in hosted ) {
			var left = junction.HostPoint( junction.AlongMin, junction.EaveAcross, roof.BaseHeight );
			var right = junction.HostPoint( junction.AlongMax, junction.EaveAcross, roof.BaseHeight );
			var apexHeight = roof.BaseHeight + slope * MathF.Abs( junction.ApexAcross - junction.EaveAcross );
			var apex = junction.HostPoint( junction.AlongMid, junction.ApexAcross, apexHeight );

			valleys.Add( (left, apex) );
			valleys.Add( (apex, right) );
		}

		JoinedSlab( canvas, faces, Vector3.Up * thickness, brush, valleys );
	}

	static void AddCrossGableSlope(
		ArchRoofPart roof,
		List<List<Vector3>> faces,
		List<ArchCrossGableJunction> junctions,
		float alongMin,
		float alongMax,
		float acrossMin,
		float acrossMax,
		float eave,
		float ridgeHeight,
		float slope,
		bool minimum ) {
		var breaks = new List<float> { alongMin, alongMax };

		foreach ( var junction in junctions ) {
			breaks.Add( Math.Clamp( junction.AlongMin, alongMin, alongMax ) );
			breaks.Add( Math.Clamp( junction.AlongMid, alongMin, alongMax ) );
			breaks.Add( Math.Clamp( junction.AlongMax, alongMin, alongMax ) );
		}

		breaks = breaks.Distinct().OrderBy( value => value ).ToList();

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

			if ( to - from < 0.05f ) {
				continue;
			}

			var fromAcross = BoundaryAt( junctions, from, eave, minimum );
			var toAcross = BoundaryAt( junctions, to, eave, minimum );
			var fromHeight = roof.BaseHeight + slope * MathF.Abs( fromAcross - eave );
			var toHeight = roof.BaseHeight + slope * MathF.Abs( toAcross - eave );

			faces.Add( new List<Vector3>
			{
				RoofPoint( roof, from, fromAcross, fromHeight ),
				RoofPoint( roof, to, toAcross, toHeight ),
				RoofPoint( roof, to, minimum ? acrossMax : acrossMin, ridgeHeight ),
				RoofPoint( roof, from, minimum ? acrossMax : acrossMin, ridgeHeight )
			} );
		}
	}

	static float BoundaryAt( List<ArchCrossGableJunction> junctions, float along, float eave, bool minimum ) {
		var boundary = eave;

		foreach ( var junction in junctions.Where( junction => along >= junction.AlongMin && along <= junction.AlongMax ) ) {
			var candidate = junction.HostBoundaryAt( along );
			boundary = minimum ? MathF.Max( boundary, candidate ) : MathF.Min( boundary, candidate );
		}

		return boundary;
	}

	static Vector3 RoofPoint( ArchRoofPart roof, float along, float across, float height ) {
		return roof.RidgeAlongX
			? new Vector3( along, across, height )
			: new Vector3( across, along, height );
	}

	static void AddGableFace( List<List<Vector3>> faces, ArchRoofPart roof, ArchBox cell, float footing, float head ) {
		float Height( Vector2 point ) {
			var across = roof.RidgeAlongX ? point.y : point.x;
			var distance = MathF.Min( across - footing, head - across );

			return roof.BaseHeight + ArchRoofPlane.Rise( roof, MathF.Max( 0f, distance ) );
		}

		var a = cell.Min;
		var b = new Vector2( cell.Max.x, cell.Min.y );
		var c = cell.Max;
		var d = new Vector2( cell.Min.x, cell.Max.y );

		faces.Add( new List<Vector3>
		{
			new( a.x, a.y, Height( a ) ),
			new( b.x, b.y, Height( b ) ),
			new( c.x, c.y, Height( c ) ),
			new( d.x, d.y, Height( d ) )
		} );
	}

	// Bays step across the bounds, but each bay only decks and glazes the CELLS the region covers - run whole,
	// the bays and the glass march straight across an L's notch.
	static void Sawtooth( ArchMesh canvas, ArchRoofPart roof, IReadOnlyList<List<Vector2>> region, Vector2 min, Vector2 max, float thickness, ArchBrush deck, ArchBrush glazing ) {
		var bays = Math.Max( 1, roof.SawtoothBays );
		var alongX = !roof.RidgeAlongX;
		var total = alongX ? max.x - min.x : max.y - min.y;
		var step = total / bays;
		var rise = ArchRoofPlane.Rise( roof, step );
		var cells = ArchFootprint.Cells( region, null );

		for ( var bay = 0; bay < bays; bay++ ) {
			var from = (alongX ? min.x : min.y) + bay * step;
			var to = from + step;

			foreach ( var cell in cells ) {
				var begin = MathF.Max( from, alongX ? cell.Min.x : cell.Min.y );
				var end = MathF.Min( to, alongX ? cell.Max.x : cell.Max.y );

				if ( end - begin < 0.05f ) {
					continue;
				}

				var low = roof.BaseHeight + ArchRoofPlane.Rise( roof, begin - from );
				var high = roof.BaseHeight + ArchRoofPlane.Rise( roof, end - from );
				var acrossLow = alongX ? cell.Min.y : cell.Min.x;
				var acrossHigh = alongX ? cell.Max.y : cell.Max.x;
				var glazed = end >= to - 0.05f;

				if ( alongX ) {
					Sloped( canvas,
						new Vector3( begin, acrossLow, low ),
						new Vector3( end, acrossLow, high ),
						new Vector3( end, acrossHigh, high ),
						new Vector3( begin, acrossHigh, low ),
						thickness, deck );

					if ( glazed ) {
						canvas.Box(
							new Vector3( to - thickness, acrossLow, roof.BaseHeight ),
							new Vector3( to, acrossHigh, roof.BaseHeight + rise ),
							glazing );
					}

					continue;
				}

				Sloped( canvas,
					new Vector3( acrossLow, begin, low ),
					new Vector3( acrossHigh, begin, low ),
					new Vector3( acrossHigh, end, high ),
					new Vector3( acrossLow, end, high ),
					thickness, deck );

				if ( glazed ) {
					canvas.Box(
						new Vector3( acrossLow, to - thickness, roof.BaseHeight ),
						new Vector3( acrossHigh, to, roof.BaseHeight + rise ),
						glazing );
				}
			}
		}
	}

	static void Sloped( ArchMesh canvas, Vector3 a, Vector3 b, Vector3 c, Vector3 d, float thickness, ArchBrush brush ) {
		ArchSlab.Face( canvas, new List<Vector3> { a, b, c, d }, Vector3.Up * thickness, brush );
	}

	// Seals the slot the deck's underside leaves at plate height; reuses the fascia's bare-edge test.
	static void Closure( ArchMesh canvas, ArchRoofPart roof, IReadOnlyList<List<Vector2>> plates, ArchKit kit, ArchBrush brush, float reach, Func<Vector2, Vector2, bool> bare, ArchPlan plan, int hostId ) {
		// Bays meet the wall line at their own heights; one band cannot seal them.
		if ( roof.Style == RoofStyle.Sawtooth ) {
			return;
		}

		var thickness = MathF.Max( 2f, kit.WallThickness );

		// Measured to the band's outer face, or the top corner stands proud of the deck.
		var lift = ArchRoofPlane.Slope( roof ) * (reach - thickness * 0.5f);

		if ( lift < 0.5f ) {
			return;
		}

		foreach ( var loop in plates ) {
			Band( canvas, loop, thickness * -0.5f, thickness, roof.BaseHeight, roof.BaseHeight + lift, brush, bare, kit, plan, roof.Level, hostId, ArchCutAffects.Trims );
		}
	}

	static void EndWalls(
		ArchMesh canvas,
		ArchRoofPart roof,
		Vector2 deckMin,
		Vector2 deckMax,
		IReadOnlyList<List<Vector2>> region,
		ArchKit kit,
		ArchBrush brush,
		IReadOnlyList<List<Vector2>> abutments,
		IReadOnlyList<ArchCrossGableJunction> junctions ) {
		if ( roof.Style != RoofStyle.Gable && roof.Style != RoofStyle.Shed ) {
			return;
		}

		var alongX = roof.RidgeAlongX;
		var half = MathF.Max( 1f, kit.WallThickness ) * 0.5f;
		var low = roof.BaseHeight;
		var gable = roof.Style == RoofStyle.Gable;

		var deckFrom = alongX ? deckMin.y : deckMin.x;
		var deckTo = alongX ? deckMax.y : deckMax.x;
		var full = deckTo - deckFrom;

		if ( full < 1f ) {
			return;
		}

		var shedRise = ArchRoofPlane.Rise( roof, full );

		float ShedUnder( float at ) => low + shedRise * (roof.Reversed ? deckTo - at : at - deckFrom) / full;

		// Every region edge closes up to the deck over it, so an L's junction gets its gablet and a shed's raked
		// flank follows its own outline rather than the bounding box. An eave edge rises nothing and is skipped.
		foreach ( var loop in region ) {
			for ( var index = 0; index < loop.Count; index++ ) {
				var from = loop[index];
				var to = loop[(index + 1) % loop.Count];
				var span = to - from;

				if ( span.Length < 1f ) {
					continue;
				}

				var crossesRidge = alongX ? MathF.Abs( span.x ) < 0.5f : MathF.Abs( span.y ) < 0.5f;

				if ( gable && !crossesRidge ) {
					continue;
				}

				var outward = ArchRegion.Outward( from, to );

				if ( gable && ArchCrossGableService.JoinsBranchEnd( roof, from, to, junctions ) ) {
					continue;
				}

				// This edge's own wing decides the profile - the box put an L's narrow ridge over its notch.
				var probe = (from + to) * 0.5f - outward * MathF.Max( 1f, half );
				var (footing, head) = gable
					? ArchRoofPlane.AcrossSpan( alongX, region, probe, deckFrom, deckTo )
					: (deckFrom, deckTo);
				var mid = (footing + head) * 0.5f;
				var halfSpan = MathF.Max( 0.5f, (head - footing) * 0.5f );
				var gableRise = ArchRoofPlane.Rise( roof, halfSpan );

				float Under( float at ) => gable
					? low + gableRise * MathF.Max( 0f, 1f - MathF.Abs( at - mid ) / halfSpan )
					: ShedUnder( at );

				foreach ( var visible in VisibleEndSegments( from, to, outward, abutments ) ) {
					var inset = outward * -half;
					var fromAcross = alongX ? visible.From.y : visible.From.x;
					var toAcross = alongX ? visible.To.y : visible.To.x;
					var topFrom = Under( fromAcross );
					var topTo = Under( toAcross );

					if ( MathF.Max( topFrom, topTo ) - low < 1f ) {
						continue;
					}

					var face = new List<Vector3>
					{
						new( visible.From.x + inset.x, visible.From.y + inset.y, low ),
						new( visible.To.x + inset.x, visible.To.y + inset.y, low ),
						new( visible.To.x + inset.x, visible.To.y + inset.y, topTo )
					};

					if ( gable && MathF.Min( fromAcross, toAcross ) < mid && MathF.Max( fromAcross, toAcross ) > mid ) {
						var peak = alongX
							? new Vector2( visible.From.x, mid )
							: new Vector2( mid, visible.From.y );

						face.Add( new Vector3( peak.x + inset.x, peak.y + inset.y, low + gableRise ) );
					}

					face.Add( new Vector3( visible.From.x + inset.x, visible.From.y + inset.y, topFrom ) );

					ArchSlab.Face( canvas, face, new Vector3( outward.x, outward.y, 0f ) * (half * 2f), brush );
				}
			}
		}
	}

	static IEnumerable<(Vector2 From, Vector2 To)> VisibleEndSegments(
		Vector2 from,
		Vector2 to,
		Vector2 outward,
		IReadOnlyList<List<Vector2>> abutments ) {
		var span = to - from;
		var lengthSquared = span.LengthSquared;

		if ( lengthSquared < 0.01f ) {
			yield break;
		}

		var breaks = new List<float> { 0f, 1f };

		foreach ( var point in abutments.SelectMany( loop => loop ) ) {
			var along = Vector2.Dot( point - from, span ) / lengthSquared;

			if ( along > 0.001f && along < 0.999f ) {
				breaks.Add( along );
			}
		}

		breaks = breaks.Distinct().OrderBy( value => value ).ToList();

		for ( var index = 0; index < breaks.Count - 1; index++ ) {
			var start = Vector2.Lerp( from, to, breaks[index] );
			var end = Vector2.Lerp( from, to, breaks[index + 1] );
			var middle = (start + end) * 0.5f + outward * ArchProbe.Step;

			if ( !abutments.Any( loop => ArchFloorGen.Contains( loop, middle ) ) ) {
				yield return (start, end);
			}
		}
	}
}