Editor/Stair/ArchApproachGen.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

namespace Sunless.Architecture;

public static class ArchApproachGen {
	// Podiums butt flush — embedding would overlap flanks on the same plane
	static float Embed( ArchApproachPart approach, ArchKit kit ) {
		return approach.OnPlatform ? 0f : Reach( kit ) + ArchLap.Bite( kit ) * 2f;
	}

	static float Reach( ArchKit kit ) => MathF.Max( 0f, kit.FoundationOversize ) + MathF.Max( 0f, kit.WallThickness ) * 0.5f;

	const float MouthDepth = 3f;

	public static ArchFloorCutout Mouth( ArchApproachPart approach, ArchKit kit, float reach = MouthDepth ) {
		return Mouth( approach, kit, reach, MouthDepth, Reach( kit ) );
	}

	// Inset differs for plinths vs podiums — the line they straddle is in a different place
	public static ArchFloorCutout Mouth( ArchApproachPart approach, ArchKit kit, float garden, float inward, float inset ) {
		var outward = ArchApproachAxes.Outward( approach );
		var across = ArchApproachAxes.Across( approach );
		var half = ArchApproachAxes.HalfWidth( approach ) - ArchLap.Bite( kit );
		var centre = approach.Origin - outward * inset;
		var reach = MathF.Max( MouthDepth, garden );
		var back = MathF.Max( MouthDepth, inward );

		var mouth = new ArchFloorCutout { Id = approach.Id, Name = approach.Name, OwnerId = approach.Id };

		mouth.Reshape( new[]
		{
			centre + across * half - outward * back,
			centre + across * half + outward * reach,
			centre - across * half + outward * reach,
			centre - across * half - outward * back
		} );

		return mouth;
	}

	public static void Build(
		ArchMesh canvas,
		ArchApproachPart approach,
		ArchRoom room,
		ArchBuilding building,
		ArchKit kit,
		ArchStyle style,
		List<ArchDoorRequest> doors,
		ArchPlan plan = null ) {
		var rise = Rise( approach, room, building, kit, plan );

		if ( MathF.Abs( rise ) < 2f ) {
			return;
		}

		if ( approach.Kind == ApproachKind.Steps ) {
			Steps( canvas, approach, room, building, kit, style, doors, rise, plan );
			return;
		}

		if ( approach.IsSpline ) {
			SplineRamp( canvas, approach, room, building, kit, style );
			return;
		}

		Ramp( canvas, approach, room, building, kit, style, plan, rise );
	}

	public static float Rise( ArchRoom room, ArchBuilding building, ArchKit kit, ArchPlan plan = null ) {
		return room.BaseHeight + ArchFloorGen.GradeLift( plan, building, kit );
	}

	// Podiums are poured on grade, so their top is restated in building space
	public static float Head( ArchApproachPart approach, ArchRoom room, ArchBuilding building, ArchKit kit, ArchPlan plan = null ) {
		if ( Podium( building, approach ) is not { } podium ) {
			return room.BaseHeight;
		}

		return ArchPlatformDeck.Seated( plan, building, kit, podium, approach.Origin );
	}

	public static float Rise( ArchApproachPart approach, ArchRoom room, ArchBuilding building, ArchKit kit, ArchPlan plan = null ) {
		return Head( approach, room, building, kit, plan ) - Grade( room, building, kit, plan );
	}

	public static ArchPlatformPart Podium( ArchBuilding building, ArchApproachPart approach ) {
		if ( !approach.OnPlatform ) {
			return null;
		}

		return building?.Platforms.FirstOrDefault( standing => standing.Id == approach.PlatformId );
	}

	static float Grade( ArchRoom room, ArchBuilding building, ArchKit kit, ArchPlan plan = null ) {
		return room.BaseHeight - Rise( room, building, kit, plan );
	}

	static void Steps(
		ArchMesh canvas,
		ArchApproachPart approach,
		ArchRoom room,
		ArchBuilding building,
		ArchKit kit,
		ArchStyle style,
		List<ArchDoorRequest> doors,
		float rise,
		ArchPlan plan ) {
		var outward = ArchApproachAxes.Outward( approach );
		var across = ArchApproachAxes.Across( approach );
		var grade = Grade( room, building, kit, plan );
		var head = Head( approach, room, building, kit, plan );
		var climb = MathF.Abs( rise );

		var flight = new ArchStairPart {
			Id = approach.Id,
			Name = approach.Name,
			BaseHeight = MathF.Min( grade, head ),
			Core = new ArchStairCore { Rise = MathF.Max( 1f, climb ) },
			Width = ArchApproachAxes.HalfWidth( approach ) * 2f,
			StepRise = MathF.Max( 3f, kit.StepRise ),
			TopLanding = false,
			WellGuard = false,
			Guard = approach.Rails ? StairGuard.Both : StairGuard.None,
			TreadThickness = 2.5f,
			Palette = approach.Palette
		};

		var count = Math.Max( 1, flight.StepCount );
		var going = approach.Run > 1f ? approach.Run / count : MathF.Max( 6f, kit.StepGoing );

		flight.StepGoing = going;
		var origin = rise > 0f
			? approach.Origin + outward * (count * going - Embed( approach, kit )) + across * (flight.Width * 0.5f)
			: approach.Origin - outward * Embed( approach, kit ) + across * (flight.Width * 0.5f);
		var direction = rise > 0f ? -outward : outward;
		var (core, lanes) = ArchStairCore.Straight( origin, direction, count * going, flight.Width, climb );

		flight.Core = core;
		flight.Lanes = lanes;

		ArchFlightBuilders.Build( new ArchFlightDrawing( canvas, flight, room, building, kit, style, doors, plan ) );
	}

	public readonly struct ArchRampRun {
		public Vector2 Wall { get; init; }
		public Vector2 Foot { get; init; }
		public float WallTop { get; init; }
		public float FootTop { get; init; }
		public float Grade { get; init; }
		public float HalfWidth { get; init; }
	}

	public static bool Resolved( ArchApproachPart approach, ArchRoom room, ArchBuilding building, ArchKit kit, ArchPlan plan, out ArchRampRun ramp ) {
		if ( approach.Kind != ApproachKind.Ramp ) {
			ramp = default;
			return false;
		}

		return ResolvedTerrain( approach, room, building, kit, plan, out ramp );
	}

	public static bool ResolvedTerrain( ArchApproachPart approach, ArchRoom room, ArchBuilding building, ArchKit kit, ArchPlan plan, out ArchRampRun ramp ) {
		ramp = default;

		var rise = Rise( approach, room, building, kit, plan );
		var climb = MathF.Abs( rise );

		if ( climb < 2f ) {
			return false;
		}

		var outward = ArchApproachAxes.Outward( approach );
		var grade = Grade( room, building, kit, plan );
		var run = approach.Run > 1f
			? approach.Run
			: approach.Kind == ApproachKind.Ramp
				? climb / Math.Clamp( kit.RampSlope, 0.02f, 1f )
				: Math.Max( 1, (int)MathF.Ceiling( climb / MathF.Max( 3f, kit.StepRise ) ) ) * MathF.Max( 6f, kit.StepGoing );
		var land = grade;

		if ( ArchAnswers.Load().Asks<IArchApproachLanding>() is { } landing
			&& landing.TryLand( plan, approach, kit, run, out var reach, out var height ) ) {
			run = reach;
			land = grade + height;
		}

		ramp = new ArchRampRun {
			Wall = approach.Origin - outward * Embed( approach, kit ),
			Foot = approach.Origin + outward * run,
			WallTop = Head( approach, room, building, kit, plan ),
			FootTop = land,
			Grade = grade,
			HalfWidth = ArchApproachAxes.HalfWidth( approach )
		};

		return true;
	}

	static void Ramp( ArchMesh canvas, ArchApproachPart approach, ArchRoom room, ArchBuilding building, ArchKit kit, ArchStyle style, ArchPlan plan, float rise ) {
		if ( !Resolved( approach, room, building, kit, plan, out var ramp ) ) {
			return;
		}

		var chain = new[] { approach.Palette, room.Palette, building.Palette };
		var deck = style.Brush( ArchSurface.Deck, chain );
		var skirt = Skirt( approach, building, kit, style, chain, deck );

		var podium = Coped( approach, building );
		var across = ArchApproachAxes.Across( approach );
		var half = ramp.HalfWidth;
		var top = ramp.WallTop;
		var land = ramp.FootTop;

		var bottom = MathF.Min( top, land ) - MathF.Max( 1f, kit.RampThickness );
		var right = Flank( approach, podium, kit, across, half );
		var left = Flank( approach, podium, kit, -across, half );
		var anchor = new Vector3( approach.Origin.x, approach.Origin.y, top );

		Wedge( canvas, ramp.Wall, ramp.Foot, across, right, -left, bottom, bottom, top, land, skirt,
			approach.Mapping, anchor, deck );

		if ( approach.Kerbs ) {
			Kerbs( canvas, approach, room, building, kit, style, ramp, anchor );
		}
	}

	static ArchPlatformPart Coped( ArchApproachPart approach, ArchBuilding building ) {
		return Podium( building, approach ) is { Coping: true, CopingHeight: > 0.05f } podium ? podium : null;
	}

	// Skirt inherits the podium's palette so the two read as one mass
	static ArchBrush Skirt( ArchApproachPart approach, ArchBuilding building, ArchKit kit, ArchStyle style, ArchPalette[] chain, ArchBrush deck ) {
		if ( !approach.Skirted ) {
			return deck;
		}

		var podium = Podium( building, approach );

		return style.Brush( ArchSurface.Foundation,
			podium is null ? chain : new[] { approach.Palette, podium.Palette, building.Palette } );
	}

	// Kerbs continue the podium's coping — same section and skin, same line
	static void Kerbs( ArchMesh canvas, ArchApproachPart approach, ArchRoom room, ArchBuilding building, ArchKit kit, ArchStyle style, ArchRampRun ramp, Vector3 anchor ) {
		var podium = Coped( approach, building );
		var chain = podium is null
			? new[] { approach.Palette, room.Palette, building.Palette }
			: new[] { approach.Palette, podium.Palette, building.Palette };
		var brush = style.Brush( podium is null ? ArchSurface.Baseboard : ArchSurface.WallCap, chain );

		var bite = ArchLap.Bite( kit );
		var back = podium is null
			? MathF.Max( 1f, kit.RampKerbWidth )
			: MathF.Max( 1f, podium.CopingWidth ) + MathF.Max( 0f, podium.CopingOversail );
		var stand = podium is null ? MathF.Max( 1f, kit.RampKerbHeight ) : MathF.Max( 1f, podium.CopingHeight );

		var outward = ArchApproachAxes.Outward( approach );
		var across = ArchApproachAxes.Across( approach );
		var span = (ramp.Foot - ramp.Wall).Length;
		var fall = span < 0.05f ? 0f : (ramp.WallTop - ramp.FootTop) / span;
		var reach = podium is null ? -bite : Embed( approach, kit ) - bite;
		var wall = ramp.Wall + outward * reach;
		var foot = ramp.Foot - outward * bite;
		var top = podium is null ? ramp.WallTop + fall * bite : ramp.WallTop;
		var land = ramp.FootTop + fall * bite;

		var nose = podium is null ? -bite : MathF.Max( 0f, podium.CopingOversail );

		foreach ( var side in new[] { 1f, -1f } ) {
			var outer = side * (Flank( approach, podium, kit, across * side, ramp.HalfWidth ) + nose);
			var inner = outer - side * back;

			Wedge( canvas, wall, foot, across, MathF.Max( outer, inner ), MathF.Min( outer, inner ),
				top - bite, land - bite, top + stand, land + stand, brush, approach.Mapping, anchor, brush );
		}
	}

	// Uses the podium's edge when the drag landed near it, otherwise the drive's own half-width
	static float Flank( ArchApproachPart approach, ArchPlatformPart podium, ArchKit kit, Vector2 across, float half ) {
		if ( podium is null ) {
			return half;
		}

		var edge = podium.Outline().Max( corner => Vector2.Dot( corner - approach.Origin, across ) );

		return MathF.Abs( edge - half ) > new ArchGridService().BaseSize ? half : edge;
	}

	static void SplineRamp( ArchMesh canvas, ArchApproachPart approach, ArchRoom room, ArchBuilding building, ArchKit kit, ArchStyle style ) {
		var curve = SplineCurve( approach, kit );
		var frames = curve.Walk( MathF.Max( 8f, kit.GridSize * 0.5f ) );

		if ( frames.Count < 2 ) {
			return;
		}

		var chain = new[] { approach.Palette, room.Palette, building.Palette };
		var deck = style.Brush( ArchSurface.Deck, chain );
		var skirt = Skirt( approach, building, kit, style, chain, deck );
		var bottom = MathF.Min( frames[0].Position.z, frames[^1].Position.z ) - MathF.Max( 1f, kit.RampThickness );
		var rows = new List<Vector3[]>();

		foreach ( var frame in frames ) {
			var half = MathF.Max( 12f, approach.Width * frame.WidthScale ) * 0.5f;

			rows.Add( new[] { frame.Side( -half ), frame.Side( half ) } );
		}

		ArchMeshSweep.Solid( canvas, rows, frames, kit.RampThickness, new[] { deck }, skirt, false, soffit: _ => bottom );

		if ( !approach.Kerbs ) {
			return;
		}

		var trim = style.Brush( ArchSurface.Baseboard, chain );
		var kerb = MathF.Max( 1f, kit.RampKerbWidth );
		var stand = MathF.Max( 1f, kit.RampKerbHeight );
		var bite = ArchLap.Bite( kit );

		foreach ( var right in new[] { false, true } ) {
			var edgeRows = new List<Vector3[]>();

			foreach ( var frame in frames ) {
				var half = MathF.Max( 12f, approach.Width * frame.WidthScale ) * 0.5f;
				var outside = right ? half - bite : -half + bite;
				var inside = right ? half - kerb : -half + kerb;

				edgeRows.Add( new[]
				{
					frame.Side( inside ) + Vector3.Up * stand,
					frame.Side( outside ) + Vector3.Up * stand
				} );
			}

			ArchMeshSweep.Solid( canvas, edgeRows, frames, stand + bite, new[] { trim }, trim, false );
		}
	}

	public static ArchCurve SplineCurve( ArchApproachPart approach, ArchKit kit ) {
		var nodes = approach.Nodes.Select( node => node.Copy() ).ToList();

		if ( nodes.Count < 2 ) {
			return ArchCurve.Of( nodes );
		}

		var outward = ArchApproachAxes.Outward( approach );
		var first = nodes[0];
		var embedded = first.Position - new Vector3( outward.x, outward.y, 0f ) * Embed( approach, kit );

		first.Position = ArchGridService.Fine( embedded );

		return ArchCurve.Of( nodes );
	}

	public static List<ArchRampRun> SplineRuns( ArchApproachPart approach, ArchRoom room, ArchBuilding building, ArchKit kit, ArchPlan plan = null ) {
		var runs = new List<ArchRampRun>();

		if ( !approach.IsSpline ) {
			return runs;
		}

		var frames = SplineCurve( approach, kit ).Walk( MathF.Max( 8f, kit.GridSize * 0.5f ) );
		var grade = Grade( room, building, kit, plan );

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

			runs.Add( new ArchRampRun {
				Wall = from.Flat,
				Foot = to.Flat,
				WallTop = from.Position.z,
				FootTop = to.Position.z,
				Grade = grade,
				HalfWidth = MathF.Max(
					MathF.Max( 12f, approach.Width * from.WidthScale ),
					MathF.Max( 12f, approach.Width * to.WidthScale ) ) * 0.5f
			} );
		}

		return runs;
	}

	static void Wedge(
		ArchMesh canvas,
		Vector2 wall,
		Vector2 foot,
		Vector2 across,
		float nearSide,
		float farSide,
		float wallBottom,
		float footBottom,
		float wallTop,
		float footTop,
		ArchBrush brush,
		ApproachMapping mapping,
		Vector3 anchor,
		ArchBrush? lid = null ) {
		if ( MathF.Abs( nearSide - farSide ) < 0.05f || wallTop - wallBottom < 0.05f ) {
			return;
		}

		var other = farSide;

		var lower = new List<Vector3>
		{
			Corner( wall, across, nearSide, wallBottom ),
			Corner( foot, across, nearSide, footBottom ),
			Corner( foot, across, other, footBottom ),
			Corner( wall, across, other, wallBottom )
		};

		var upper = new List<Vector3>
		{
			Corner( wall, across, nearSide, wallTop ),
			Corner( foot, across, nearSide, footTop ),
			Corner( foot, across, other, footTop ),
			Corner( wall, across, other, wallTop )
		};

		var run = foot - wall;
		var raked = mapping == ApproachMapping.Raked;

		canvas.Prism( lower, upper, brush, true, null,
			raked ? new Vector3( run.x, run.y, footTop - wallTop ).Normal : default, lid,
			raked ? anchor : null );
	}

	static Vector3 Corner( Vector2 along, Vector2 across, float side, float height ) {
		var flat = along + across * side;

		return new Vector3( flat.x, flat.y, height );
	}
}