Editor/Road/ArchJunction.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

namespace Sunless.Architecture;

public readonly struct ArchRoadSpan {
	public float From { get; init; }
	public float To { get; init; }

	public float Length => MathF.Max( 0f, To - From );
}

// DERIVED: a crossing IS a junction; legs ordered by bearing, a road through contributes two.
public static class ArchJunction {
	const float DetectStep = 48f;
	internal const float ShortestLeg = 24f;

	public static List<ArchJunctionNode> Derived( ArchPlan plan, ArchKit kit ) {
		return new ArchJunctionDerivationService( plan, kit ).Create();
	}

	// No section sweeps through a node's mouth, or it drives through the apron.
	public static List<ArchRoadSpan> Spans( ArchRoadPart road, IReadOnlyList<ArchJunctionNode> nodes, float length ) {
		return new ArchJunctionSpanService( nodes, length ).For( road );
	}

	// End stations land on the cuts, where the junction expects the face.
	public static List<ArchFrame> Clipped( ArchCurve curve, IReadOnlyList<ArchFrame> frames, ArchRoadSpan span ) {
		var clipped = frames
			.Where( frame => frame.Distance > span.From + 1f && frame.Distance < span.To - 1f )
			.ToList();

		if ( curve.Sample( span.From, out var start ) ) {
			clipped.Insert( 0, start );
		}

		if ( curve.Sample( span.To, out var end ) ) {
			clipped.Add( end );
		}

		return clipped;
	}

	internal static ArchJunctionNode Clustered( List<ArchJunctionNode> nodes, Vector2 at, float height, ArchKit kit ) {
		foreach ( var node in nodes ) {
			if ( (node.Centre - at).Length <= MathF.Max( 24f, kit.JunctionSpread ) ) {
				return node;
			}
		}

		var fresh = new ArchJunctionNode { Centre = at, Height = height };

		nodes.Add( fresh );

		return fresh;
	}

	// Crossing in plan is not meeting: different heights pass over, never cut back.
	internal static IEnumerable<(Vector2 At, float Height, float Along, float Across)> Meetings( ArchRoadWalk first, ArchRoadWalk second, ArchKit kit ) {
		if ( !first.Bounds.Overlaps( second.Bounds ) ) {
			yield break;
		}

		var level = MathF.Max( 1f, kit.JunctionLevel );

		for ( var here = 0; here < first.Line.Count - 1; here++ ) {
			for ( var there = 0; there < second.Line.Count - 1; there++ ) {
				if ( !Crosses( first.Line[here], first.Line[here + 1], second.Line[there], second.Line[there + 1],
					out var at, out var a, out var b ) ) {
					continue;
				}

				var along = MathX.Lerp( first.Stations[here], first.Stations[here + 1], a );
				var across = MathX.Lerp( second.Stations[there], second.Stations[there + 1], b );
				var over = first.Height( along );
				var under = second.Height( across );

				if ( MathF.Abs( over - under ) > level ) {
					continue;
				}

				yield return (at, (over + under) * 0.5f, along, across);
			}
		}
	}

	static bool Crosses( Vector2 from, Vector2 to, Vector2 other, Vector2 end, out Vector2 at, out float first, out float second ) {
		at = default;
		first = 0f;
		second = 0f;

		var span = to - from;
		var reach = end - other;
		var denominator = span.x * reach.y - span.y * reach.x;

		if ( MathF.Abs( denominator ) < 0.0001f ) {
			return false;
		}

		var gap = other - from;

		first = (gap.x * reach.y - gap.y * reach.x) / denominator;
		second = (gap.x * span.y - gap.y * span.x) / denominator;

		if ( first < 0f || first > 1f || second < 0f || second > 1f ) {
			return false;
		}

		at = from + span * first;

		return true;
	}
}

public sealed class ArchJunctionDerivationService {
	readonly ArchPlan plan;
	readonly ArchKit kit;
	readonly List<ArchRoadWalk> roads;
	readonly List<ArchJunctionNode> nodes = new();

	public ArchJunctionDerivationService( ArchPlan plan, ArchKit kit ) {
		this.plan = plan;
		this.kit = kit;
		roads = plan.Roads()
			.Select( road => new ArchRoadWalk( road ) )
			.Where( walk => walk.Usable )
			.ToList();
	}

	public List<ArchJunctionNode> Create() {
		for ( var index = 0; index < roads.Count; index++ ) {
			for ( var other = index + 1; other < roads.Count; other++ ) {
				Connect( roads[index], roads[other] );
			}
		}

		foreach ( var node in nodes ) {
			node.Resolve( kit );
		}

		return nodes.Where( node => node.Legs.Count >= 3 ).ToList();
	}

	void Connect( ArchRoadWalk first, ArchRoadWalk second ) {
		foreach ( var meeting in ArchJunction.Meetings( first, second, kit ) ) {
			var node = ArchJunction.Clustered( nodes, meeting.At, meeting.Height, kit );

			node.Take( first, meeting.Along, ArchJunction.ShortestLeg );
			node.Take( second, meeting.Across, ArchJunction.ShortestLeg );
		}
	}
}

public sealed class ArchJunctionSpanService {
	readonly IReadOnlyList<ArchJunctionNode> nodes;
	readonly float length;

	public ArchJunctionSpanService( IReadOnlyList<ArchJunctionNode> nodes, float length ) {
		this.nodes = nodes;
		this.length = length;
	}

	public List<ArchRoadSpan> For( ArchRoadPart road ) {
		var mouths = nodes
			.Select( node => node.Mouth( road ) )
			.Where( mouth => mouth.Length > 0f )
			.OrderBy( mouth => mouth.From )
			.ToList();

		var spans = new List<ArchRoadSpan>();
		var open = 0f;

		foreach ( var mouth in mouths ) {
			if ( mouth.From > open + ArchJunction.ShortestLeg ) {
				spans.Add( new ArchRoadSpan { From = open, To = MathF.Min( mouth.From, length ) } );
			}

			open = MathF.Max( open, mouth.To );
		}

		if ( length > open + ArchJunction.ShortestLeg ) {
			spans.Add( new ArchRoadSpan { From = open, To = length } );
		}

		return spans;
	}
}

// Flattened walk with a station per point, so crossings report as distances along the curve.
public sealed class ArchRoadWalk {
	public ArchRoadWalk( ArchRoadPart road ) {
		Road = road;
		Curve = road.Curve();

		if ( !Curve.IsUsable ) {
			return;
		}

		var frames = Curve.Walk( 48f );

		Line = frames.Select( frame => frame.Flat ).ToList();
		Stations = frames.Select( frame => frame.Distance ).ToList();
		Length = Curve.Length;
		Bounds = BBox.FromPoints( frames.Select( frame => frame.Position ) ).Grow( 8f );
		Usable = Line.Count >= 2;
	}

	public ArchRoadPart Road { get; }
	public ArchCurve Curve { get; }
	public List<Vector2> Line { get; } = new();
	public List<float> Stations { get; } = new();
	public float Length { get; }
	public BBox Bounds { get; }
	public bool Usable { get; }

	public float Height( float station ) => Curve.Sample( station, out var frame ) ? frame.Position.z : 0f;
}