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

namespace Sunless.Architecture;

public static partial class ArchMeshSweep {
	public static void Solid(
		ArchMesh canvas,
		IReadOnlyList<Vector3[]> rows,
		IReadOnlyList<ArchFrame> frames,
		float depth,
		IReadOnlyList<ArchBrush> tops,
		ArchBrush side,
		bool closed,
		Func<int, int, ArchBrush?> paint = null,
		Func<int, float?> soffit = null,
		IReadOnlyList<ArchCarveVolume> carves = null ) {
		if ( rows.Count < 2 || rows[0].Length < 2 ) {
			return;
		}

		var ribbed = Ribbed( rows, carves );
		var grid = ribbed.Rows;
		var across = ribbed.Across;
		var bases = Bases( grid, depth, soffit );
		var columns = ribbed.Columns;
		var segments = closed ? grid.Count : grid.Count - 1;
		var carved = Carved( grid, carves, closed );

		bool Taken( int row, int column ) => carved.Contains( (row, column) );

		int Gap( int column ) => ribbed.Gap[column];

		for ( var row = 0; row < segments; row++ ) {
			var next = (row + 1) % grid.Count;
			var along = frames[row].Along;
			var station = frames[row].Distance;
			var ahead = next == 0 ? station + (grid[next][0] - grid[row][0]).Length : frames[next].Distance;

			for ( var column = 0; column < columns; column++ ) {
				if ( Taken( row, column ) ) {
					Reveal( canvas, grid, bases, Taken, row, next, column, segments, columns, closed, along, station, ahead, side );

					continue;
				}

				canvas.Ribbon( grid[row][column], grid[row][column + 1], grid[next][column + 1], grid[next][column],
					along, paint?.Invoke( row, Gap( column ) ) ?? Top( tops, Gap( column ) ),
					Cell( across, row, next, column, station, ahead ) );
			}

			if ( !Taken( row, 0 ) ) {
				Flank( canvas, grid, bases, row, next, 0, along, station, ahead, Top( tops, Gap( 0 ) ), false );
			}

			if ( !Taken( row, columns - 1 ) ) {
				Flank( canvas, grid, bases, row, next, columns, along, station, ahead, Top( tops, Gap( columns - 1 ) ), true );
			}

			Underside( canvas, grid, bases, across, Taken, row, next, columns, along, station, ahead, side );
		}

		if ( closed ) {
			return;
		}

		Cap( canvas, grid[0], bases[0], Taken, 0, side, false );
		Cap( canvas, grid[^1], bases[^1], Taken, segments - 1, side, true );
	}

	public static HashSet<(int Row, int Column)> Carved( IReadOnlyList<Vector3[]> rows, IReadOnlyList<ArchCarveVolume> volumes, bool closed = false ) {
		var carved = new HashSet<(int Row, int Column)>();

		if ( volumes is not { Count: > 0 } || rows is not { Count: > 1 } ) {
			return carved;
		}

		var segments = closed ? rows.Count : rows.Count - 1;

		for ( var row = 0; row < segments; row++ ) {
			var next = (row + 1) % rows.Count;

			for ( var column = 0; column + 1 < rows[row].Length; column++ ) {
				var middle = (rows[row][column] + rows[row][column + 1] + rows[next][column + 1] + rows[next][column]) * 0.25f;

				foreach ( var volume in volumes ) {
					if ( Takes( volume, middle ) ) {
						carved.Add( (row, column) );

						break;
					}
				}
			}
		}

		return carved;
	}

	static bool Takes( ArchCarveVolume volume, Vector3 point ) {
		return volume.Covers( new Vector2( point.x, point.y ) ) && Banded( volume, point );
	}

	static bool Banded( ArchCarveVolume volume, Vector3 point ) {
		var flat = new Vector2( point.x, point.y );

		return point.z > volume.Floor.At( flat ) && point.z < volume.Ceiling.At( flat );
	}

	static ArchWeave Cell( IReadOnlyList<float[]> across, int row, int next, int column, float station, float ahead ) {
		return new ArchWeave {
			A = new Vector2( station, across[row][column] ),
			B = new Vector2( station, across[row][column + 1] ),
			C = new Vector2( ahead, across[next][column + 1] ),
			D = new Vector2( ahead, across[next][column] )
		};
	}

	static void Reveal(
		ArchMesh canvas,
		IReadOnlyList<Vector3[]> rows,
		IReadOnlyList<float> bases,
		Func<int, int, bool> carved,
		int row,
		int next,
		int column,
		int segments,
		int columns,
		bool closed,
		Vector3 along,
		float station,
		float ahead,
		ArchBrush brush ) {
		if ( Stands( carved, row, column - 1, segments, columns ) ) {
			Flank( canvas, rows, bases, row, next, column, along, station, ahead, brush, true );
		}

		if ( Stands( carved, row, column + 1, segments, columns ) ) {
			Flank( canvas, rows, bases, row, next, column + 1, along, station, ahead, brush, false );
		}

		if ( Stands( carved, Behind( row, segments, closed ), column, segments, columns ) ) {
			Rib( canvas, rows[row], bases[row], column, brush, true );
		}

		if ( Stands( carved, Ahead( row, segments, closed ), column, segments, columns ) ) {
			Rib( canvas, rows[next], bases[next], column, brush, false );
		}
	}

	static int Behind( int row, int segments, bool closed ) => closed ? (row + segments - 1) % segments : row - 1;

	static int Ahead( int row, int segments, bool closed ) => closed ? (row + 1) % segments : row + 1;

	static bool Stands( Func<int, int, bool> carved, int row, int column, int segments, int columns ) {
		return row >= 0 && row < segments && column >= 0 && column < columns && carved?.Invoke( row, column ) != true;
	}

	static void Underside(
		ArchMesh canvas,
		IReadOnlyList<Vector3[]> rows,
		IReadOnlyList<float> bases,
		IReadOnlyList<float[]> across,
		Func<int, int, bool> carved,
		int row,
		int next,
		int columns,
		Vector3 along,
		float station,
		float ahead,
		ArchBrush brush ) {
		var width = across[row][columns];

		if ( !Opened( carved, row, columns ) ) {
			canvas.Ribbon(
				Base( rows[row][columns], bases[row] ),
				Base( rows[row][0], bases[row] ),
				Base( rows[next][0], bases[next] ),
				Base( rows[next][columns], bases[next] ),
				along, brush, ArchWeave.Cell( station, ahead, 0f, width ) );

			return;
		}

		for ( var column = 0; column < columns; column++ ) {
			if ( carved( row, column ) ) {
				continue;
			}

			canvas.Ribbon(
				Base( rows[row][column + 1], bases[row] ),
				Base( rows[row][column], bases[row] ),
				Base( rows[next][column], bases[next] ),
				Base( rows[next][column + 1], bases[next] ),
				along, brush, new ArchWeave {
					A = new Vector2( station, width - across[row][column + 1] ),
					B = new Vector2( station, width - across[row][column] ),
					C = new Vector2( ahead, width - across[next][column] ),
					D = new Vector2( ahead, width - across[next][column + 1] )
				} );
		}
	}

	static bool Opened( Func<int, int, bool> carved, int row, int columns ) {
		for ( var column = 0; column < columns; column++ ) {
			if ( carved( row, column ) ) {
				return true;
			}
		}

		return false;
	}

	static ArchBrush Top( IReadOnlyList<ArchBrush> tops, int column ) {
		return tops.Count == 0 ? default : tops[Math.Min( column, tops.Count - 1 )];
	}

	static void Flank( ArchMesh canvas, IReadOnlyList<Vector3[]> rows, IReadOnlyList<float> bases, int row, int next, int column, Vector3 along, float station, float ahead, ArchBrush brush, bool right ) {
		var nearTop = rows[row][column];
		var farTop = rows[next][column];
		var nearBase = Base( nearTop, bases[row] );
		var farBase = Base( farTop, bases[next] );

		var near = nearTop.z - bases[row];
		var far = farTop.z - bases[next];

		if ( right ) {
			canvas.Ribbon( nearTop, nearBase, farBase, farTop, along, brush, new ArchWeave {
				A = new Vector2( station, 0f ),
				B = new Vector2( station, near ),
				C = new Vector2( ahead, far ),
				D = new Vector2( ahead, 0f )
			} );

			return;
		}

		canvas.Ribbon( nearTop, farTop, farBase, nearBase, along, brush, new ArchWeave {
			A = new Vector2( station, 0f ),
			B = new Vector2( ahead, 0f ),
			C = new Vector2( ahead, far ),
			D = new Vector2( station, near )
		} );
	}

	static float[] Developed( IReadOnlyList<Vector3[]> rows ) {
		var widest = rows[0];
		var reach = 0f;

		foreach ( var row in rows ) {
			var width = Across( row );

			if ( width[^1] <= reach ) continue;

			reach = width[^1];
			widest = row;
		}

		return Across( widest );
	}

	static List<float[]> Arcs( IReadOnlyList<Vector3[]> rows ) {
		var arcs = new List<float[]>();

		foreach ( var row in rows ) {
			arcs.Add( Across( row ) );
		}

		return arcs;
	}

	static float[] Across( IReadOnlyList<Vector3> row ) {
		var across = new float[row.Count];

		for ( var index = 1; index < row.Count; index++ ) {
			across[index] = across[index - 1] + (row[index] - row[index - 1]).Length;
		}

		return across;
	}

	static void Cap( ArchMesh canvas, IReadOnlyList<Vector3> row, float bottom, Func<int, int, bool> carved, int band, ArchBrush brush, bool end ) {
		for ( var column = 0; column < row.Count - 1; column++ ) {
			if ( carved( band, column ) ) {
				continue;
			}

			Rib( canvas, row, bottom, column, brush, end );
		}
	}

	static void Rib( ArchMesh canvas, IReadOnlyList<Vector3> row, float bottom, int column, ArchBrush brush, bool forward ) {
		var near = row[column];
		var far = row[column + 1];

		if ( forward ) {
			canvas.Quad( near, far, Base( far, bottom ), Base( near, bottom ), brush );

			return;
		}

		canvas.Quad( far, near, Base( near, bottom ), Base( far, bottom ), brush );
	}

	static List<float> Bases( IReadOnlyList<Vector3[]> rows, float depth, Func<int, float?> soffit ) {
		var bases = new List<float>();

		for ( var row = 0; row < rows.Count; row++ ) {
			var lowest = float.MaxValue;

			foreach ( var point in rows[row] ) {
				lowest = MathF.Min( lowest, point.z );
			}

			var named = soffit?.Invoke( row );

			bases.Add( named.HasValue
				? MathF.Min( named.Value, lowest - 0.5f )
				: lowest - MathF.Max( 0.5f, depth ) );
		}

		return bases;
	}

	static Vector3 Base( Vector3 point, float bottom ) => point.WithZ( bottom );

	// Rings wind right to left — reversed winding inverts the inner surface
	public static void Tube(
		ArchMesh canvas,
		IReadOnlyList<Vector3[]> inner,
		IReadOnlyList<Vector3[]> outer,
		IReadOnlyList<ArchFrame> frames,
		ArchBrush boreFace,
		ArchBrush shellFace,
		Func<int, int, bool> skip = null ) {
		if ( inner.Count < 2 || inner.Count != outer.Count || inner[0].Length < 2 || outer[0].Length != inner[0].Length ) {
			return;
		}

		var developed = Arcs( inner );
		var shell = Arcs( outer );

		var columns = inner[0].Length - 1;
		var segments = inner.Count - 1;

		for ( var row = 0; row < segments; row++ ) {
			var next = row + 1;
			var along = frames[row].Along;
			var station = frames[row].Distance;
			var ahead = frames[next].Distance;

			for ( var column = 0; column < columns; column++ ) {
				if ( skip?.Invoke( row, column ) == true ) {
					Bridged( canvas, inner, outer, frames, developed, skip, row, next, column, segments, columns, shellFace );

					continue;
				}

				canvas.Ribbon( inner[row][column], inner[row][column + 1], inner[next][column + 1], inner[next][column],
					along, boreFace, Cell( developed, row, next, column, station, ahead ) );

				canvas.Ribbon( outer[row][column + 1], outer[row][column], outer[next][column], outer[next][column + 1],
					along, shellFace, new ArchWeave {
						A = new Vector2( station, shell[row][column + 1] ),
						B = new Vector2( station, shell[row][column] ),
						C = new Vector2( ahead, shell[next][column] ),
						D = new Vector2( ahead, shell[next][column + 1] )
					} );
			}
		}

		Ring( canvas, inner[0], outer[0], developed[0], shellFace, false, skip, 0 );
		Ring( canvas, inner[^1], outer[^1], developed[^1], shellFace, true, skip, segments - 1 );

		Seam( canvas, inner, outer, frames, 0, shellFace, skip );
		Seam( canvas, inner, outer, frames, columns, shellFace, skip );
	}

	static void Bridged(
		ArchMesh canvas,
		IReadOnlyList<Vector3[]> inner,
		IReadOnlyList<Vector3[]> outer,
		IReadOnlyList<ArchFrame> frames,
		IReadOnlyList<float[]> developed,
		Func<int, int, bool> skip,
		int row,
		int next,
		int column,
		int segments,
		int columns,
		ArchBrush brush ) {
		if ( Stands( skip, row, column - 1, segments, columns ) ) {
			Foot( canvas, inner, outer, frames, row, column, brush, false );
		}

		if ( Stands( skip, row, column + 1, segments, columns ) ) {
			Foot( canvas, inner, outer, frames, row, column + 1, brush, true );
		}

		if ( Stands( skip, row - 1, column, segments, columns ) ) {
			Annulus( canvas, inner[row], outer[row], developed[row], column, brush, true );
		}

		if ( Stands( skip, row + 1, column, segments, columns ) ) {
			Annulus( canvas, inner[next], outer[next], developed[next], column, brush, false );
		}
	}

	static void Seam( ArchMesh canvas, IReadOnlyList<Vector3[]> inner, IReadOnlyList<Vector3[]> outer, IReadOnlyList<ArchFrame> frames, int column, ArchBrush brush, Func<int, int, bool> skip ) {
		var right = column == 0;
		var cell = right ? 0 : column - 1;

		for ( var row = 0; row < inner.Count - 1; row++ ) {
			if ( skip?.Invoke( row, cell ) == true ) {
				continue;
			}

			Foot( canvas, inner, outer, frames, row, column, brush, right );
		}
	}

	static void Foot(
		ArchMesh canvas,
		IReadOnlyList<Vector3[]> inner,
		IReadOnlyList<Vector3[]> outer,
		IReadOnlyList<ArchFrame> frames,
		int row,
		int column,
		ArchBrush brush,
		bool right ) {
		var next = row + 1;
		var station = frames[row].Distance;
		var ahead = frames[next].Distance;
		var here = (outer[row][column] - inner[row][column]).Length;
		var there = (outer[next][column] - inner[next][column]).Length;

		if ( right ) {
			canvas.Ribbon( outer[row][column], inner[row][column], inner[next][column], outer[next][column],
				frames[row].Along, brush, new ArchWeave {
					A = new Vector2( station, here ),
					B = new Vector2( station, 0f ),
					C = new Vector2( ahead, 0f ),
					D = new Vector2( ahead, there )
				} );

			return;
		}

		canvas.Ribbon( inner[row][column], outer[row][column], outer[next][column], inner[next][column],
			frames[row].Along, brush, new ArchWeave {
				A = new Vector2( station, 0f ),
				B = new Vector2( station, here ),
				C = new Vector2( ahead, there ),
				D = new Vector2( ahead, 0f )
			} );
	}

	static void Ring(
		ArchMesh canvas,
		IReadOnlyList<Vector3> inner,
		IReadOnlyList<Vector3> outer,
		IReadOnlyList<float> developed,
		ArchBrush brush,
		bool end,
		Func<int, int, bool> skip,
		int band ) {
		for ( var column = 0; column < inner.Count - 1; column++ ) {
			if ( skip?.Invoke( band, column ) == true ) {
				continue;
			}

			Annulus( canvas, inner, outer, developed, column, brush, end );
		}
	}

	// UV mapped along the ring — outer edge takes the stretch
	static void Annulus(
		ArchMesh canvas,
		IReadOnlyList<Vector3> inner,
		IReadOnlyList<Vector3> outer,
		IReadOnlyList<float> developed,
		int column,
		ArchBrush brush,
		bool end ) {
		var here = (outer[column] - inner[column]).Length;
		var there = (outer[column + 1] - inner[column + 1]).Length;

		if ( end ) {
			canvas.Ribbon( outer[column], inner[column], inner[column + 1], outer[column + 1], default, brush, new ArchWeave {
				A = new Vector2( here, developed[column] ),
				B = new Vector2( 0f, developed[column] ),
				C = new Vector2( 0f, developed[column + 1] ),
				D = new Vector2( there, developed[column + 1] )
			} );

			return;
		}

		canvas.Ribbon( inner[column], outer[column], outer[column + 1], inner[column + 1], default, brush, new ArchWeave {
			A = new Vector2( 0f, developed[column] ),
			B = new Vector2( here, developed[column] ),
			C = new Vector2( there, developed[column + 1] ),
			D = new Vector2( 0f, developed[column + 1] )
		} );
	}

	public static void Skin(
		ArchMesh canvas,
		IReadOnlyList<Vector3[]> rows,
		IReadOnlyList<ArchFrame> frames,
		ArchBrush brush,
		bool closed,
		IReadOnlyList<ArchCarveVolume> carves = null ) {
		if ( rows.Count < 2 || rows[0].Length < 2 ) {
			return;
		}

		var ribbed = Ribbed( rows, carves );
		var grid = ribbed.Rows;
		var across = ribbed.Across;
		var columns = ribbed.Columns;
		var segments = closed ? grid.Count : grid.Count - 1;
		var carved = Carved( grid, carves, closed );

		for ( var row = 0; row < segments; row++ ) {
			var next = (row + 1) % grid.Count;
			var station = frames[row].Distance;
			var ahead = next == 0 ? station + (grid[next][0] - grid[row][0]).Length : frames[next].Distance;

			for ( var column = 0; column < columns; column++ ) {
				if ( carved.Contains( (row, column) ) ) {
					continue;
				}

				canvas.Ribbon( grid[row][column], grid[row][column + 1], grid[next][column + 1], grid[next][column],
					frames[row].Along, brush, Cell( across, row, next, column, station, ahead ) );
			}
		}
	}
}