Editor/Tool/ArchWorkPlane.cs
using System;
using Sandbox;

namespace Sunless.Architecture;

public enum ArchCursorFace {
	Floor,
	Ceiling
}

public readonly struct ArchCursor {
	public bool Found { get; init; }
	public Vector3 World { get; init; }
	public Vector2 Plan { get; init; }
	public ArchCursorFace Face { get; init; }
	// Pre-snap ray hit
	public Vector2 Free { get; init; }
	public ArchSnapKind Snap { get; init; }
	public ArchWall SnappedTo { get; init; }
	public Vector2 SnapHeading { get; init; }
	// Snapped to a surface ABOVE this storey — align head, not base
	public bool SnapAbove { get; init; }
	public IReadOnlyList<Vector2> Surface { get; init; }
	public float Height { get; init; }
	public ArchViewAxis Axis { get; init; }
	public bool OnGround { get; init; }
	public ArchRoofPart OnDeck { get; init; }
	public ArchPlatformPart OnPlatform { get; init; }

	public bool InElevation => Axis is ArchViewAxis.Front or ArchViewAxis.Side;
}

public readonly struct ArchWorkPlane {
	public ArchViewAxis Axis { get; init; }
	public float Depth { get; init; }
	public float Level { get; init; }
	public float Soffit { get; init; }
	// Locks the work plane to the surface the gesture began on
	public float? Standing { get; init; }
	public ArchRoofPart StandingDeck { get; init; }
	public ArchPlan Plan { get; init; }
	public ArchKit Kit { get; init; }
	public int Storey { get; init; }
	public ArchSnapReach WallReach { get; init; }

	public static ArchWorkPlane For( ArchTool tool ) {
		var axis = tool.Axis;

		return new ArchWorkPlane {
			Axis = axis == ArchViewAxis.Free ? ArchViewAxis.Top : axis,
			Depth = axis is ArchViewAxis.Front or ArchViewAxis.Side ? tool.Depth( axis ) : tool.LevelHeight,
			Level = tool.LevelHeight,
			Soffit = tool.Overhead(),
			Standing = tool.Standing,
			StandingDeck = tool.StandingDeck,
			Plan = tool.Plan,
			Kit = tool.Kit,
			Storey = tool.Level,
			WallReach = tool.SnapReach()
		};
	}

	// Walls snap ahead of grid — decks use only their own walls, ground adds storey edges
	ArchWallSnapped Settle( ArchGridService grid, Vector2 free, ArchRoofPart deck, out Vector2 snapped ) {
		var walls = ArchWallSnap.On( Plan, Storey, deck );

		return deck is not null
			? Settled( grid, free, ArchWallSnap.Nearest( walls, free, WallReach ), out snapped )
			: Settled( grid, free, ArchWallSnap.Nearest( walls, ArchStoreyEdges.On( Plan, Kit, Storey ), free, WallReach ), out snapped );
	}

	ArchWallSnapped Settled( ArchGridService grid, Vector2 free, IEnumerable<ArchSnapLoop> offered, out Vector2 snapped ) {
		return Settled( grid, free, ArchWallSnap.Nearest( offered, free, WallReach ), out snapped );
	}

	// Face snaps: flush across the wall, grid-snapped along it
	ArchWallSnapped Settled( ArchGridService grid, Vector2 free, ArchWallSnapped found, out Vector2 snapped ) {
		if ( !found.Took ) {
			snapped = grid.Base( free );

			return found;
		}

		if ( found.Kind != ArchSnapKind.Face || found.Heading.Length < 0.001f ) {
			snapped = found.Point;

			return found;
		}

		// Clamped to the snap line so it can't overshoot the wall's end
		var travelled = Math.Clamp( Vector2.Dot( grid.Base( free ) - found.Start, found.Heading ), 0f, found.Length );

		snapped = found.Start + found.Heading * travelled;

		return found;
	}

	public bool Locate( ArchGridService grid, Scene scene, Ray ray, out ArchCursor cursor ) {
		cursor = default;

		if ( Axis == ArchViewAxis.Top ) {
			return Ground( grid, scene, ray, out cursor );
		}

		var normal = Axis == ArchViewAxis.Front ? Vector3.Forward : Vector3.Left;

		if ( !Crosses( ray, normal, Depth, out var hit ) ) {
			return false;
		}

		var plan = Axis == ArchViewAxis.Front
			? new Vector2( Depth, grid.Base( hit.y ) )
			: new Vector2( grid.Base( hit.x ), Depth );

		var height = grid.Height( hit.z );

		cursor = new ArchCursor {
			Found = true,
			World = new Vector3( plan.x, plan.y, height ),
			Plan = plan,
			Free = Axis == ArchViewAxis.Front ? new Vector2( Depth, hit.y ) : new Vector2( hit.x, Depth ),
			Height = height,
			Axis = Axis
		};

		return true;
	}

	bool Overhead( ArchGridService grid, Ray ray, out ArchCursor cursor ) {
		cursor = default;

		if ( ray.Forward.z <= 0.001f ) {
			return false;
		}

		// Only count the ceiling plane where the storey actually stands
		var ceiling = Soffit > Level
			&& Crosses( ray, Vector3.Up, Soffit, out var flat )
			&& ArchSoffit.Ceilinged( Plan, Storey, new Vector2( flat.x, flat.y ) )
				? flat
				: default( Vector3? );

		var surface = default( List<Vector2> );

		if ( ArchSoffit.Struck( Plan, Kit, Storey, ray, out var struck, out _, out var outline )
			&& (ceiling is not { } standing || Reach( ray, struck ) < Reach( ray, standing )) ) {
			ceiling = struck;
			surface = outline;
		}

		if ( ceiling is not { } hit ) {
			return false;
		}

		var free = new Vector2( hit.x, hit.y );

		// Soffit outline snaps first, storey edges behind it
		var offered = ArchStoreyEdges.On( Plan, Kit, Storey );

		if ( surface is not null ) {
			offered.Insert( 0, new ArchSnapLoop( surface, true ) );
		}

		var snap = Settled( grid, free, offered, out var point );

		cursor = new ArchCursor {
			Found = true,
			World = new Vector3( point.x, point.y, hit.z ),
			Plan = point,
			Free = free,
			Snap = snap.Kind,
			SnappedTo = snap.Wall,
			SnapHeading = snap.Heading,
			SnapAbove = snap.Above,
			Surface = surface,
			Height = hit.z,
			Axis = ArchViewAxis.Top,
			Face = ArchCursorFace.Ceiling
		};

		return true;
	}

	// Locks to the surface the gesture started on
	bool Pinned( ArchGridService grid, Ray ray, float height, out ArchCursor cursor ) {
		cursor = default;

		if ( !Crosses( ray, Vector3.Up, height, out var hit ) ) {
			return false;
		}

		var free = new Vector2( hit.x, hit.y );
		var snap = Settle( grid, free, StandingDeck, out var point );

		cursor = new ArchCursor {
			Found = true,
			World = new Vector3( point.x, point.y, height ),
			Plan = point,
			Free = free,
			Snap = snap.Kind,
			SnappedTo = snap.Wall,
			SnapHeading = snap.Heading,
			SnapAbove = snap.Above,
			Height = height,
			Axis = ArchViewAxis.Top
		};

		return true;
	}

	bool Deck( ArchGridService grid, Ray ray, out ArchCursor cursor ) {
		cursor = default;

		if ( Plan is null || Kit is null ) {
			return false;
		}

		var roofed = Roof( grid, ray, out var onRoof );
		var paved = Platform( grid, ray, out var onPlatform );

		if ( roofed && (!paved || Reach( ray, onRoof.World ) <= Reach( ray, onPlatform.World )) ) {
			return Take( onRoof, out cursor );
		}

		return paved && Take( onPlatform, out cursor );
	}

	bool Roof( ArchGridService grid, Ray ray, out ArchCursor cursor ) {
		cursor = default;

		if ( !ArchAsks.RoofStruck( Plan, Storey, Kit, ray, out var hit, out var roof ) ) {
			return false;
		}

		var free = new Vector2( hit.x, hit.y );
		var snap = Settle( grid, free, roof, out var point );
		// Height is in building-local space; lift converts to scene space for the cursor
		var lift = Plan.HostOf( roof ) is ArchBuilding on ? ArchAsks.Lift( Plan, on, Kit ) : 0f;

		cursor = new ArchCursor {
			Found = true,
			World = new Vector3( point.x, point.y, hit.z + lift ),
			Plan = point,
			Free = free,
			Snap = snap.Kind,
			SnappedTo = snap.Wall,
			SnapHeading = snap.Heading,
			SnapAbove = snap.Above,
			Height = hit.z,
			Axis = ArchViewAxis.Top,
			OnDeck = roof
		};

		return true;
	}

	bool Platform( ArchGridService grid, Ray ray, out ArchCursor cursor ) {
		cursor = default;

		if ( !ArchPlatformDeck.Struck( Plan, Kit, Storey, ray, out var struck ) ) {
			return false;
		}

		var free = new Vector2( struck.World.x, struck.World.y );
		var surface = struck.Platform.Outline();
		var offered = ArchStoreyEdges.On( Plan, Kit, Storey );

		offered.Insert( 0, new ArchSnapLoop( surface, false ) );

		// Platform outline + storey walls — both snap, unlike soffits where walls are below
		var snap = Settled( grid, free,
			ArchWallSnap.Nearest( ArchWallSnap.On( Plan, Storey, null ), offered, free, WallReach ), out var point );
		var height = ArchPlatformDeck.Seated( Plan, struck.On, Kit, struck.Platform, point );

		cursor = new ArchCursor {
			Found = true,
			World = struck.World.WithX( point.x ).WithY( point.y ),
			Plan = point,
			Free = free,
			Snap = snap.Kind,
			SnappedTo = snap.Wall,
			SnapHeading = snap.Heading,
			SnapAbove = snap.Above,
			Surface = surface,
			Height = height,
			Axis = ArchViewAxis.Top,
			OnPlatform = struck.Platform
		};

		return true;
	}

	bool Ground( ArchGridService grid, Scene scene, Ray ray, out ArchCursor cursor ) {
		if ( Standing is { } pinned ) {
			return Pinned( grid, ray, pinned, out cursor );
		}

		if ( Overhead( grid, ray, out cursor ) ) {
			return true;
		}

		var onDeck = default( ArchCursor );
		var deck = ray.Forward.z < -0.001f && Deck( grid, ray, out onDeck );

		cursor = default;

		var ground = Vector3.Zero;
		var onPlane = Crosses( ray, Vector3.Up, Level, out var plane );
		var onGround = scene.IsValid() && ArchTerrain.Ground( scene, ray, out ground );

		if ( !onPlane && !onGround ) {
			return deck && Take( onDeck, out cursor );
		}

		var terrain = onGround && (!onPlane || Reach( ray, ground ) < Reach( ray, plane ));
		var nearest = terrain ? ground : plane;

		if ( deck && Reach( ray, onDeck.World ) < Reach( ray, nearest ) ) {
			return Take( onDeck, out cursor );
		}

		var free = new Vector2( nearest.x, nearest.y );
		var snap = Settle( grid, free, null, out var point );

		cursor = new ArchCursor {
			Found = true,
			World = new Vector3( point.x, point.y, nearest.z ),
			Plan = point,
			Free = free,
			Snap = snap.Kind,
			SnappedTo = snap.Wall,
			SnapHeading = snap.Heading,
			SnapAbove = snap.Above,
			Height = Level,
			Axis = ArchViewAxis.Top,
			OnGround = terrain
		};

		return true;
	}

	static bool Take( ArchCursor found, out ArchCursor cursor ) {
		cursor = found;

		return true;
	}

	static bool Crosses( Ray ray, Vector3 normal, float offset, out Vector3 hit ) {
		hit = default;

		var facing = Vector3.Dot( ray.Forward, normal );

		if ( MathF.Abs( facing ) < 0.0001f ) {
			return false;
		}

		var distance = (offset - Vector3.Dot( ray.Position, normal )) / facing;

		if ( distance < 0f ) {
			return false;
		}

		hit = ray.Position + ray.Forward * distance;

		return true;
	}

	static float Reach( Ray ray, Vector3 hit ) => Vector3.Dot( hit - ray.Position, ray.Forward );
}