Editor/Tool/ArchToolShapes.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

namespace Sunless.Architecture;

// What the plan resolves to, held between frames: resolved once per EDIT, because every answer walks the whole curve.
public partial class ArchTool {
	static readonly List<Vector2> NoOutline = new();

	readonly Dictionary<(Type Type, int Id), object> resolved = new();

	ArchElevationCut section;

	Vector2 roadUnderAt;
	ArchRoadPart roadUnder;
	bool roadUnderTaken;

	// The ceiling plane the cursor may land on. Held between frames like every other resolve: the cursor is
	// asked several times a frame and this walks the room's cuts to find any recess in it.
	float overhead;
	bool overheadTaken;

	// Every edit funnels here, so nothing resolved before one survives it; a handle drag edits per frame without committing.
	public void Revise() {
		resolved.Clear();
		section = null;
		roadUnderTaken = false;
		overheadTaken = false;
		cursorTaken = false;
		projectedLayers = null;

		// Every face of the build has just been remade, so the probed ones are somebody else's geometry.
		ForgetBuiltFaces();
	}

	public T Resolved<T>( int id, Func<T> resolve ) {
		if ( id == 0 ) {
			return resolve();
		}

		var key = (typeof( T ), id);

		if ( resolved.TryGetValue( key, out var held ) ) {
			return (T)held;
		}

		var found = resolve();
		resolved[key] = found;

		return found;
	}

	// The cached loop itself, not a copy - reshape a room via ArchFloorGen.
	public List<Vector2> Outline( ArchRoom room ) {
		if ( room is null ) {
			return NoOutline;
		}

		return Resolved( room.Id, () => ArchFloorGen.Footprint( room ) );
	}

	// Nearest scans a whole dense walk, and the overlay asks every frame.
	public ArchRoadPart RoadUnder( Vector2 point ) {
		if ( roadUnderTaken && roadUnderAt == point ) {
			return roadUnder;
		}

		roadUnderTaken = true;
		roadUnderAt = point;

		return roadUnder = Plan.Roads().Count == 0 ? null : RoadAt( point, out _ );
	}

	// Keyed on depth as well as building: scrubbing the section bypasses Revise.
	public ArchElevationCut Section( ArchBuilding building ) {
		var axis = Axis;
		var depth = Depth( axis );

		if ( section is not null && section.Matches( building, axis, depth ) ) {
			return section;
		}

		return section = ArchElevation.Cut( Plan, Kit, building, axis, depth );
	}

	// Framing and section defaults both ask per frame.
	public BBox Extent( ArchBuilding building ) {
		var id = building?.Id ?? 0;

		return Resolved( id, () => ArchLocate.Find( this, id != 0 ? id.ToString() : "plan" ).Frame );
	}

	public ArchCurve Curve( ArchFencePart fence ) => Resolved( fence.Id, fence.Curve );

	public ArchBarrierShape Barrier( ArchFencePart fence )
		=> Resolved( fence.Id, () => ArchBarrierShape.Resolve( Curve( fence ), fence.Barrier, 0f, 0f, Kit ) );

	// A whole terrain walk per bay, so the overlay asking every frame has to be held like every other resolve.
	public ArchRetainingShape Retaining( ArchFencePart fence )
		=> Resolved( fence.Id, () => ArchRetainingShape.Resolve( Barrier( fence ), Kit, new ArchGround( Scene ) ) );

	// Four subtools and the overlay used to walk every centreline per frame for this.
	public ArchRoadPart RoadAt( Vector2 point, out ArchFrame frame ) => RoadAt( point, road => road.Reach(), out frame );

	public ArchRoadPart RoadAt( Vector2 point, Func<ArchRoadPart, float> reach, out ArchFrame frame ) {
		ArchRoadPart best = null;
		var nearest = float.MaxValue;

		frame = default;

		foreach ( var road in Plan.Roads() ) {
			if ( !Resolved( road.Id, road.Curve ).Nearest( point, out var station, out var gap ) ) {
				continue;
			}

			if ( gap > reach( road ) || gap >= nearest ) {
				continue;
			}

			nearest = gap;
			best = road;
			frame = station;
		}

		return best;
	}
}