Editor/Barriers/ArchWing.cs
using System;
using System.Collections.Generic;
using Sandbox;

namespace Sunless.Architecture;

// Both ends carry their own ground height, so a wing dies into a slope.
public readonly struct ArchWingWall {
	public Vector2 From { get; init; }
	public Vector2 To { get; init; }
	public float Head { get; init; }
	public float Ground { get; init; }
	public float Buried { get; init; }
}

// Where a wing's splay is measured FROM, the difference between the two structures.
public enum WingSplay {
	// Off the structure's own direction, angling out: what an abutment wants.
	FromRun,
	// Off the face's own normal, angling back: what a portal wants.
	FromFace
}

// Abutment and portal share one answer: how far before the bank meets their top.
public static class ArchWing {
	// The one granularity terrain is walked at, so a freestanding wall reads the ground exactly as an abutment does.
	public const float ProbeStep = 24f;

	// A wing retains FILL: where the ground already meets its head, none is built.
	public static IEnumerable<ArchWingWall> Splayed(
		ArchFrame frame,
		Vector3 away,
		float leftEdge,
		float rightEdge,
		float reach,
		float splay,
		float head,
		float datum,
		ArchGround ground,
		WingSplay measured = WingSplay.FromRun ) {
		if ( reach < 12f ) {
			yield break;
		}

		var spread = Math.Clamp( splay, 0f, 75f ).DegreeToRadian();

		foreach ( var right in new[] { false, true } ) {
			var start = Flat( frame, right ? rightEdge : -leftEdge );
			var yaw = Heading( frame, away, right, spread, measured );
			var heading = new Vector2( MathF.Cos( yaw ), MathF.Sin( yaw ) );
			var died = Dies( start, heading, reach, head, ground, datum );

			if ( died < ProbeStep ) {
				continue;
			}

			var end = start + heading * died;

			yield return new ArchWingWall {
				From = start,
				To = end,
				Head = head,
				Ground = MathF.Min( head, ground.Under( end, datum ) ),
				Buried = datum
			};
		}
	}

	// From-face 'back' follows the structure's direction, so it works at either end of a bore.
	static float Heading( ArchFrame frame, Vector3 away, bool right, float spread, WingSplay measured ) {
		var along = MathF.Atan2( away.y, away.x );

		if ( measured == WingSplay.FromRun ) {
			return along + (right ? spread : -spread);
		}

		var outward = right ? frame.Across : -frame.Across;
		var face = MathF.Atan2( outward.y, outward.x );
		var turn = MathF.Atan2( MathF.Sin( along - face ), MathF.Cos( along - face ) );

		return face + MathF.Sign( turn ) * spread;
	}

	// Authored length is a MAXIMUM: past the bank's meeting point, a wing lies flat.
	public static float Dies( Vector2 start, Vector2 heading, float reach, float head, ArchGround ground, float datum ) {
		for ( var walked = ProbeStep; walked <= reach; walked += ProbeStep ) {
			if ( ground.Under( start + heading * walked, datum ) >= head ) {
				return walked;
			}
		}

		return reach;
	}

	// Dies just proud of the ground: the end is meant to disappear into the bank.
	public static void Build( ArchMesh canvas, IEnumerable<ArchWingWall> wings, float thickness, ArchKit kit, ArchBrush brush ) {
		var embedment = ArchGround.Embedment( kit );
		var half = MathF.Max( 2f, thickness ) * 0.5f;

		foreach ( var wing in wings ) {
			var foot = wing.Buried - embedment;
			var died = Math.Clamp( wing.Ground + ArchLap.Proud( kit ), foot + 1f, wing.Head );

			canvas.Rake( wing.From, wing.To, -half, half, foot, wing.Ground - embedment, wing.Head, died, brush );
		}
	}

	static Vector2 Flat( ArchFrame frame, float across ) {
		var point = frame.Side( across );

		return new Vector2( point.x, point.y );
	}
}