Editor/Services/ArchFixtureAnchor.cs
using System;
using System.Collections.Generic;
using Sandbox;

namespace Sunless.Architecture;

// Exterior wall wins over a nearer partition; free-standing walls use click side.
public readonly record struct ArchFixtureStation(
	ArchWall Wall,
	ArchBuilding Building,
	ArchRoom Room,
	Vector2 Outward,
	float Along,
	float Width,
	bool Dragged ) {
	public Vector2 Centre => Wall.PointAt( Along );
}

public static class ArchFixtureAnchor {
	public const float Reach = 240f;

	public const float Least = 12f;

	public static ArchWall Nearest(
		ArchPlan plan,
		IEnumerable<ArchBuilding> buildings,
		int? level,
		Vector2 point,
		out ArchBuilding host,
		out ArchRoom room,
		out float along,
		out Vector2 outward,
		bool exteriorFirst = true,
		float reach = Reach ) {
		host = null;
		room = null;
		along = 0f;
		outward = default;

		if ( buildings is null ) {
			return null;
		}

		var kinds = ArchKinds.Load();
		var found = new Candidate();
		var loose = new Candidate();

		foreach ( var building in buildings ) {
			foreach ( var candidateRoom in building.Rooms ) {
				if ( level is { } storey && candidateRoom.Floor != storey ) {
					continue;
				}

				foreach ( var wall in plan.Filed<ArchWall>( candidateRoom, kinds ) ) {
					Consider( building, candidateRoom, wall, point, ref found, ref loose );
				}
			}
		}

		var best = exteriorFirst
			? found.Wall is not null ? found : loose
			: Nearer( found, loose );

		if ( best.Wall is null || best.Gap > reach ) {
			return null;
		}

		host = best.Building;
		room = best.Room;
		along = best.Along;
		outward = best.Outward;

		return best.Wall;
	}

	// Clamp both ends into the run BEFORE measuring width; a short drag reads as a click.
	public static ArchFixtureStation? Station(
		IEnumerable<ArchBuilding> buildings,
		int? level,
		ArchGridService grid,
		Vector2 from,
		Vector2 to,
		float typedWidth,
		ArchPlan plan = null,
		bool exteriorFirst = true,
		float reach = Reach ) {
		var wall = Nearest( plan, buildings, level, (from + to) * 0.5f, out var host, out var room, out _, out var outward,
			exteriorFirst, reach );

		if ( wall is null || room is null ) {
			return null;
		}

		var run = wall.Length;
		var usable = MathF.Max( Least, run );
		var start = Math.Clamp( Vector2.Dot( from - wall.Start, wall.Direction ), 0f, run );
		var finish = Math.Clamp( Vector2.Dot( to - wall.Start, wall.Direction ), 0f, run );

		if ( finish < start ) {
			(start, finish) = (finish, start);
		}

		var dragged = finish - start >= grid.SubgridSize();
		var width = dragged
			? finish - start
			: Math.Clamp( typedWidth > Least ? typedWidth : Least, Least, usable );

		if ( dragged ) {
			start = Snapped( wall, grid, start );
			finish = Snapped( wall, grid, finish );
			width = MathF.Max( Least, finish - start );
		} else {
			width = MathF.Max( Least, grid.Subgrid( width, 4 ) );
			start = Math.Clamp( Vector2.Dot( from - wall.Start, wall.Direction ) - width * 0.5f, 0f, MathF.Max( 0f, run - width ) );
		}

		width = MathF.Min( width, usable );

		var along = Math.Clamp( start + width * 0.5f, width * 0.5f, MathF.Max( width * 0.5f, run - width * 0.5f ) );

		return new ArchFixtureStation( wall, host, room, outward, along, width, dragged );
	}

	// The click form: one point, and the width comes from what was typed.
	public static ArchFixtureStation? At( IEnumerable<ArchBuilding> buildings, int? level, ArchGridService grid, Vector2 point, float typedWidth, ArchPlan plan = null, bool exteriorFirst = true ) {
		return Station( buildings, level, grid, point, point, typedWidth, plan, exteriorFirst );
	}

	// Snap to neighbouring opening/fixture edges first, grid second.
	static float Snapped( ArchWall wall, ArchGridService grid, float along ) {
		var reach = grid.SubgridSize();
		var best = float.MaxValue;
		var landed = along;

		foreach ( var station in Neighbours( wall ) ) {
			var gap = MathF.Abs( station - along );

			if ( gap < reach && gap < best ) {
				best = gap;
				landed = station;
			}
		}

		return best < float.MaxValue ? landed : grid.Subgrid( along, 4 );
	}

	static IEnumerable<float> Neighbours( ArchWall wall ) {
		foreach ( var opening in wall.Openings ) {
			var half = opening.Width * 0.5f;

			yield return opening.Offset - half;
			yield return opening.Offset + half;
		}
	}

	static void Consider( ArchBuilding building, ArchRoom room, ArchWall wall, Vector2 point, ref Candidate found, ref Candidate loose ) {
		var length = wall.Length;

		if ( length < 0.5f ) {
			return;
		}

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

		if ( ArchWallFaces.TryOutward( wall, room, building, out var outward ) ) {
			found.Take( building, room, wall, along, gap, outward );

			return;
		}

		loose.Take( building, room, wall, along, gap, Clicked( wall, along, point ) );
	}

	static Candidate Nearer( Candidate one, Candidate other ) {
		if ( one.Wall is null ) {
			return other;
		}

		return other.Wall is not null && other.Gap < one.Gap ? other : one;
	}

	static Vector2 Clicked( ArchWall wall, float along, Vector2 point ) {
		var normal = wall.Normal;

		return Vector2.Dot( normal, point - wall.PointAt( along ) ) < 0f ? -normal : normal;
	}

	struct Candidate {
		public ArchBuilding Building;
		public ArchRoom Room;
		public ArchWall Wall;
		public float Along;
		public float Gap;
		public Vector2 Outward;

		public void Take( ArchBuilding building, ArchRoom room, ArchWall wall, float along, float gap, Vector2 outward ) {
			if ( Wall is not null && gap >= Gap ) {
				return;
			}

			Building = building;
			Room = room;
			Wall = wall;
			Along = along;
			Gap = gap;
			Outward = outward;
		}
	}
}