Editor/Barriers/ArchRetaining.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

namespace Sunless.Architecture;

// One station of a retaining run: the level it holds, the grade it stands open to, and which way the fill lies.
// Shared by the two bays either side of it, so a section cannot step across a joint.
public readonly struct ArchRetainingStation {
	public Vector2 At { get; init; }
	public float Head { get; init; }
	public float Grade { get; init; }
	public Vector2 Fill { get; init; }
	public bool End { get; init; }

	public float Retains => Head - Grade;
}

public readonly struct ArchRetainingBay {
	public ArchRetainingStation From { get; init; }
	public ArchRetainingStation To { get; init; }
	public int Index { get; init; }

	public float Span => (To.At - From.At).Length;

	public float Retains => MathF.Max( From.Retains, To.Retains );
}

// A freestanding retaining wall: the barrier spline says where it runs, ArchGround says what it holds, and where
// the two faces meet there is no fill and no wall. One resolve, read by the generator, the ghost and the report.
public sealed class ArchRetainingShape {
	// The thinnest wall worth pouring, and the floor every wing thickness in the tool already takes.
	const float ThinnestStem = 8f;

	public List<ArchRetainingBay> Bays { get; init; } = new();
	public List<ArchWingWall> Returns { get; init; } = new();
	public float Stem { get; init; }
	public float Lean { get; init; }

	public bool IsUsable => Bays.Count > 0;

	public float Retains => Bays.Count == 0 ? 0f : Bays.Max( bay => bay.Retains );

	public static ArchRetainingShape Resolve( ArchBarrierShape barrier, ArchKit kit, ArchGround ground ) {
		if ( barrier is null || !barrier.IsUsable ) {
			return new ArchRetainingShape();
		}

		var stations = barrier.Posts.Select( post => Station( post, ground ) ).ToList();
		// A drop shallower than the wall's own footing is a kerb, not something that retains anything.
		var least = ArchGround.Embedment( kit );
		var bays = new List<ArchRetainingBay>();

		foreach ( var bay in barrier.Bays ) {
			if ( bay.Missing || bay.Index + 1 >= stations.Count ) {
				continue;
			}

			var standing = new ArchRetainingBay { From = stations[bay.Index], To = stations[bay.Index + 1], Index = bay.Index };

			if ( standing.Retains < least || standing.Span < 1f ) {
				continue;
			}

			bays.Add( standing );
		}

		var retains = bays.Count == 0 ? 0f : bays.Max( bay => bay.Retains );
		var batter = MathF.Max( 0.02f, kit?.RetainingBatter ?? 0.08f );
		// One section for the whole wall, off its deepest exposure: sized per bay, a joint steps mid-run.
		var stem = MathF.Max( ThinnestStem, retains * batter );

		return new ArchRetainingShape {
			Bays = bays,
			Returns = Ends( bays, ground ),
			Stem = stem,
			Lean = Math.Clamp( retains * batter, 0f, stem * 0.5f )
		};
	}

	// The head is the fill's own level, never the line the author drew: a wall dragged along the toe of a bank
	// still comes out as tall as what it holds.
	static ArchRetainingStation Station( ArchBarrierPost post, ArchGround ground ) {
		var across = ArchBarrierGen.Across( post.Frame );
		var seat = post.Seat;
		var right = ground.Under( post.Flat + across * ArchWing.ProbeStep, seat );
		var left = ground.Under( post.Flat - across * ArchWing.ProbeStep, seat );

		return new ArchRetainingStation {
			At = post.Flat,
			Head = MathF.Max( seat, MathF.Max( right, left ) ),
			Grade = MathF.Min( right, left ),
			Fill = right >= left ? across : -across,
			End = post.End
		};
	}

	// Each open end carries on into the bank and dies where it meets its own head - ArchWing's rule, and its
	// refusal: no fill, no wing.
	static List<ArchWingWall> Ends( IReadOnlyList<ArchRetainingBay> bays, ArchGround ground ) {
		var returns = new List<ArchWingWall>();

		if ( bays.Count == 0 ) {
			return returns;
		}

		Return( returns, bays[0].From, Heading( bays[0], false ), ground );
		Return( returns, bays[^1].To, Heading( bays[^1], true ), ground );

		return returns;
	}

	static void Return( List<ArchWingWall> into, ArchRetainingStation station, Vector2 heading, ArchGround ground ) {
		if ( !station.End ) {
			return;
		}

		var died = ArchWing.Dies( station.At, heading, station.Retains, station.Head, ground, station.Grade );

		if ( died < ArchWing.ProbeStep ) {
			return;
		}

		var end = station.At + heading * died;

		into.Add( new ArchWingWall {
			From = station.At,
			To = end,
			Head = station.Head,
			Ground = MathF.Min( station.Head, ground.Under( end, station.Grade ) ),
			Buried = station.Grade
		} );
	}

	static Vector2 Heading( ArchRetainingBay bay, bool forward ) {
		var span = bay.To.At - bay.From.At;
		var along = span.Length < 0.05f ? new Vector2( 1f, 0f ) : span.Normal;

		return forward ? along : -along;
	}
}