Editor/Pillar/ArchPillarSnap.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

namespace Sunless.Architecture;

// Seats a column against a snap target rather than centring on it — direction comes from the author's aim.
public static class ArchPillarSnap {
	public static Vector2 Seat( ArchPlan plan, ArchKit kit, ArchCursor cursor, ArchPillarPart section ) {
		if ( section is null || cursor.Snap == ArchSnapKind.None ) {
			return cursor.Plan;
		}

		// Under a soffit the capital must land flush; elsewhere the widest course must not overhang.
		var overhead = cursor.Face == ArchCursorFace.Ceiling || cursor.SnapAbove;
		var reach = overhead ? section.Heading : section.Seated;
		// Walls snap on centreline (need half-thickness offset); outlines are already the outer face.
		var skin = cursor.SnappedTo is { } wall ? ArchWallSection.Thickness( wall, kit ) * 0.5f : 0f;
		var inside = Inside( plan, kit, cursor );
		var step = MathF.Max( 1f, MathF.Max( reach.x, reach.y ) );

		if ( cursor.Snap == ArchSnapKind.Corner ) {
			var toward = Inward( inside, cursor.Plan, step, cursor.Free );

			return cursor.Plan + new Vector2( toward.x * (reach.x - skin), toward.y * (reach.y - skin) );
		}

		var heading = cursor.SnapHeading.Length > 0.001f ? cursor.SnapHeading : cursor.SnappedTo?.Direction ?? Vector2.Zero;

		if ( heading.Length < 0.001f ) {
			return cursor.Plan;
		}

		var normal = new Vector2( -heading.y, heading.x );
		var across = MathF.Abs( normal.x ) > MathF.Abs( normal.y ) ? reach.x : reach.y;
		var side = ArchFootprint.Encloses( inside, cursor.Plan + normal * step ) ? 1f
			: ArchFootprint.Encloses( inside, cursor.Plan - normal * step ) ? -1f
			: Toward( Vector2.Dot( cursor.Free - cursor.Plan, normal ) );

		return cursor.Plan + normal * side * (across - skin);
	}

	// Which diagonal is "inside" comes from the plan; falls back to the author's aim if nothing encloses.
	static Vector2 Inward( IReadOnlyList<List<Vector2>> inside, Vector2 corner, float step, Vector2 free ) {
		foreach ( var toward in Diagonals ) {
			if ( ArchFootprint.Encloses( inside, corner + toward * step ) ) {
				return toward;
			}
		}

		return new Vector2( Toward( free.x - corner.x ), Toward( free.y - corner.y ) );
	}

	static readonly Vector2[] Diagonals =
	{
		new( 1f, 1f ),
		new( -1f, 1f ),
		new( -1f, -1f ),
		new( 1f, -1f )
	};

	// Under an overhang use that slab's outline; otherwise union all storeys (stacked plots cancel in Encloses).
	static List<List<Vector2>> Inside( ArchPlan plan, ArchKit kit, ArchCursor cursor ) {
		if ( cursor.Surface is { Count: >= 3 } surface ) {
			return new List<List<Vector2>> { surface.ToList() };
		}

		var rooms = plan?.AllRooms() ?? Enumerable.Empty<ArchRoom>();

		return ArchFootprint.Union( ArchRegion.Shell( ArchRegion.Footprints( rooms ), kit.WallThickness ) );
	}

	// A free point sitting exactly on the snap says nothing, so the section stays where the grid put it.
	static float Toward( float offset ) => MathF.Abs( offset ) < 0.01f ? 0f : MathF.Sign( offset );
}