Editor/Geometry/ArchRoofPlane.cs
using System;
using System.Collections.Generic;
using Sandbox;

namespace Sunless.Architecture;

// What a deck IS, read off the section's own authored fields. The module that owns the kind lofts real patches over
// this and answers a finer seat, but a section drawn on a plan has a nominal surface whether or not it is installed,
// and the tool, the report and the elevation all need to draw it.
public static class ArchRoofPlane {
	public static bool Ridged( RoofStyle style ) => style is RoofStyle.Gable or RoofStyle.Shed or RoofStyle.Sawtooth;

	public static bool Planar( ArchRoofPart roof ) => roof.Style is RoofStyle.Flat or RoofStyle.Shed or RoofStyle.Gable;

	// A deck whose slopes ARE planes but whose faces the carve grid cannot hold: every hip and valley runs at 45°.
	public static bool Faceted( ArchRoofPart roof ) => roof.Style == RoofStyle.Hip;

	// WHICH SECTIONS CARRY WHICH FORMS, asked wherever a placement is offered or refused. A light, a housing and
	// plant all want the axis-aligned patches the carve grid holds, because their kerb, frame, pane and plinth are
	// rings grown off a rectangle sitting on one. A dormer wants nothing but a seat and a fall, and a hip has both -
	// each of its slopes is a plane rising off one eave - so it goes on a hip through the face carve instead.
	public static bool Carries( ArchRoofPart roof, RoofLightForm form ) => Planar( roof ) || (form.Roofed() && Faceted( roof ));

	// A flat deck falls nowhere, whatever pitch the section was authored with - and the pitch is kept rather than
	// zeroed so switching back to a gable gives the same roof. Read straight, it stood a fascia-deep upstand round
	// every flat roof in the plan, because the closure band is sized by the fall over the eave reach.
	public static float Slope( ArchRoofPart roof ) => roof.Style == RoofStyle.Flat ? 0f : ArchPitch.Slope( roof.Pitch );

	public static float Rise( ArchRoofPart roof, float span ) => Slope( roof ) * span;

	public static float Thickness( ArchRoofPart roof, ArchKit kit ) => roof.Thickness > 0f ? roof.Thickness : kit.RoofThickness;

	// A parapet IS the wall carried up past the deck, so the deck must not oversail it.
	public static float Overhang( ArchRoofPart roof ) => roof.Parapet ? 0f : roof.Overhang;

	// Drip edge: runoff clears the fascia and buries its top face.
	public static float Drip( ArchRoofPart roof, ArchKit kit ) => roof.Fascia ? ArchProfiles.FasciaDepth( kit ) + 1.5f : 0f;

	// The nominal surface over any point, evaluated on the OUTLINE the deck is built over. A shed and a sawtooth
	// are single planes and rightly read the bounds; a hip is the skeleton's own arrival and a gable eaves off
	// the wing the point stands in - the bounding box put both a pitch-times-inset above the walls of an L's leg.
	public static float At( ArchRoofPart roof, Vector2 at ) {
		var outline = ArchFootprint.Wind( roof.Outline() );

		ArchFootprint.Bounds( outline, out var min, out var max );

		var acrossY = roof.RidgeAlongX;
		var across = acrossY ? at.y : at.x;
		var low = acrossY ? min.y : min.x;
		var high = acrossY ? max.y : max.x;

		switch ( roof.Style ) {
			case RoofStyle.Flat:
				return roof.BaseHeight;

			case RoofStyle.Shed: {
					var span = MathF.Max( 1f, high - low );
					var reach = roof.Reversed ? high - across : across - low;

					return roof.BaseHeight + Rise( roof, Math.Clamp( reach, 0f, span ) );
				}

			case RoofStyle.Sawtooth: {
					var alongX = !roof.RidgeAlongX;
					var from = alongX ? min.x : min.y;
					var total = MathF.Max( 1f, (alongX ? max.x : max.y) - from );
					var step = total / Math.Max( 1, roof.SawtoothBays );
					var into = ((alongX ? at.x : at.y) - from) % step;

					return roof.BaseHeight + Rise( roof, Math.Clamp( into, 0f, step ) );
				}

			case RoofStyle.Hip:
				return roof.BaseHeight + Rise( roof, MathF.Max( 0f, ArchRoofSkeleton.Arrival( new[] { outline }, at ) ) );

			default: {
					var (footing, head) = AcrossSpan( acrossY, new[] { (IReadOnlyList<Vector2>)outline }, at, low, high );

					return roof.BaseHeight + Rise( roof, MathF.Max( 0f, MathF.Min( across - footing, head - across ) ) );
				}
		}
	}

	// The across extent of the loops at this station: the contiguous stack of cells the point stands in, so a
	// gable over an L eaves off its own wing. One resolution for the deck, the patches and every seat query.
	public static (float Low, float High) AcrossSpan( bool acrossY, IEnumerable<IReadOnlyList<Vector2>> loops, Vector2 at, float low, float high ) {
		var along = acrossY ? at.x : at.y;
		var across = acrossY ? at.y : at.x;

		var slices = ArchFootprint.Cells( loops, null )
			.Where( cell => along >= (acrossY ? cell.Min.x : cell.Min.y) - 0.01f
				&& along <= (acrossY ? cell.Max.x : cell.Max.y) + 0.01f )
			.Select( cell => acrossY ? (cell.Min.y, cell.Max.y) : (cell.Min.x, cell.Max.x) )
			.OrderBy( slice => slice.Item1 )
			.ToList();

		if ( slices.Count == 0 ) {
			return (low, high);
		}

		var footing = slices[0].Item1;
		var head = slices[0].Item2;

		foreach ( var (from, to) in slices.Skip( 1 ) ) {
			if ( from > head + 0.05f ) {
				if ( across <= head + 0.05f ) {
					break;
				}

				footing = from;
			}

			head = MathF.Max( head, to );
		}

		return (footing, head);
	}

	// How high the deck ever gets, so a cut told to come out the top of a roof knows how far up that is.
	public static float Peak( ArchRoofPart roof ) {
		var outline = ArchFootprint.Wind( roof.Outline() );

		ArchFootprint.Bounds( outline, out var min, out var max );

		var alongX = !roof.RidgeAlongX;
		var bay = (alongX ? max.x - min.x : max.y - min.y) / Math.Max( 1, roof.SawtoothBays );

		switch ( roof.Style ) {
			case RoofStyle.Shed:
				return At( roof, roof.Reversed ? min : max );

			case RoofStyle.Sawtooth:
				return At( roof, alongX
					? new Vector2( min.x + bay * 0.999f, min.y )
					: new Vector2( min.x, min.y + bay * 0.999f ) );

			case RoofStyle.Hip:
				return roof.BaseHeight + Rise( roof, ArchRoofSkeleton.Depth( new[] { outline } ) );

			default: {
					// The tallest ridge is the widest wing's, and the box centre may stand in an L's notch.
					var reach = ArchFootprint.Cells( new[] { outline }, null )
						.Select( cell => (cell.Min + cell.Max) * 0.5f )
						.Select( centre => {
							var (footing, head) = AcrossSpan( roof.RidgeAlongX, new[] { (IReadOnlyList<Vector2>)outline }, centre, min.y, max.y );

							return (head - footing) * 0.5f;
						} )
						.DefaultIfEmpty( 0f )
						.Max();

					return roof.BaseHeight + Rise( roof, reach );
				}
		}
	}
}