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

namespace Sunless.Architecture;

public enum ArchSnapKind {
	None,
	Corner,
	Face
}

public readonly struct ArchWallSnapped {
	public ArchSnapKind Kind { get; init; }
	public Vector2 Point { get; init; }
	public ArchWall Wall { get; init; }
	public Vector2 Heading { get; init; }
	public Vector2 Start { get; init; }
	public float Length { get; init; }
	// Line belongs to geometry above this storey — snapped head aligns to it, not base
	public bool Above { get; init; }
	public float Distance { get; init; }

	public bool Took => Kind != ArchSnapKind.None;
}

// Separate reach for corners (a point) vs faces (a line) — different targets need different bands
public readonly record struct ArchSnapReach( float Corner, float Face ) {
	public bool Any => Corner > 0.01f || Face > 0.01f;

	public static readonly ArchSnapReach None = default;
}

// Corners win over faces at equal reach — a corner is an authored point, a face is just a line
public static class ArchWallSnap {
	const float Span = 20f;

	// Generous band for flush placement (clicks); drags use Reach() instead or they lose their across component
	public static ArchSnapReach Flush( float authored ) {
		var band = authored > 0.01f ? authored : MathF.Max( Span, new ArchGridService().SubgridSize() );

		return new ArchSnapReach( band * 2.5f, band * 5f );
	}

	public static float Unstated => new ArchGridService().SubgridSize();

	public static ArchSnapReach Reach( float authored ) {
		var band = authored > 0.01f ? authored : Unstated;

		return new ArchSnapReach( band, band );
	}

	public static IEnumerable<ArchWall> On( ArchPlan plan, int level, ArchRoofPart deck ) {
		if ( plan is null ) {
			return Enumerable.Empty<ArchWall>();
		}

		return deck is not null ? deck.Walls : plan.WallsOn( level );
	}

	public static ArchWallSnapped Nearest( IEnumerable<ArchWall> walls, Vector2 point, ArchSnapReach reach ) {
		if ( walls is null || !reach.Any ) {
			return default;
		}

		var corner = default( ArchWallSnapped );
		var face = default( ArchWallSnapped );

		foreach ( var wall in walls ) {
			var length = wall.Length;

			if ( length < ArchGridService.LeastWallLength ) {
				continue;
			}

			var line = new ArchSnapLine( wall.Start, wall.Direction, length, wall, false );

			Closer( ref corner, ArchSnapKind.Corner, wall.Start, line, reach.Corner, point );
			Closer( ref corner, ArchSnapKind.Corner, wall.End, line, reach.Corner, point );

			var along = Math.Clamp( Vector2.Dot( point - wall.Start, wall.Direction ), 0f, length );

			Closer( ref face, ArchSnapKind.Face, wall.PointAt( along ), line, reach.Face, point );
		}

		return corner.Took ? corner : face;
	}

	public static ArchWallSnapped Nearest( IEnumerable<ArchSnapLoop> loops, Vector2 point, ArchSnapReach reach ) {
		if ( loops is null || !reach.Any ) {
			return default;
		}

		var corner = default( ArchWallSnapped );
		var face = default( ArchWallSnapped );

		foreach ( var edge in loops ) {
			var loop = edge.Loop;

			if ( loop is null || loop.Count < 2 ) {
				continue;
			}

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

				if ( length < ArchGridService.LeastWallLength ) {
					continue;
				}

				var heading = span / length;
				var line = new ArchSnapLine( from, heading, length, null, edge.Above );
				var travelled = Math.Clamp( Vector2.Dot( point - from, heading ), 0f, length );

				Closer( ref corner, ArchSnapKind.Corner, from, line, reach.Corner, point );
				Closer( ref face, ArchSnapKind.Face, from + heading * travelled, line, reach.Face, point );
			}
		}

		return corner.Took ? corner : face;
	}

	// Measured to the near FACE, not the centreline: on mixed thicknesses that reorders the candidates against
	// ArchTool.NearestWall, and the face is what a part seated flush against a wall lands on.
	public static ArchWall NearestWallFace( IEnumerable<ArchRoom> rooms, Vector2 point, float unstatedThickness, out float distance, out Vector2 onFace ) {
		distance = float.MaxValue;
		onFace = default;
		ArchWall best = null;

		foreach ( var room in rooms ) {
			foreach ( var wall in room.Walls ) {
				var span = wall.End - wall.Start;
				var length = span.Length;

				if ( length < 1f ) {
					continue;
				}

				var travelled = Math.Clamp( Vector2.Dot( point - wall.Start, span ) / (length * length), 0f, 1f );
				var at = wall.Start + span * travelled;
				var side = point - at;
				var thickness = wall.Thickness > 0.1f ? wall.Thickness : unstatedThickness;
				var face = at + (side.IsNearZeroLength ? wall.Normal : side.Normal) * thickness * 0.5f;
				var gap = (face - point).Length;

				if ( gap < distance ) {
					distance = gap;
					onFace = face;
					best = wall;
				}
			}
		}

		return best;
	}

	// Corners win over faces across both walls and loops
	public static ArchWallSnapped Nearest( IEnumerable<ArchWall> walls, IEnumerable<ArchSnapLoop> loops, Vector2 point, ArchSnapReach reach ) {
		var onWalls = Nearest( walls, point, reach );
		var onEdges = Nearest( loops, point, reach );

		if ( !onWalls.Took || !onEdges.Took ) {
			return onWalls.Took ? onWalls : onEdges;
		}

		if ( onWalls.Kind != onEdges.Kind ) {
			return onWalls.Kind == ArchSnapKind.Corner ? onWalls : onEdges;
		}

		return onWalls.Distance <= onEdges.Distance ? onWalls : onEdges;
	}

	readonly record struct ArchSnapLine( Vector2 Start, Vector2 Heading, float Length, ArchWall Wall, bool Above );

	static void Closer( ref ArchWallSnapped best, ArchSnapKind kind, Vector2 at, ArchSnapLine line, float reach, Vector2 point ) {
		var distance = (point - at).Length;

		if ( distance > reach || (best.Took && distance >= best.Distance) ) {
			return;
		}

		best = new ArchWallSnapped {
			Kind = kind,
			Point = at,
			Wall = line.Wall,
			Heading = line.Heading,
			Start = line.Start,
			Length = line.Length,
			Above = line.Above,
			Distance = distance
		};
	}

	public static string Describe( ArchSnapKind kind ) => kind switch {
		ArchSnapKind.Corner => "corner",
		ArchSnapKind.Face => "wall",
		_ => null
	};
}