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

namespace Sunless.Architecture;

// One pick for every caller: flights by footprint, not by their far-away origin.
public static class ArchPick {
	static IEnumerable<ArchRoom> Storey( ArchPlan plan, int level ) {
		return plan?.Buildings.SelectMany( building => building.Rooms ).Where( room => room.Floor == level )
			?? Enumerable.Empty<ArchRoom>();
	}

	// By its rectangle, not its origin: the origin sits at the wall end.
	public static ArchApproachPart ApproachIn( ArchRoom room, Vector2 point ) {
		return room?.Approaches.FirstOrDefault( approach => Covers( approach, point ) );
	}

	public static ArchApproachPart ApproachUnder( ArchPlan plan, int level, Vector2 point, out ArchRoom room ) {
		room = null;

		foreach ( var candidate in Storey( plan, level ) ) {
			if ( ApproachIn( candidate, point ) is not { } approach ) {
				continue;
			}

			room = candidate;

			return approach;
		}

		return null;
	}

	static bool Covers( ArchApproachPart approach, Vector2 point ) {
		if ( approach.IsSpline ) {
			return approach.Curve().Nearest( point, out var frame, out var gap )
				&& gap <= MathF.Max( 12f, approach.Width * frame.WidthScale ) * 0.5f + 4f;
		}

		var outward = ArchApproachAxes.Outward( approach );
		var across = new Vector2( -outward.y, outward.x );
		var local = point - approach.Origin;

		var along = Vector2.Dot( local, outward );
		var side = MathF.Abs( Vector2.Dot( local, across ) );

		return along >= -4f && along <= MathF.Max( 12f, approach.Run ) + 4f && side <= approach.Width * 0.5f + 4f;
	}

	// Wound first: an authored outline can arrive either way round.
	public static ArchRoofPart RoofUnder( ArchBuilding building, int level, Vector2 point ) {
		return building?.Roofs.FirstOrDefault( roof =>
			roof.Level == level && ArchFootprint.Contains( ArchFootprint.Wind( roof.Outline() ), point ) );
	}

	public static ArchRoofPart RoofUnder( ArchPlan plan, int level, Vector2 point, out ArchBuilding owner ) {
		owner = null;

		foreach ( var building in plan?.Buildings ?? new List<ArchBuilding>() ) {
			if ( RoofUnder( building, level, point ) is not { } roof ) {
				continue;
			}

			owner = building;

			return roof;
		}

		return null;
	}

	public static IReadOnlyList<ArchHit> HitsAt( ArchLayerTree tree, ArchKit kit, int level, Vector2 point, int limit = 12 ) {
		var hits = new List<ArchHit>();
		var kinds = ArchKinds.Load();

		if ( tree is not null ) {
			foreach ( var group in tree.Domains ) {
				foreach ( var root in group.Children ) {
					Collect( root, kit, level, point, hits, kinds );
				}
			}
		}

		// Groups are pickable but rank below their members.
		foreach ( var group in hits
			.Select( hit => tree?.Find( hit.Layer.ItemId ) )
			.Select( GroupAbove )
			.Where( node => node is not null )
			.Distinct()
			.ToList() ) {
			hits.Add( Hit( group, ArchHitChannel.Boundary, 0f, false ) );
		}

		hits.Sort( ( left, right ) => {
			var rank = Rank( kinds, left ).CompareTo( Rank( kinds, right ) );

			return rank != 0 ? rank : left.Distance.CompareTo( right.Distance );
		} );

		return hits.Take( Math.Max( 1, limit ) ).ToList();
	}

	static void Collect( ArchLayerNode node, ArchKit kit, int level, Vector2 point, List<ArchHit> hits, ArchKinds kinds ) {
		if ( node is null ) {
			return;
		}

		if ( node.Payload is { } payload ) {
			if ( ArchPickers.Load().For( node.Kind ) is { } picker ) {
				picker.Under( new ArchPicking( node, kit, level, point, hits ) );
			} else
				switch ( payload ) {
					case ArchBuilding building:
						if ( ArchHandles.Bounds( building, out var boundsMin, out var boundsMax )
							&& point.x >= boundsMin.x && point.x <= boundsMax.x
							&& point.y >= boundsMin.y && point.y <= boundsMax.y ) {
							hits.Add( Hit( node, ArchHitChannel.Boundary, 0f, false ) );
						}
						break;

					// A spanning room is a Walkway; the rank, not the case, decides which it outranks.
					case ArchRoom room:
						var loop = ArchFloorGen.Footprint( room );

						if ( loop.Count >= 3 && ArchFootprint.Contains( loop, point ) ) {
							hits.Add( Hit( node, ArchHitChannel.Footprint, 0f, true ) );
						}
						break;

					case ArchWall wall:
						WallHit( node, wall, point, hits );
						break;

					case ArchOpening opening:
						if ( node.Parent?.Payload is ArchWall owner ) {
							StationHit( node, owner, opening.Left, opening.Right, point, hits );
						}
						break;

					case ArchWallModPart modifier:
						if ( node.Parent?.Payload is ArchWall host ) {
							StationHit( node, host, modifier.Left, modifier.Right, point, hits );
						}
						break;

					case ArchApproachPart approach when node.Room is not null:
						if ( ApproachIn( node.Room, point ) == approach ) {
							hits.Add( Hit( node, ArchHitChannel.Footprint, 0f, true ) );
						}
						break;

					case ArchFencePart fence:
						if ( fence.Curve().Nearest( point, out _, out var fenceGap )
							&& fenceGap <= MathF.Max( 36f, fence.Barrier.PostSize * 4f ) ) {
							hits.Add( Hit( node, ArchHitChannel.Path, fenceGap, true ) );
						}
						break;

					case ArchCutPart cut when cut.Level == level:
						if ( cut.Outlines().Any( outline => ArchFootprint.Contains( outline, point ) ) ) {
							hits.Add( Hit( node, ArchHitChannel.Footprint, 0f, true ) );
						}
						break;

					case ArchPlatformPart platform when platform.Level == level:
						if ( ArchFootprint.Contains( platform.Outline(), point ) ) {
							hits.Add( Hit( node, ArchHitChannel.Footprint, 0f, true ) );
						}
						break;

					case ArchBeamPart beam when beam.Level == level:
						if ( ArchFootprint.Contains( beam.Outline(), point ) ) {
							hits.Add( Hit( node, ArchHitChannel.Footprint, 0f, true ) );
						}
						break;

					case ArchDownpipePart pipe:
						var pipeGap = (point - pipe.Wall).Length;

						if ( pipeGap < 96f ) {
							hits.Add( Hit( node, ArchHitChannel.Anchor, pipeGap, false ) );
						}
						break;

					case ArchRoofPart roof when roof.Level == level:
						if ( node.Building is { } roofOwner && RoofUnder( roofOwner, level, point ) == roof ) {
							hits.Add( Hit( node, ArchHitChannel.Footprint, 0f, true ) );
						}
						break;

				}
		}

		foreach ( var child in node.Children ) {
			Collect( child, kit, level, point, hits, kinds );
		}
	}

	static void StationHit( ArchLayerNode node, ArchWall wall, float left, float right, Vector2 point, List<ArchHit> hits ) {
		var length = wall.Length;

		if ( length < 0.5f ) {
			return;
		}

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

		if ( gap < 96f && at >= left && at <= right ) {
			hits.Add( Hit( node, ArchHitChannel.Span, gap, false ) );
		}
	}

	static void WallHit( ArchLayerNode node, ArchWall wall, Vector2 point, List<ArchHit> hits ) {
		if ( wall.Length < 0.5f ) {
			return;
		}

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

		if ( gap < 96f ) {
			hits.Add( Hit( node, ArchHitChannel.Boundary, gap, false ) );
		}
	}

	static ArchLayerNode GroupAbove( ArchLayerNode node ) {
		for ( var current = node?.Parent; current is not null; current = current.Parent ) {
			if ( current.Kind == ArchKind.Assembly ) {
				return current;
			}
		}

		return null;
	}

	internal static ArchLayerNode Child( ArchLayerNode parent, object payload ) {
		return parent?.Children.FirstOrDefault( child => ReferenceEquals( child.Payload, payload ) );
	}

	internal static ArchHit Hit( ArchLayerNode node, ArchHitChannel channel, float distance, bool direct ) {
		return new ArchHit {
			Layer = node.Ref ?? new ArchLayerRef { ItemId = -1, Kind = node.Kind },
			Channel = channel,
			Distance = distance,
			Depth = Depth( node ),
			Direct = direct,
		};
	}

	static int Depth( ArchLayerNode node ) {
		var depth = 0;

		for ( var current = node.Parent; current is not null; current = current.Parent ) {
			depth++;
		}

		return depth;
	}

	static int Rank( ArchKinds kinds, ArchHit hit ) {
		var kind = hit.Layer.Kind;

		if ( kind == ArchKind.Assembly ) {
			return -1;
		}

		if ( hit.Direct ) {
			return kinds.For( kind )?.DirectRank ?? 200;
		}

		return kinds.For( kind )?.IndirectRank ?? 999;
	}
}