Editor/Road/ArchRoadTerrainContribution.cs
namespace Sunless.Architecture;

public sealed class ArchRoadTerrainContribution : IArchTerrainContribution {
	public string Ident => "sunless.arch_roads";

	public int Carve( ArchTerrainRequest request ) {
		var padding = MathF.Max( 8f, request.Kit.RoadPadding );
		var inset = MathF.Max( 0f, request.Kit.RoadInset );
		var ground = new ArchGround( request.Terrain.Scene );
		var stamped = 0;
		var shallow = 0;

		foreach ( var road in request.Plan.Roads().Where( entry => entry.Stamp ) ) {
			var curve = road.Curve();

			if ( !curve.IsUsable ) {
				continue;
			}

			var frames = ArchRoadGen.Frames( road, curve );

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

			var bores = ArchTunnel.Shapes( road, curve, frames, request.Kit, ground, request.Plan )
				.Where( shape => shape.Part.Bored )
				.ToList();

			var verge = road.Reach();
			var reach = verge + padding;

			var min = new Vector2( frames.Min( frame => frame.Position.x ), frames.Min( frame => frame.Position.y ) );
			var max = new Vector2( frames.Max( frame => frame.Position.x ), frames.Max( frame => frame.Position.y ) );

			var lo = Cell( min - new Vector2( reach, reach ), request.Origin, request.Step, request.Resolution );
			var hi = Cell( max + new Vector2( reach, reach ), request.Origin, request.Step, request.Resolution );

			for ( var y = lo.y; y <= hi.y; y++ ) {
				for ( var x = lo.x; x <= hi.x; x++ ) {
					var world = new Vector2( request.Origin.x + x * request.Step, request.Origin.y + y * request.Step );

					if ( !Closest( frames, world, out var surface, out var gap, out var right, out var along ) || gap > reach ) {
						continue;
					}

					if ( ArchBridge.Carried( road, along ) ) {
						continue;
					}

					if ( bores.Any( bore => bore.Carries( along ) ) ) {
						continue;
					}

					var index = y * request.Resolution + x;
					var current = request.Origin.z + request.Map[index] / (float)ushort.MaxValue * request.MaxHeight;
					var shoulder = road.Reach( right );
					var target = Shoulder( road, request.Kit, surface, gap, shoulder, inset );

					if ( gap > shoulder ) {
						target = MathX.Lerp( target, current, Ease( (gap - shoulder) / padding ) );
					}

					// A road is cut into the ground, never stood on a bank raised to meet it: filling here is what
					// walled a street in on both sides and left the kerb reading as a plinth.
					if ( target >= current ) {
						shallow++;
						continue;
					}

					request.Map[index] = (ushort)Math.Clamp( (target - request.Origin.z) / request.MaxHeight * ushort.MaxValue, 0f, ushort.MaxValue );
					stamped++;
				}
			}
		}

		// Silence here reads as a broken stamp; it is nearly always a road laid over the ground rather than into it.
		if ( stamped == 0 && shallow > 0 ) {
			Log.Info( $"Architecture: every one of {shallow} samples under the roads already sits below them - nothing to cut. Lower the run onto the ground, or give the ground headroom first." );
		}

		return stamped;
	}

	public int Cut( ArchTerrainRequest request ) {
		var ground = new ArchGround( request.Terrain.Scene );
		var cut = 0;

		foreach ( var road in request.Plan.Roads() ) {
			var curve = road.Curve();

			if ( !curve.IsUsable ) {
				continue;
			}

			var frames = ArchRoadGen.Frames( road, curve );

			foreach ( var tunnel in ArchTunnel.Shapes( road, curve, frames, request.Kit, ground, request.Plan ) ) {
				foreach ( var mouth in new[] { tunnel.Near, tunnel.Far } ) {
					cut += Cutting( tunnel, mouth, request );
				}
			}
		}

		return cut;
	}

	static int Cutting( ArchTunnelShape shape, ArchTunnelMouth mouth, ArchTerrainRequest request ) {
		var part = shape.Part;
		var frame = mouth.Frame;
		var reach = ArchTunnel.Reach( part, shape.Section );
		var padding = MathF.Max( 8f, request.Kit.RoadPadding );
		var inset = MathF.Max( 0f, request.Kit.RoadInset );
		var run = MathF.Max( MathF.Max( 96f, part.WingWall ), MathF.Max( 12f, part.PortalDepth ) * 2f );
		var splay = MathF.Tan( Math.Clamp( part.WingSplay, 0f, 75f ).DegreeToRadian() );

		var outward = mouth.Near ? -frame.Along : frame.Along;
		var ahead = new Vector2( outward.x, outward.y ).Normal;
		var sideways = new Vector2( -ahead.y, ahead.x );
		var floor = frame.Position.z + shape.Section.Floor - inset;
		var widest = reach + run * splay + padding;

		var lo = Cell( frame.Flat - new Vector2( widest + run, widest + run ), request.Origin, request.Step, request.Resolution );
		var hi = Cell( frame.Flat + new Vector2( widest + run, widest + run ), request.Origin, request.Step, request.Resolution );
		var cut = 0;

		for ( var y = lo.y; y <= hi.y; y++ ) {
			for ( var x = lo.x; x <= hi.x; x++ ) {
				var world = new Vector2( request.Origin.x + x * request.Step, request.Origin.y + y * request.Step );
				var offset = world - frame.Flat;
				var along = Vector2.Dot( offset, ahead );

				if ( along < 0f || along > run + padding ) {
					continue;
				}

				var across = MathF.Abs( Vector2.Dot( offset, sideways ) );
				var half = reach + along * splay;
				var outside = MathF.Max( across - half, along - run );

				if ( outside >= padding ) {
					continue;
				}

				var index = y * request.Resolution + x;
				var current = request.Origin.z + request.Map[index] / (float)ushort.MaxValue * request.MaxHeight;
				var target = outside > 0f ? MathX.Lerp( current, floor, Ease( 1f - outside / padding ) ) : floor;

				if ( target >= current ) {
					continue;
				}

				request.Map[index] = (ushort)Math.Clamp( (target - request.Origin.z) / request.MaxHeight * ushort.MaxValue, 0f, ushort.MaxValue );
				cut++;
			}
		}

		return cut;
	}

	// The ground meets the section it runs beside, not the crown. Carve the whole verge to the carriageway and the
	// pavement is left standing on a plinth its own kerb height - which is what the drive's landing already knew.
	public static float Shoulder( ArchRoadPart road, ArchKit kit, float surface, float gap, float shoulder, float inset ) {
		var carriage = road.HalfWidth;
		var grade = surface - inset;

		if ( gap <= carriage || shoulder <= carriage + 0.01f ) {
			return grade;
		}

		return MathX.Lerp( grade, surface + ArchCrossover.Back( road, kit ),
			Math.Clamp( (gap - carriage) / (shoulder - carriage), 0f, 1f ) );
	}

	static bool Closest( IReadOnlyList<ArchFrame> frames, Vector2 point, out float height, out float gap, out bool right, out float station ) {
		height = 0f;
		gap = float.MaxValue;
		right = false;
		station = 0f;

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

			if ( length < 0.01f ) {
				continue;
			}

			var direction = span / length;
			var along = Math.Clamp( Vector2.Dot( point - from.Flat, direction ), 0f, length );
			var offset = point - (from.Flat + direction * along);
			var reach = offset.Length;

			if ( reach >= gap ) {
				continue;
			}

			gap = reach;
			// Which pavement this sample stands beside: only that side's verge decides how high the ground comes up.
			right = Vector2.Dot( offset, new Vector2( direction.y, -direction.x ) ) > 0f;
			height = MathX.Lerp( from.Position.z, to.Position.z, along / length );
			station = MathX.Lerp( from.Distance, to.Distance, along / length );
		}

		return gap < float.MaxValue;
	}

	static (int x, int y) Cell( Vector2 world, Vector3 origin, float step, int resolution ) {
		var x = (int)MathF.Round( (world.x - origin.x) / step );
		var y = (int)MathF.Round( (world.y - origin.y) / step );

		return (Math.Clamp( x, 0, resolution - 1 ), Math.Clamp( y, 0, resolution - 1 ));
	}

	static float Ease( float t ) {
		var clamped = Math.Clamp( t, 0f, 1f );

		return clamped * clamped * (3f - 2f * clamped);
	}
}