Editor/Tunnel/ArchTunnel.cs
using System;
using System.Collections.Generic;
using Sandbox;

namespace Sunless.Architecture;

// Where a road runs THROUGH the ground, answered here and nowhere else - four consumers would otherwise disagree.
public static class ArchTunnel {
	// Shorter, and there is no bore - just two portals standing in each other.
	public const float ShortestBore = 120f;

	const float ProbeStep = 24f;

	// A clamp, not a search - past this the drag was describing something else.
	const float Trimmed = 0.4f;

	// Ground over the crown before a mouth calls it a hill worth burying in.
	const float Cover = 24f;

	public static ArchTunnelPart Holding( ArchRoadPart road, float distance ) {
		return new ArchRoadAttachmentService( road ).TunnelAt( distance );
	}

	public static bool Held( ArchRoadPart road, float distance ) => Holding( road, distance ) is not null;

	// Both bore ends, and every corner of every cut standing over one - without them a straight tunnel is
	// simplified away, and a mouth lands on whatever ribs the sweep already had rather than on its own edges.
	public static IEnumerable<float> Stations( ArchRoadPart road, ArchCurve curve = null ) {
		foreach ( var station in new ArchRoadAttachmentService( road ).TunnelStations() ) {
			yield return station;
		}

		if ( curve is null ) {
			yield break;
		}

		foreach ( var corner in road.Cuts.Where( cut => cut.HasContent ).SelectMany( cut => cut.Outlines() ).SelectMany( loop => loop ) ) {
			if ( curve.Nearest( corner, out var frame, out _ ) ) {
				yield return frame.Distance;
			}
		}
	}

	// The plan is REQUIRED here, unlike on Resolve: this is the build path, and a bore built without it comes
	// out whole with every cut through it silently ignored.
	public static List<ArchTunnelShape> Shapes(
		ArchRoadPart road,
		ArchCurve curve,
		IReadOnlyList<ArchFrame> frames,
		ArchKit kit,
		ArchGround ground,
		ArchPlan plan ) {
		var shapes = new List<ArchTunnelShape>();

		foreach ( var part in ArchLayerGate.Enabled( road.Tunnels ) ) {
			var shape = Resolve( road, part, curve, frames, kit, ground, plan );

			if ( shape.IsUsable ) {
				shapes.Add( shape );
			}
		}

		return shapes;
	}

	public static ArchTunnelShape Resolve(
		ArchRoadPart road,
		ArchTunnelPart part,
		ArchCurve curve,
		IReadOnlyList<ArchFrame> frames,
		ArchKit kit,
		ArchGround ground,
		ArchPlan plan = null ) {
		return new ArchTunnelShapeBuilder()
			.On( road, part )
			.Along( curve )
			.WithFrames( frames )
			.WithKit( kit )
			.Under( ground )
			.In( plan )
			.Create();
	}

	internal static ArchTunnelShape ResolveCore(
		ArchRoadPart road,
		ArchTunnelPart part,
		ArchCurve curve,
		IReadOnlyList<ArchFrame> frames,
		ArchKit kit,
		ArchGround ground,
		ArchPlan plan = null ) {
		var span = Clamped( part, curve.Length );
		var empty = new ArchTunnelShape { Part = part, Road = road, Span = span };

		if ( span.Length < ShortestBore || frames.Count < 2 ) {
			return empty;
		}

		span = Buried( road, part, curve, span, ground );

		var bored = ArchJunction.Clipped( curve, frames, span );

		if ( bored.Count < 2 ) {
			return empty;
		}

		var cuts = Cutting( plan, part, kit ).ToList();
		var heads = Heads( cuts, bored );
		var section = ArchTunnelProfile.Section( road, part, bored[0], heads );
		var inner = new List<Vector3[]>();
		var outer = new List<Vector3[]>();

		foreach ( var frame in bored ) {
			var ribs = ArchTunnelProfile.Section( road, part, frame, heads );

			inner.Add( ArchTunnelProfile.Ring( frame, ribs.Inner ) );
			outer.Add( ArchTunnelProfile.Ring( frame, ribs.Outer ) );

			if ( ribs.Half > section.Half ) {
				section = ribs;
			}
		}

		var near = Mouth( road, part, bored[0], true, section, kit, ground );
		var far = Mouth( road, part, bored[^1], false, section, kit, ground );

		near = near with { Wings = Wings( road, part, near, section, ground ).ToList() };
		far = far with { Wings = Wings( road, part, far, section, ground ).ToList() };

		return new ArchTunnelShape {
			Part = part,
			Road = road,
			Span = span,
			Frames = bored,
			Inner = inner,
			Outer = outer,
			Section = section,
			Carved = ArchMeshSweep.Carved( inner, cuts ),
			Cuts = cuts,
			Near = near,
			Far = far
		};
	}

	// The cut volumes standing over this bore. Scoped to the tunnel as its own host, so a group bounds what may
	// open it exactly as it bounds what may open a slab, and ordered against it, so a cut made before the bore
	// was driven leaves it alone.
	static IEnumerable<ArchCarveVolume> Cutting( ArchPlan plan, ArchTunnelPart part, ArchKit kit ) {
		if ( plan is null ) {
			return Array.Empty<ArchCarveVolume>();
		}

		return ArchCut.Volumes( plan, 0, kit, float.MinValue * 0.5f, float.MaxValue * 0.5f,
			part.Id, ArchCutAffects.Lining, part.Id );
	}

	// Ribs at the lift each cut reaches, off the road's crown, so a mouth comes out square rather than stepped to
	// whatever facet the arc happened to land on. The lining's own Split cuts BOTH rings at the same parameter.
	static List<float> Heads( IReadOnlyList<ArchCarveVolume> cuts, IReadOnlyList<ArchFrame> bored ) {
		var lifts = new List<float>();

		foreach ( var volume in cuts ) {
			var middle = Middle( volume.Footprint );

			lifts.Add( volume.Ceiling.At( middle ) - bored[0].Position.z );
			lifts.Add( volume.Floor.At( middle ) - bored[0].Position.z );
		}

		return lifts;
	}

	static Vector2 Middle( IReadOnlyList<Vector2> loop ) {
		var total = Vector2.Zero;

		foreach ( var corner in loop ) {
			total += corner;
		}

		return total / MathF.Max( 1, loop.Count );
	}

	// The bore starts where the ground is a hill; in the open a portal is a wall across the road.
	static ArchRoadSpan Buried( ArchRoadPart road, ArchTunnelPart part, ArchCurve curve, ArchRoadSpan span, ArchGround ground ) {
		var reach = span.Length * Trimmed;
		var moved = new ArchRoadSpan {
			From = span.From + Hill( road, part, curve, span.From, reach, 1f, ground ),
			To = span.To - Hill( road, part, curve, span.To, reach, -1f, ground )
		};

		return moved.Length < ShortestBore ? span : moved;
	}

	static float Hill( ArchRoadPart road, ArchTunnelPart part, ArchCurve curve, float station, float reach, float inward, ArchGround ground ) {
		for ( var walked = 0f; walked <= reach; walked += ProbeStep ) {
			if ( Over( road, part, curve, station + walked * inward, ground ) >= Cover ) {
				return walked;
			}
		}

		return 0f;
	}

	// Handed no terrain the datum answers as the crown itself - the authored tunnel stands.
	static float Over( ArchRoadPart road, ArchTunnelPart part, ArchCurve curve, float station, ArchGround ground ) {
		if ( !curve.Sample( station, out var frame ) ) {
			return 0f;
		}

		var crown = Crown( road, part, frame );

		return ground.Under( frame.Flat, crown ) - crown;
	}

	// The top of the bore at a station, in world height.
	public static float Crown( ArchRoadPart road, ArchTunnelPart part, ArchFrame frame ) {
		var floor = ArchTunnelProfile.Floor( road );

		return frame.Position.z + MathF.Max( floor + 48f, part.Headroom ) + MathF.Max( 2f, part.Lining );
	}

	// Reads the ground in FRONT, the cutting - its own station is the hillside it is buried in.
	static ArchTunnelMouth Mouth( ArchRoadPart road, ArchTunnelPart part, ArchFrame frame, bool near, ArchTunnelSection section, ArchKit kit, ArchGround ground ) {
		var crown = frame.Position.z + section.Crown + MathF.Max( 2f, part.Lining );
		var head = crown + MathF.Max( 0f, part.PortalRise );
		var invert = frame.Position.z + section.Floor - MathF.Max( 1f, part.Invert );
		var outward = near ? -frame.Along : frame.Along;
		var ahead = new Vector2( outward.x, outward.y );
		var lowest = ground.Under( frame.Flat, invert );

		foreach ( var step in ArchDivide.AtMost( MathF.Max( 12f, part.PortalDepth ), ProbeStep ).Nodes ) {
			lowest = MathF.Min( lowest, ground.Under( frame.Flat + ahead * step, invert ) );
		}

		var bank = ground.Under( frame.Flat, crown );

		return new ArchTunnelMouth {
			Frame = frame,
			Near = near,
			Ground = lowest,
			Head = head,
			Foot = MathF.Min( invert, lowest ) - ArchGround.Embedment( kit ),
			Cover = bank - crown,
			Bank = bank
		};
	}

	// Coped to the headwall's top; not built where there is no fill to retain.
	static IEnumerable<ArchWingWall> Wings( ArchRoadPart road, ArchTunnelPart part, ArchTunnelMouth mouth, ArchTunnelSection section, ArchGround ground ) {
		if ( !part.Portals || mouth.Bank - mouth.Frame.Position.z < 12f ) {
			return Array.Empty<ArchWingWall>();
		}

		var frame = mouth.Frame;
		var reach = Reach( part, section );

		// Off the FACE, not the run - a portal's wings flare the cutting open.
		return ArchWing.Splayed( frame, mouth.Near ? -frame.Along : frame.Along, reach, reach,
			MathF.Max( 0f, part.WingWall ), part.WingSplay, mouth.Head, mouth.Ground, ground, WingSplay.FromFace );
	}

	public static float Reach( ArchTunnelPart part, ArchTunnelSection section ) {
		return section.Half + MathF.Max( 2f, part.Lining ) + MathF.Max( 0f, part.PortalReach );
	}

	// What a walkway seats on - its own kerb where the verge has one, the road where not.
	public static float Walkway( ArchRoadPart road, ArchTunnelPart part, bool right ) {
		if ( !road.Carries( part.Walkways, right ) || part.WalkwayWidth < 6f ) {
			return -ArchTunnelProfile.Floor( road );
		}

		return -ArchTunnelProfile.Floor( road ) + MathF.Max( 0f, part.WalkwayHeight )
			+ (road.Carries( road.Pavements, right ) ? MathF.Max( 0f, road.KerbHeight ) : 0f);
	}

	public static ArchRoadSpan Clamped( ArchTunnelPart part, float length ) {
		var from = Math.Clamp( MathF.Min( part.From, part.To ), 0f, length );
		var to = Math.Clamp( MathF.Max( part.From, part.To ), 0f, length );

		return new ArchRoadSpan { From = from, To = to };
	}
}