Editor/Carve/ArchCarve.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

namespace Sunless.Architecture;

public sealed class ArchCarve {
	public const float Grain = 0.01f;

	readonly List<ArchCarveVolume> solids = new();
	readonly List<ArchCarveVolume> cuts = new();

	float frame;

	public static ArchCarve Prism( IReadOnlyList<Vector2> footprint, float from, float to ) {
		return new ArchCarve().Plus( ArchCarveVolume.Over( footprint, from, to ) );
	}

	public static ArchCarve Wedge( IReadOnlyList<Vector2> footprint, float from, ArchCarvePlane top ) {
		return new ArchCarve().Plus( ArchCarveVolume.Under( footprint, from, top ) );
	}

	public ArchCarve In( float yaw ) {
		frame = MathF.Round( yaw % 90f, 4 );

		return this;
	}

	public ArchCarve Plus( ArchCarveVolume volume ) {
		if ( Real( volume ) ) {
			solids.Add( volume );
		}

		return this;
	}

	public ArchCarve Less( ArchCarveVolume volume ) {
		if ( Real( volume ) ) {
			cuts.Add( volume );
		}

		return this;
	}

	public ArchCarveShape Resolve() {
		var shape = new ArchCarveShape();

		if ( solids.Count == 0 ) {
			return shape;
		}

		var standing = solids.Select( Framed ).ToList();
		var taking = cuts.Select( Framed ).ToList();

		var grid = ArchCarveCells.Over( standing.Concat( taking ).ToList() );

		shape.Grid = grid;
		shape.Frame = frame;

		foreach ( var piece in grid.Pieces ) {
			var bands = new ArchCarveBands();

			foreach ( var volume in standing.Where( volume => volume.Covers( piece.Centre ) ) ) {
				bands.Add( volume.Floor.At( piece.Centre ), volume.Ceiling.At( piece.Centre ), volume.Floor, volume.Ceiling );
			}

			piece.Whole = bands.Copy();

			foreach ( var cut in taking.Where( cut => cut.Covers( piece.Centre ) ) ) {
				bands.Remove( cut.Floor.At( piece.Centre ), cut.Ceiling.At( piece.Centre ), cut.Floor, cut.Ceiling );
			}

			piece.Standing = bands;
		}

		var courses = Courses( grid );

		foreach ( var piece in grid.Pieces ) {
			var taken = piece.Standing.Missing( piece.Whole ).ToList();

			foreach ( var band in piece.Standing.Spans ) {
				var cell = new ArchCarveCell {
					Min = piece.Min,
					Max = piece.Max,
					Centre = piece.Centre,
					Loop = piece.Loop,
					From = band.From,
					To = band.To,
					Foot = band.Foot,
					Head = band.Head
				};

				shape.Cells.Add( cell );

				Cap( shape, grid, cell, piece, taken );
				Flank( shape, grid, cell, piece, courses );
			}
		}

		Unframe( shape );

		return shape;
	}

	ArchCarveVolume Framed( ArchCarveVolume volume ) {
		if ( frame == 0f ) {
			return volume;
		}

		return new ArchCarveVolume {
			Footprint = volume.Footprint.Select( point => ArchCarveFrame.Turn( point, -frame ) ).ToList(),
			Floor = ArchCarveFrame.Turn( volume.Floor, -frame ),
			Ceiling = ArchCarveFrame.Turn( volume.Ceiling, -frame ),
			Break = volume.Break
		};
	}

	void Unframe( ArchCarveShape shape ) {
		if ( frame == 0f ) {
			return;
		}

		foreach ( var face in shape.Faces ) {
			for ( var index = 0; index < face.Points.Count; index++ ) {
				face.Points[index] = ArchCarveFrame.Turn( face.Points[index], frame );
			}
		}

		for ( var index = 0; index < shape.Cells.Count; index++ ) {
			shape.Cells[index] = ArchCarveFrame.Turn( shape.Cells[index], frame );
		}
	}

	static void Cap( ArchCarveShape shape, ArchCarveGrid grid, ArchCarveCell cell, ArchCarvePiece piece, IReadOnlyList<ArchCarveSpan> taken ) {
		var cut = taken.Any( gap => MathF.Abs( gap.From - cell.To ) < Grain );
		var under = taken.Any( gap => MathF.Abs( gap.To - cell.From ) < Grain );

		foreach ( var part in ArchCarveQuads.Split( Conforming( grid, cell, piece ) ) ) {
			shape.Faces.Add( Level( part, cell.Head, true, cut ? ArchCarveSide.Sill : ArchCarveSide.Top ) );
			shape.Faces.Add( Level( part, cell.Foot, false, under ? ArchCarveSide.Head : ArchCarveSide.Bottom ) );
		}
	}

	// Inserts neighbour edge-split stations to avoid hanging vertices
	static List<Vector2> Conforming( ArchCarveGrid grid, ArchCarveCell cell, ArchCarvePiece piece ) {
		if ( piece?.Beside is null ) {
			return cell.Loop.ToList();
		}

		var loop = new List<Vector2>();

		for ( var index = 0; index < cell.Loop.Count; index++ ) {
			var from = cell.Loop[index];
			var to = cell.Loop[(index + 1) % cell.Loop.Count];

			loop.Add( from );

			foreach ( var station in Stations( piece.Beside[index] ) ) {
				var at = grid.Welded( Vector2.Lerp( from, to, station ) );

				if ( (at - from).Length > Grain && (at - to).Length > Grain ) {
					loop.Add( at );
				}
			}
		}

		return loop;
	}

	static IEnumerable<float> Stations( IReadOnlyList<ArchCarveTouch> touching ) {
		var marks = new List<float>();

		foreach ( var at in touching.SelectMany( touch => new[] { touch.From, touch.To } ).OrderBy( at => at ) ) {
			if ( at <= 0.001f || at >= 0.999f || marks.Any( held => MathF.Abs( held - at ) < 0.001f ) ) {
				continue;
			}

			marks.Add( at );
		}

		return marks;
	}

	static void Flank( ArchCarveShape shape, ArchCarveGrid grid, ArchCarveCell cell, ArchCarvePiece piece, IReadOnlyList<float> courses ) {
		var bands = Within( cell, courses );

		for ( var index = 0; index < piece.Loop.Count; index++ ) {
			var from = piece.Loop[index];
			var to = piece.Loop[(index + 1) % piece.Loop.Count];

			foreach ( var (start, end, beside) in Stretches( piece.Beside[index] ) ) {
				var a = grid.Welded( Vector2.Lerp( from, to, start ) );
				var b = grid.Welded( Vector2.Lerp( from, to, end ) );

				if ( (b - a).Length < Grain ) {
					continue;
				}

				if ( beside is null ) {
					Clip( shape, cell, a, b, Whole( cell ), bands, ArchCarveSide.Face );
					continue;
				}

				foreach ( var gap in beside.Standing.Missing( beside.Whole ) ) {
					Clip( shape, cell, a, b, gap, bands, ArchCarveSide.Jamb );
				}

				foreach ( var bare in beside.Whole.Beyond( cell.From, cell.To ) ) {
					Clip( shape, cell, a, b, bare, bands, ArchCarveSide.Face );
				}
			}
		}
	}

	static IEnumerable<(float From, float To, ArchCarvePiece Beside)> Stretches( List<ArchCarveTouch> touching ) {
		if ( touching.Count == 0 ) {
			yield return (0f, 1f, null);

			yield break;
		}

		var marks = new List<float> { 0f, 1f };

		foreach ( var touch in touching ) {
			marks.Add( touch.From );
			marks.Add( touch.To );
		}

		marks.Sort();

		for ( var index = 0; index + 1 < marks.Count; index++ ) {
			var from = marks[index];
			var to = marks[index + 1];

			if ( to - from < 0.001f ) {
				continue;
			}

			var middle = (from + to) * 0.5f;

			yield return (from, to, touching.FirstOrDefault( touch => middle > touch.From && middle < touch.To ).Piece);
		}
	}

	static ArchCarveSpan Whole( ArchCarveCell cell ) {
		return new ArchCarveSpan { From = cell.From, To = cell.To, Foot = cell.Foot, Head = cell.Head };
	}

	static void Clip(
		ArchCarveShape shape,
		ArchCarveCell cell,
		Vector2 edgeFrom,
		Vector2 edgeTo,
		ArchCarveSpan range,
		IReadOnlyList<float> bands,
		ArchCarveSide side ) {
		var from = MathF.Max( range.From, cell.From );
		var to = MathF.Min( range.To, cell.To );

		if ( to - from < Grain ) {
			return;
		}

		var course = from;

		foreach ( var height in bands.Where( height => height > from + Grain && height < to - Grain ) ) {
			shape.Faces.Add( Side( cell, range, edgeFrom, edgeTo, course, height, side ) );

			course = height;
		}

		shape.Faces.Add( Side( cell, range, edgeFrom, edgeTo, course, to, side ) );
	}

	// Arrangement-wide band heights — consistency must be global to avoid hanging vertices
	static List<float> Courses( ArchCarveGrid grid ) {
		var heights = new List<float>();

		void Mark( float height ) {
			if ( heights.Any( held => MathF.Abs( held - height ) < Grain ) ) {
				return;
			}

			heights.Add( height );
		}

		foreach ( var piece in grid.Pieces ) {
			foreach ( var span in piece.Whole.Spans.Concat( piece.Standing.Spans ) ) {
				Mark( span.From );
				Mark( span.To );
			}
		}

		heights.Sort();

		return heights;
	}

	static List<float> Within( ArchCarveCell cell, IReadOnlyList<float> courses ) {
		return courses.Where( height => height > cell.From + Grain && height < cell.To - Grain ).ToList();
	}

	static ArchCarveFace Level( IReadOnlyList<Vector2> loop, ArchCarvePlane plane, bool up, ArchCarveSide side ) {
		var points = loop.Select( point => new Vector3( point.x, point.y, plane.At( point ) ) ).ToList();

		if ( !up ) {
			points = points.Take( 1 ).Concat( points.Skip( 1 ).Reverse() ).ToList();
		}

		return new ArchCarveFace { Points = points, Side = side };
	}

	// Course splits are level — only the band's own ends carry the surface plane
	static ArchCarveFace Side( ArchCarveCell cell, ArchCarveSpan range, Vector2 from, Vector2 to, float bottom, float top, ArchCarveSide side ) {
		var head = Ending( cell, range, top, true );
		var foot = Ending( cell, range, bottom, false );

		return new ArchCarveFace {
			Points = new List<Vector3>
			{
				new( from.x, from.y, foot.At( from ) ),
				new( to.x, to.y, foot.At( to ) ),
				new( to.x, to.y, head.At( to ) ),
				new( from.x, from.y, head.At( from ) )
			},
			Side = side
		};
	}

	// Cell's own surface wins; span answers for an end that dies inside the band
	static ArchCarvePlane Ending( ArchCarveCell cell, ArchCarveSpan range, float height, bool up ) {
		if ( MathF.Abs( height - (up ? cell.To : cell.From) ) < Grain ) {
			return up ? cell.Head : cell.Foot;
		}

		if ( MathF.Abs( height - (up ? range.To : range.From) ) < Grain ) {
			return up ? range.Head : range.Foot;
		}

		return ArchCarvePlane.Level( height );
	}

	static bool Real( ArchCarveVolume volume ) {
		if ( volume.Footprint is not { Count: >= 3 } ) {
			return false;
		}

		var centre = volume.Footprint.Aggregate( Vector2.Zero, ( total, point ) => total + point ) / volume.Footprint.Count;

		return volume.Ceiling.At( centre ) - volume.Floor.At( centre ) > Grain;
	}
}