Editor/Geometry/ArchMeshSweep.Ribs.cs
using System;
using System.Collections.Generic;
using Sandbox;

namespace Sunless.Architecture;

// A swept strip with a rib standing wherever a cut's boundary crosses it, and the developed grid to match.
sealed class ArchMeshSweepRibs {
	public List<Vector3[]> Rows { get; init; } = new();

	// Per row: a rib the SECTION put there quotes the one developed grid, a rib a CUT put there quotes its own
	// proportional place between the two it stands between - the same number in every row while the cut's edge
	// runs parallel to the road, raking with the edge where it does not.
	public List<float[]> Across { get; init; } = new();

	// Which SECTION gap each gap of the split grid came from. The brush list and the paving callback are both
	// indexed by the section's own columns, so a rib inserted between two of them would shift every brush after it.
	public int[] Gap { get; init; } = Array.Empty<int>();

	public int Columns => Rows.Count == 0 ? 0 : Rows[0].Length - 1;
}

// A section's ribs are where the ROAD wanted them; a cut's edge falls where it was dragged. Between the two the
// cut used to lose - a cell went whole or stayed whole - so a strip ONE column wide took nothing at all however
// much of its width a cut covered, which is why a cross-passage left the invert under a bore standing across it.
static partial class ArchMeshSweep {
	// Slot COUNT is fixed per section gap at the most any row needs, so every row still comes out the same width
	// and the grid can never twist. A row needing fewer stands its spare ribs on the gap's far end; the empty
	// cells that leaves cost nothing, because ArchMesh reads a collapsed quad as a line and drops it.
	internal static ArchMeshSweepRibs Ribbed( IReadOnlyList<Vector3[]> rows, IReadOnlyList<ArchCarveVolume> volumes ) {
		var section = Developed( rows );

		if ( volumes is not { Count: > 0 } ) {
			return Plain( rows, section );
		}

		var gaps = rows[0].Length - 1;
		var crossings = new List<float>[rows.Count][];
		var slots = new int[gaps];

		for ( var row = 0; row < rows.Count; row++ ) {
			crossings[row] = new List<float>[gaps];

			for ( var gap = 0; gap < gaps; gap++ ) {
				var found = Crossed( rows[row][gap], rows[row][gap + 1], volumes );

				crossings[row][gap] = found;
				slots[gap] = Math.Max( slots[gap], found.Count );
			}
		}

		return Array.TrueForAll( slots, count => count == 0 )
			? Plain( rows, section )
			: Split( rows, section, crossings, slots, gaps );
	}

	// Where a cut's boundary crosses one gap of one row, as parameters along it. Only a cut whose BAND stands at
	// that height is asked - one passing clear over the strip wants no rib in it, and Carved would not take the
	// cell either way.
	static List<float> Crossed( Vector3 from, Vector3 to, IReadOnlyList<ArchCarveVolume> volumes ) {
		var found = new List<float>();
		var near = new Vector2( from.x, from.y );
		var far = new Vector2( to.x, to.y );

		foreach ( var volume in volumes ) {
			if ( volume.Footprint is not { Count: >= 3 } ) {
				continue;
			}

			foreach ( var at in ArchFootprint.Crossings( volume.Footprint, near, far ) ) {
				if ( Banded( volume, Vector3.Lerp( from, to, at ) ) ) {
					found.Add( at );
				}
			}
		}

		found.Sort();

		return Spread( found, (to - from).Length );
	}

	// A rib a hair off one the section already stands, or off another cut's, is a sliver nobody can skin.
	static List<float> Spread( List<float> found, float length ) {
		var kept = new List<float>();

		foreach ( var at in found ) {
			if ( at * length < ArchGridService.FinestSize || (1f - at) * length < ArchGridService.FinestSize ) {
				continue;
			}

			if ( kept.Count > 0 && (at - kept[^1]) * length < ArchGridService.FinestSize ) {
				continue;
			}

			kept.Add( at );
		}

		return kept;
	}

	static ArchMeshSweepRibs Plain( IReadOnlyList<Vector3[]> rows, float[] section ) {
		var ribs = new ArchMeshSweepRibs { Gap = Ordinals( rows[0].Length - 1 ) };

		foreach ( var row in rows ) {
			ribs.Rows.Add( row );
			ribs.Across.Add( section );
		}

		return ribs;
	}

	static ArchMeshSweepRibs Split( IReadOnlyList<Vector3[]> rows, float[] section, List<float>[][] crossings, int[] slots, int gaps ) {
		var mapping = new List<int>();

		for ( var gap = 0; gap < gaps; gap++ ) {
			for ( var slot = 0; slot <= slots[gap]; slot++ ) {
				mapping.Add( gap );
			}
		}

		var ribs = new ArchMeshSweepRibs { Gap = mapping.ToArray() };

		for ( var row = 0; row < rows.Count; row++ ) {
			var points = new List<Vector3> { rows[row][0] };
			var across = new List<float> { section[0] };

			for ( var gap = 0; gap < gaps; gap++ ) {
				var found = crossings[row][gap];

				for ( var slot = 0; slot < slots[gap]; slot++ ) {
					var at = slot < found.Count ? found[slot] : 1f;

					points.Add( Vector3.Lerp( rows[row][gap], rows[row][gap + 1], at ) );
					across.Add( section[gap] + (section[gap + 1] - section[gap]) * at );
				}

				points.Add( rows[row][gap + 1] );
				across.Add( section[gap + 1] );
			}

			ribs.Rows.Add( points.ToArray() );
			ribs.Across.Add( across.ToArray() );
		}

		return ribs;
	}

	static int[] Ordinals( int count ) {
		var ordinals = new int[count];

		for ( var index = 0; index < count; index++ ) {
			ordinals[index] = index;
		}

		return ordinals;
	}
}