Editor/Services/ArchRun.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

namespace Sunless.Architecture;

public readonly struct ArchStation {
	public Vector2 Point { get; init; }
	public Vector2 Along { get; init; }
	public Vector2 Outward { get; init; }
	public float Height { get; init; }
	public float Distance { get; init; }

	public Vector3 Raised => new( Point.x, Point.y, Height );

	public static ArchStation Between( Vector2 from, Vector2 to, float along, float height, float distance ) {
		var span = to - from;
		var length = span.Length;
		var direction = length < 0.05f ? Vector2.Zero : span / length;

		return new ArchStation {
			Point = from + direction * along,
			Along = direction,
			Outward = ArchRegion.Outward( from, to ),
			Height = height,
			Distance = distance
		};
	}
}

// Open ends are where the run was cut - a corner must not pass one.
public sealed class ArchRunPath {
	public List<Vector2> Points { get; init; } = new();
	public bool Closed { get; init; }
	// Runs are level; anything that rakes is a swept profile, not a run.
	public float Height { get; init; }

	public int Edges => Closed ? Points.Count : Points.Count - 1;

	public Vector2 At( int index ) => Points[(index % Points.Count + Points.Count) % Points.Count];

	public bool Continues( int corner ) => Closed || (corner > 0 && corner < Points.Count - 1);

	public List<Vector3> Raised() => Raised( Height );

	public List<Vector3> Raised( float height ) => Points.Select( point => new Vector3( point.x, point.y, height ) ).ToList();

	public static ArchRunPath Of( IReadOnlyList<Vector2> points, float height = 0f, bool closed = false ) {
		return new ArchRunPath { Points = points.ToList(), Closed = closed, Height = height };
	}

	public static ArchRunPath Between( Vector2 from, Vector2 to, float height = 0f ) {
		return new ArchRunPath { Points = new List<Vector2> { from, to }, Height = height };
	}

	public static bool IsClosed( IReadOnlyList<Vector3> points ) {
		return points.Count >= 4 && (points[0] - points[^1]).Length < 0.05f;
	}

	// Detect closure, drop the duplicated closing point.
	public static ArchRunPath Normalized( IReadOnlyList<Vector3> raised, float height ) {
		var closed = IsClosed( raised );

		return new ArchRunPath {
			Points = (closed ? raised.Take( raised.Count - 1 ) : raised)
				.Select( point => new Vector2( point.x, point.y ) )
				.ToList(),
			Closed = closed,
			Height = height
		};
	}

	public static ArchRunPath Normalized( IReadOnlyList<Vector2> points, float height ) {
		return Normalized( points.Select( point => new Vector3( point.x, point.y, height ) ).ToList(), height );
	}

	public float Length {
		get {
			var total = 0f;

			for ( var edge = 0; edge < Edges; edge++ ) {
				total += (At( edge + 1 ) - At( edge )).Length;
			}

			return total;
		}
	}

	public bool Station( float distance, out ArchStation station ) {
		var travelled = 0f;

		for ( var edge = 0; edge < Edges; edge++ ) {
			var from = At( edge );
			var to = At( edge + 1 );
			var length = (to - from).Length;

			if ( length >= 0.05f && travelled + length >= distance ) {
				station = ArchStation.Between( from, to, distance - travelled, Height, distance );

				return true;
			}

			travelled += length;
		}

		station = default;

		return false;
	}

	public bool Nearest( Vector2 point, out ArchStation station, out float gap ) {
		station = default;
		gap = float.MaxValue;

		var travelled = 0f;

		for ( var edge = 0; edge < Edges; edge++ ) {
			var from = At( edge );
			var to = At( edge + 1 );
			var length = (to - from).Length;

			if ( length < 0.5f ) {
				travelled += length;
				continue;
			}

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

			if ( reach < gap ) {
				gap = reach;
				station = ArchStation.Between( from, to, along, Height, travelled + along );
			}

			travelled += length;
		}

		return gap < float.MaxValue;
	}

	// Outer loops wind counter-clockwise; holes wind the other way and read as reflex.
	public bool Convex( int corner ) {
		if ( !Continues( corner ) ) {
			return false;
		}

		var into = At( corner ) - At( corner - 1 );
		var outOf = At( corner + 1 ) - At( corner );

		return into.x * outOf.y - into.y * outOf.x > 0f;
	}
}

// The one place that walks a run for its breaks - abutments, stairwells, doorways.
public sealed class ArchRun {
	readonly List<Vector2> source;
	readonly bool closed;
	readonly List<Func<Vector2, Vector2, bool>> breaks = new();

	float sample;
	float height;

	ArchRun( IEnumerable<Vector2> points, bool closed ) {
		source = points.ToList();
		this.closed = closed;
	}

	public static ArchRun Around( IReadOnlyList<Vector2> loop ) => new( loop, true );

	public static ArchRun Along( IReadOnlyList<Vector2> path ) => new( path, false );

	public ArchRun BreakWhere( Func<Vector2, Vector2, bool> blocked ) {
		if ( blocked is not null ) {
			breaks.Add( blocked );
		}

		return this;
	}

	public ArchRun BreakOver( IReadOnlyList<ArchFloorCutout> holes ) {
		if ( holes is { Count: > 0 } ) {
			breaks.Add( ( a, b ) => holes.Any( hole => hole.Contains( (a + b) * 0.5f ) ) );
		}

		return this;
	}

	// Extra points so a break can land mid-wall; corners stay exact, so mitres are untouched.
	public ArchRun Sampled( float step ) {
		sample = step;

		return this;
	}

	public ArchRun Level( float at ) {
		height = at;

		return this;
	}

	public void Emit( ArchMesh canvas, ArchRunSection section, ArchBrush brush ) {
		foreach ( var path in Resolve() ) {
			section.Emit( canvas, path, brush );
		}
	}

	public List<ArchRunPath> Resolve() {
		var points = sample > 0.01f ? Subdivided() : source;

		if ( points.Count < 2 || (closed && points.Count < 3) ) {
			return new List<ArchRunPath>();
		}

		var edges = closed ? points.Count : points.Count - 1;
		var blocked = new bool[edges];
		var any = false;

		for ( var edge = 0; edge < edges; edge++ ) {
			var a = points[edge];
			var b = points[(edge + 1) % points.Count];

			blocked[edge] = breaks.Any( test => test( a, b ) );
			any |= blocked[edge];
		}

		if ( !any ) {
			return new List<ArchRunPath> { Path( points.ToList(), closed ) };
		}

		if ( blocked.All( entry => entry ) ) {
			return new List<ArchRunPath>();
		}

		// Walk from the first break so a stretch spanning the seam comes out whole.
		var first = closed ? Array.IndexOf( blocked, true ) : 0;
		var paths = new List<ArchRunPath>();
		var open = new List<Vector2>();

		for ( var step = 0; step < edges; step++ ) {
			var edge = (first + step) % edges;

			if ( blocked[edge] ) {
				Close( paths, open );
				open = new List<Vector2>();
				continue;
			}

			if ( open.Count == 0 ) {
				open.Add( points[edge] );
			}

			open.Add( points[(edge + 1) % points.Count] );
		}

		Close( paths, open );

		return paths;
	}

	void Close( List<ArchRunPath> paths, List<Vector2> open ) {
		if ( open.Count >= 2 ) {
			paths.Add( Path( open, false ) );
		}
	}

	// Sampling is only for breaks - collinear sample points must not survive into the path.
	ArchRunPath Path( List<Vector2> points, bool closed ) {
		return new ArchRunPath { Points = Straightened( points, closed ), Closed = closed, Height = height };
	}

	static List<Vector2> Straightened( List<Vector2> points, bool closed ) {
		if ( points.Count < 3 ) {
			return points;
		}

		var kept = new List<Vector2>();

		for ( var index = 0; index < points.Count; index++ ) {
			// Open ends were cut, so they stay whatever angle they sit at.
			if ( !closed && (index == 0 || index == points.Count - 1) ) {
				kept.Add( points[index] );
				continue;
			}

			var into = (points[index] - points[(index - 1 + points.Count) % points.Count]).Normal;
			var outOf = (points[(index + 1) % points.Count] - points[index]).Normal;

			if ( MathF.Abs( into.x * outOf.y - into.y * outOf.x ) > 0.001f ) {
				kept.Add( points[index] );
			}
		}

		return kept.Count >= (closed ? 3 : 2) ? kept : points;
	}

	List<Vector2> Subdivided() {
		var points = new List<Vector2>();
		var edges = closed ? source.Count : source.Count - 1;

		for ( var edge = 0; edge < edges; edge++ ) {
			var a = source[edge];
			var b = source[(edge + 1) % source.Count];
			var length = (b - a).Length;

			points.Add( a );

			if ( length < 0.05f ) {
				continue;
			}

			var direction = (b - a) / length;

			foreach ( var offset in ArchDivide.AtMost( length, sample ).Inner ) {
				points.Add( a + direction * offset );
			}
		}

		if ( !closed ) {
			points.Add( source[^1] );
		}

		return points;
	}
}