Editor/Bands/ArchCorniceRing.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

namespace Sunless.Architecture;

// A segment where the ring steps out by Depth and back, shoulders mitred like any other corner.
public readonly record struct ArchCorniceReturn( Vector2 From, Vector2 To, float Depth );

// Swept on the centreline, not an offset loop — an offset mitres each vertex independently, skewing oblique segments.
public sealed class ArchCorniceRing {
	public ArchCorniceStyle Crown { get; init; }
	public IReadOnlyList<Vector2> Outline { get; init; }
	public float Head { get; init; }
	public float Thickness { get; init; }
	public Func<Vector2, Vector2, bool> Abuts { get; init; }
	public IReadOnlyList<ArchCorniceReturn> Returns { get; init; } = Array.Empty<ArchCorniceReturn>();
	public ArchKit Kit { get; init; }
	public ArchStyle Style { get; init; }
	public ArchPalette[] Chain { get; init; }
	public ArchPlan Plan { get; init; }
	public int Level { get; init; }
	public int HostId { get; init; }
	// Whether a pier may step ABOVE the ring. A deck edge is the only head with nothing standing on it, so it is the
	// only one that breaks its line upward - stepped off a storey head the blocks stand inside the storey above.
	public bool Steps { get; init; } = true;

	// What a pier standing under the ring dies at: the seat of its lowest course, less a bite so the pier's head and
	// the entablature's underside are not one plane. The one answer, so nothing measures the entablature twice.
	public static float Soffit( ArchCorniceStyle crown, float head, ArchKit kit ) {
		var layers = ArchCorniceGen.Layers( crown, head );

		return layers.Count == 0 ? head : layers[0].Lid( kit ) + ArchLap.Bite( kit );
	}

	public void Lay( ArchMesh canvas ) {
		if ( Crown is not { Stands: true } || Outline is not { Count: >= 3 } ) {
			return;
		}

		var half = MathF.Max( 1f, Thickness ) * 0.5f;
		var bite = ArchLap.Bite( Kit );
		var back = half - bite;
		var outline = ArchFootprint.Wind( Outline.ToList() );
		var loop = Stepped( outline );
		var layers = ArchCorniceGen.Layers( Crown, Head );

		foreach ( var layer in layers ) {
			var brush = Style.Brush( layer.Course.Field, Chain );

			switch ( layer.Course.Kind ) {
				case CorniceCourse.Coping:
					Band( canvas, loop, -half - layer.Projection, Thickness + layer.Projection * 2f,
						layer.Seat( Kit ), layer.Lid( Kit ), brush );
					break;

				// SWEPT like every other proud course, not beamed edge by edge: a corona laid as a beam per edge is
				// as many solids as the loop has sides, each authoring its own texcoords, and the run reads as
				// blocks with a seam at every mitre where the reference has one continuous band.
				case CorniceCourse.Band:
					Swept( canvas, layer, loop, brush, Standing( Squared( layer, bite ), back ) );
					break;

				case CorniceCourse.Moulding:
					Swept( canvas, layer, loop, brush, Standing( ArchProfiles.Named( Kit, layer.Course.Profile ), back ) );
					break;

				default:
					Studded( canvas, layer, loop, back - bite, bite, brush );
					break;
			}
		}

		Brackets( canvas, loop, outline, layers, half - bite );
		Piers( canvas, loop, half - bite );
	}

	// The path a proud shape pushes out of line. Each return lands on the edge it was measured against, so a pier
	// wrapping a corner steps both of the runs meeting there and the two shoulders mitre into one another.
	List<Vector2> Stepped( List<Vector2> loop ) {
		if ( Returns.Count == 0 || loop.Count < 3 ) {
			return loop;
		}

		var stepped = new List<Vector2>();

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

			stepped.Add( from );

			if ( length < 0.5f ) {
				continue;
			}

			var along = span / length;
			var outward = ArchRegion.Outward( from, to );

			foreach ( var crossing in Crossings( from, along, outward, length ) ) {
				stepped.Add( from + along * crossing.From );
				stepped.Add( from + along * crossing.From + outward * crossing.Depth );
				stepped.Add( from + along * crossing.To + outward * crossing.Depth );
				stepped.Add( from + along * crossing.To );
			}
		}

		return stepped;
	}

	IEnumerable<(float From, float To, float Depth)> Crossings( Vector2 from, Vector2 along, Vector2 outward, float length ) {
		var found = new List<(float From, float To, float Depth)>();

		foreach ( var crossing in Returns ) {
			if ( MathF.Abs( Vector2.Dot( crossing.From - from, outward ) ) > 1f
				|| MathF.Abs( Vector2.Dot( crossing.To - from, outward ) ) > 1f ) {
				continue;
			}

			var start = Math.Clamp( Vector2.Dot( crossing.From - from, along ), 0f, length );
			var finish = Math.Clamp( Vector2.Dot( crossing.To - from, along ), 0f, length );

			if ( finish - start > 0.5f && crossing.Depth > 0.05f ) {
				found.Add( (start, finish, crossing.Depth) );
			}
		}

		return found.OrderBy( crossing => crossing.From );
	}

	void Band( ArchMesh canvas, IReadOnlyList<Vector2> loop, float shift, float depth, float bottom, float top, ArchBrush brush ) {
		var section = new ArchBandSection { Shift = shift, Depth = depth, Bottom = bottom, Top = top };

		foreach ( var run in Broken( loop, bottom ) ) {
			section.Emit( canvas, run, brush );
		}
	}

	// A plain projecting course as a section, so it sweeps as one solid like every moulding does.
	ArchProfile Squared( ArchCorniceLayer layer, float bite ) {
		var reach = layer.Projection + bite;
		var rise = layer.Lid( Kit ) - layer.Seat( Kit );

		return new ArchProfile {
			Name = "corona",
			Points = new List<Vector2> { new( 0f, 0f ), new( reach, 0f ), new( reach, rise ), new( 0f, rise ) }
		};
	}

	// The kit's own section stood off the centreline by the wall's half thickness, less a bite so its back is buried
	// in the elevation rather than landing on it. Copied, never shifted in place - the kit's profile is shared.
	static ArchProfile Standing( ArchProfile section, float shift ) {
		if ( section is not { IsUsable: true } ) {
			return null;
		}

		return new ArchProfile {
			Name = section.Name,
			Points = section.Points.Select( point => new Vector2( point.x + shift, point.y ) ).ToList(),
			Closed = section.Closed,
			Capped = section.Capped,
			MapAlongPath = section.MapAlongPath
		};
	}

	// Walked in REVERSE, because the outline is wound counter-clockwise and the extruder's right-hand side is the
	// inside of such a loop. A section authored standing proud of the wall would otherwise crown the roof deck.
	void Swept( ArchMesh canvas, ArchCorniceLayer layer, IReadOnlyList<Vector2> loop, ArchBrush brush, ArchProfile section ) {
		if ( section is not { IsUsable: true } ) {
			return;
		}

		foreach ( var run in Broken( loop, layer.Seat( Kit ) ) ) {
			var path = run.Raised();

			path.Reverse();

			canvas.Extrude( path, section, 1f, Rotation.Identity, brush, run.Closed );
		}
	}

	void Studded( ArchMesh canvas, ArchCorniceLayer layer, IReadOnlyList<Vector2> loop, float back, float bite, ArchBrush brush ) {
		var section = ArchProfiles.Named( Kit, layer.Course.Profile );
		var reach = layer.Projection + bite * 2f;

		foreach ( var run in Broken( loop, layer.Bottom ) ) {
			for ( var edge = 0; edge < run.Edges; edge++ ) {
				var from = run.At( edge );
				var along = run.At( edge + 1 ) - from;
				var span = along.Length;
				var head = CornerClearance( run, edge, reach );
				var tail = CornerClearance( run, edge + 1, reach );

				if ( span - head - tail < 1f ) {
					continue;
				}

				var frame = new ArchBandFrame {
					Origin = from,
					Along = along / span,
					Across = ArchRegion.Outward( from, run.At( edge + 1 ) )
				};

				ArchCorniceGen.Blocks( canvas, layer, Kit, frame, head, span - tail, back, reach, section, brush );
			}
		}
	}

	static float CornerClearance( ArchRunPath run, int corner, float reach ) {
		if ( !run.Continues( corner ) ) {
			return 0f;
		}

		var incoming = run.At( corner ) - run.At( corner - 1 );
		var outgoing = run.At( corner + 1 ) - run.At( corner );

		if ( incoming.Length < 0.5f || outgoing.Length < 0.5f ) {
			return 0f;
		}

		incoming = incoming.Normal;
		outgoing = outgoing.Normal;

		var turn = MathF.Abs( incoming.x * outgoing.y - incoming.y * outgoing.x );
		var opened = 1f + Vector2.Dot( incoming, outgoing );

		return opened < 0.05f ? reach : MathF.Min( reach, reach * turn / opened );
	}

	IEnumerable<ArchRunPath> Broken( IReadOnlyList<Vector2> loop, float seat ) {
		foreach ( var run in ArchBrokenRun.Of( loop, Abuts ).Level( seat ).Resolve() ) {
			foreach ( var surviving in ArchCut.Runs( Plan, Kit, Level, HostId, run, ArchCutAffects.Trims ) ) {
				yield return surviving;
			}
		}
	}

	// The corbel that carries the turn. A block row is held off every mitre, so the vertex is where a bracket goes -
	// deeper than the blocks and hung under the corona, which is the course the brackets are seen to be carrying.
	void Brackets( ArchMesh canvas, IReadOnlyList<Vector2> loop, IReadOnlyList<Vector2> outline, IReadOnlyList<ArchCorniceLayer> layers, float back ) {
		var bracket = Crown.Bracket;

		if ( !bracket.Stands || layers.Count == 0 ) {
			return;
		}

		// It stands in the BLOCK ROW's own band, which is the course whose vertex it is there to fill; Drop corbels it
		// further into the frieze. Hung anywhere else it is a solid driven through the mouldings behind it.
		var row = layers.FirstOrDefault( layer => layer.Course.Kind is CorniceCourse.Dentils or CorniceCourse.Modillions );
		var course = row.Course is null ? layers[0] : row;
		var brush = Style.Brush( bracket.Field, Chain );

		var corners = Turns( loop ).ToList();

		foreach ( var corner in Turns( outline ) ) {
			if ( corners.All( other => (other.At - corner.At).Length >= 0.5f
				|| Vector2.Dot( other.Incoming, corner.Incoming ) < 0.99f
				|| Vector2.Dot( other.Outgoing, corner.Outgoing ) < 0.99f ) ) {
				corners.Add( corner );
			}
		}

		foreach ( var corner in corners ) {
			Block( canvas, corner.At, corner.Incoming, corner.Outgoing, bracket.Width, back + bracket.Projection,
				course.Seat( Kit ) - bracket.Drop, course.Lid( Kit ), brush );
		}
	}

	// One at every corner of the ring, which is where a street block breaks its parapet line.
	void Piers( ArchMesh canvas, IReadOnlyList<Vector2> loop, float back ) {
		var pier = Crown.Pier;

		if ( !Steps || !pier.Stands ) {
			return;
		}

		var brush = Style.Brush( pier.Field, Chain );
		var cap = Style.Brush( ArchSurface.WallCap, Chain );
		var shaft = MathF.Max( 0.5f, pier.Rise - MathF.Max( 0f, pier.Cap ) );

		foreach ( var corner in Turns( loop ) ) {
			Block( canvas, corner.At, corner.Incoming, corner.Outgoing, pier.Width, back + pier.Projection,
				Head, Head + shaft, brush );

			if ( pier.Cap > 0.05f ) {
				Block( canvas, corner.At, corner.Incoming, corner.Outgoing, pier.Width + pier.CapOversail * 2f,
					back + pier.Projection + pier.CapOversail, Head + shaft, Head + pier.Rise, cap );
			}
		}
	}

	// Only where the loop actually turns. A stepped loop carries repeated and collinear points, and a block on one
	// of those is a corbel standing in the middle of an elevation.
	static IEnumerable<(Vector2 At, Vector2 Incoming, Vector2 Outgoing)> Turns( IReadOnlyList<Vector2> loop ) {
		for ( var corner = 0; corner < loop.Count; corner++ ) {
			var at = loop[corner];
			var incoming = Bearing( loop, corner, -1 );
			var outgoing = Bearing( loop, corner, 1 );

			if ( incoming.Length < 0.5f || outgoing.Length < 0.5f || Vector2.Dot( incoming, outgoing ) > 0.99f ) {
				continue;
			}

			yield return (at, incoming, outgoing);
		}
	}

	// The outward normal of the nearest edge with any length in it. A normal taken off a repeated point is NaN, and
	// one NaN corner takes every block on the ring with it.
	static Vector2 Bearing( IReadOnlyList<Vector2> loop, int corner, int step ) {
		var at = loop[corner];

		for ( var walk = 1; walk < loop.Count; walk++ ) {
			var other = loop[((corner + step * walk) % loop.Count + loop.Count) % loop.Count];

			if ( (other - at).Length > 0.5f ) {
				return step > 0 ? ArchRegion.Outward( at, other ) : ArchRegion.Outward( other, at );
			}
		}

		return Vector2.Zero;
	}

	// A block on a corner belongs to BOTH elevations, so each of its outer faces lines up with the course running
	// away from it. Laid along one edge instead it stands square to that one and runs half its width out past the
	// other's face, which is the overhang this replaced.
	static void Block( ArchMesh canvas, Vector2 at, Vector2 incoming, Vector2 outgoing, float width, float reach, float bottom, float top, ArchBrush brush ) {
		if ( top - bottom < 0.05f || width < 0.5f ) {
			return;
		}

		var outer = Meeting( at, incoming, outgoing, reach );
		var plan = ArchFootprint.Wind( new List<Vector2>
		{
			outer,
			outer - incoming * width,
			outer - (incoming + outgoing) * width,
			outer - outgoing * width
		} );

		var seat = plan.Select( point => new Vector3( point.x, point.y, bottom ) ).ToList();

		canvas.Prism( seat, seat.Select( point => point.WithZ( top ) ).ToList(), brush );
	}

	// Where the two offset faces actually cross, which is only the sum of the normals at a right angle. The same
	// opened denominator ArchBandGen mitres a board on.
	static Vector2 Meeting( Vector2 at, Vector2 incoming, Vector2 outgoing, float reach ) {
		var opened = 1f + Vector2.Dot( incoming, outgoing );

		return opened < 0.05f ? at + outgoing * reach : at + (incoming + outgoing) * (reach / opened);
	}
}