Editor/Stair/ArchStairWalk.cs
namespace Sunless.Architecture;

public sealed class ArchStairWalked {
	public ArchStairCore Core { get; init; }
	public List<ArchStairLane> Lanes { get; init; } = new();

	public bool Stands => Core is not null && Lanes.Count > 0;
}

// Legs snap to the first leg's 4-direction frame so steps always abut cleanly
public static class ArchStairWalk {
	readonly record struct Box( float AlongFrom, float AlongTo, float AcrossFrom, float AcrossTo );

	const float RightAngle = 90f;

	public static ArchStairWalked Sketch( IReadOnlyList<Vector3> points, float width, float climb ) {
		if ( points is not { Count: >= 2 } || !Aim( points, out var yaw ) ) {
			return new ArchStairWalked();
		}

		var frame = new ArchStairAxes { Origin = Flat( points[0] ), Yaw = yaw };
		var lanes = Steps( Local( frame, points ), Half( width ) );

		if ( lanes.Count == 0 ) {
			return new ArchStairWalked();
		}

		var core = new ArchStairCore { Yaw = yaw, Rise = MathF.Max( 1f, Climbed( points, climb ) ) };

		Seat( core, frame, lanes, Extents( lanes ) );

		return new ArchStairWalked { Core = core, Lanes = lanes };
	}

	public static bool Head( ArchStairPart stair, out Vector3 head ) {
		head = default;

		if ( stair?.Core is not { } core || stair.Lanes.Count == 0 ) {
			return false;
		}

		var lane = stair.Lanes[^1];
		var flat = lane.Axes( core ).Flat( lane.Length, lane.Width * 0.5f );

		head = new Vector3( flat.x, flat.y, stair.BaseHeight + stair.TotalRise );

		return true;
	}

	public static bool Extend( ArchStairPart stair, IReadOnlyList<Vector3> points, float width ) {
		if ( stair?.Core is not { } core || points is not { Count: >= 2 } ) {
			return false;
		}

		var frame = core.Axes;
		var walked = Steps( Local( frame, points ), Half( width ) );

		if ( walked.Count == 0 ) {
			return false;
		}

		var standing = stair.Lanes.ToList();
		var bounds = Extents( standing.Append( Whole( core ) ).Concat( walked ) );

		// Only the join is turned — standing steps are already retreated
		if ( standing.Count > 0 && Turn( standing[^1], walked[0] ) is { } landing ) {
			standing.Add( landing );
		}

		standing.AddRange( walked );

		Seat( core, frame, standing, bounds );

		stair.Lanes.Clear();
		stair.Lanes.AddRange( standing );

		core.Rise = MathF.Max( core.Rise, points[^1].z - stair.BaseHeight );

		return true;
	}

	// ---- The legs ----

	static List<ArchStairLane> Steps( IReadOnlyList<Vector2> locals, float half ) {
		var flights = Banded( locals, half );
		var steps = new List<ArchStairLane>();

		for ( var index = 0; index < flights.Count; index++ ) {
			steps.Add( flights[index] );

			if ( index + 1 < flights.Count && Turn( flights[index], flights[index + 1] ) is { } landing ) {
				steps.Add( landing );
			}
		}

		return steps;
	}

	static List<ArchStairLane> Banded( IReadOnlyList<Vector2> locals, float half ) {
		var lanes = new List<ArchStairLane>();

		for ( var index = 0; index + 1 < locals.Count; index++ ) {
			if ( Leg( locals[index], locals[index + 1], half ) is { } lane ) {
				lanes.Add( lane );
			}
		}

		return lanes;
	}

	// Centred on the head (the aimed click), not the foot
	static ArchStairLane Leg( Vector2 from, Vector2 to, float half ) {
		var span = to - from;

		if ( MathF.Abs( span.x ) < ArchStairLanes.MinLane && MathF.Abs( span.y ) < ArchStairLanes.MinLane ) {
			return null;
		}

		if ( MathF.Abs( span.y ) > MathF.Abs( span.x ) ) {
			return new ArchStairLane {
				AlongFrom = to.x - half,
				AlongTo = to.x + half,
				AcrossFrom = MathF.Min( from.y, to.y ),
				AcrossTo = MathF.Max( from.y, to.y ),
				Walk = span.y >= 0f ? StairWalk.Left : StairWalk.Right
			};
		}

		return new ArchStairLane {
			AlongFrom = MathF.Min( from.x, to.x ),
			AlongTo = MathF.Max( from.x, to.x ),
			AcrossFrom = to.y - half,
			AcrossTo = to.y + half,
			Walk = span.x >= 0f ? StairWalk.Ahead : StairWalk.Back
		};
	}

	static ArchStairLane Turn( ArchStairLane lane, ArchStairLane next ) {
		if ( lane.Walk == next.Walk ) {
			return null;
		}

		var room = MathF.Min( lane.Length, next.Length ) * 0.4f;
		var back = next.Walk == lane.Reversed;

		var head = MathF.Max( 0f, MathF.Min( back ? MathF.Min( lane.Width, next.Width ) : next.Width * 0.5f, room ) );
		var foot = MathF.Max( 0f, MathF.Min( back ? head : lane.Width * 0.5f, room ) );

		if ( head < 0.05f && foot < 0.05f ) {
			return null;
		}

		var before = Rect( lane );
		var after = Rect( next );

		Retreat( lane, head, true );
		Retreat( next, foot, false );

		var given = Union( Given( before, Rect( lane ) ), Given( after, Rect( next ) ) );

		return new ArchStairLane {
			Step = StairStep.Landing,
			AlongFrom = given.AlongFrom,
			AlongTo = given.AlongTo,
			AcrossFrom = given.AcrossFrom,
			AcrossTo = given.AcrossTo,
			Walk = lane.Walk
		};
	}

	public static ArchStairLane Split( ArchStairLane lane, float depth ) {
		var room = MathF.Min( depth, lane.Length - ArchStairLanes.MinLane );

		if ( room < 0.05f ) {
			return null;
		}

		var before = Rect( lane );

		Retreat( lane, room, true );

		var given = Given( before, Rect( lane ) );

		return new ArchStairLane {
			Step = StairStep.Landing,
			AlongFrom = given.AlongFrom,
			AlongTo = given.AlongTo,
			AcrossFrom = given.AcrossFrom,
			AcrossTo = given.AcrossTo,
			Walk = lane.Walk
		};
	}

	static void Retreat( ArchStairLane lane, float depth, bool head ) {
		if ( depth < 0.05f ) {
			return;
		}

		switch ( lane.Walk ) {
			case StairWalk.Ahead when head:
			case StairWalk.Back when !head:
				lane.AlongTo -= depth;
				break;

			case StairWalk.Back when head:
			case StairWalk.Ahead when !head:
				lane.AlongFrom += depth;
				break;

			case StairWalk.Left when head:
			case StairWalk.Right when !head:
				lane.AcrossTo -= depth;
				break;

			default:
				lane.AcrossFrom += depth;
				break;
		}
	}

	static Box Given( Box raw, Box kept ) {
		if ( kept.AlongTo < raw.AlongTo - 0.05f ) {
			return raw with { AlongFrom = kept.AlongTo };
		}

		if ( kept.AlongFrom > raw.AlongFrom + 0.05f ) {
			return raw with { AlongTo = kept.AlongFrom };
		}

		if ( kept.AcrossTo < raw.AcrossTo - 0.05f ) {
			return raw with { AcrossFrom = kept.AcrossTo };
		}

		return raw with { AcrossTo = kept.AcrossFrom };
	}

	static Box Union( Box one, Box two ) => new(
		MathF.Min( one.AlongFrom, two.AlongFrom ),
		MathF.Max( one.AlongTo, two.AlongTo ),
		MathF.Min( one.AcrossFrom, two.AcrossFrom ),
		MathF.Max( one.AcrossTo, two.AcrossTo ) );

	static Box Rect( ArchStairLane lane ) => new( lane.AlongFrom, lane.AlongTo, lane.AcrossFrom, lane.AcrossTo );

	// ---- The shaft the steps bound ----

	static void Seat( ArchStairCore core, ArchStairAxes frame, IReadOnlyList<ArchStairLane> lanes, Box bounds ) {
		foreach ( var lane in lanes ) {
			lane.AlongFrom -= bounds.AlongFrom;
			lane.AlongTo -= bounds.AlongFrom;
			lane.AcrossFrom -= bounds.AcrossFrom;
			lane.AcrossTo -= bounds.AcrossFrom;
		}

		core.Origin = frame.Flat( bounds.AlongFrom, bounds.AcrossFrom );
		core.Length = MathF.Max( ArchStairLanes.MinLane, bounds.AlongTo - bounds.AlongFrom );
		core.Width = MathF.Max( ArchStairLanes.MinLane, bounds.AcrossTo - bounds.AcrossFrom );
	}

	// Measured before retreats so the shaft includes half-landing stations
	static Box Extents( IEnumerable<ArchStairLane> lanes ) {
		var alongFrom = float.MaxValue;
		var alongTo = float.MinValue;
		var acrossFrom = float.MaxValue;
		var acrossTo = float.MinValue;

		foreach ( var lane in lanes ) {
			alongFrom = MathF.Min( alongFrom, lane.AlongFrom );
			alongTo = MathF.Max( alongTo, lane.AlongTo );
			acrossFrom = MathF.Min( acrossFrom, lane.AcrossFrom );
			acrossTo = MathF.Max( acrossTo, lane.AcrossTo );
		}

		return new Box( alongFrom, alongTo, acrossFrom, acrossTo );
	}

	static ArchStairLane Whole( ArchStairCore core ) => new() {
		AlongFrom = 0f,
		AlongTo = core.Length,
		AcrossFrom = 0f,
		AcrossTo = core.Width
	};

	// ---- Reading the clicks ----

	// Snapped to nearest 90 — a stair with a deliberate angle is aimed after placement
	static bool Aim( IReadOnlyList<Vector3> points, out float yaw ) {
		yaw = 0f;

		for ( var index = 0; index + 1 < points.Count; index++ ) {
			var span = Flat( points[index + 1] ) - Flat( points[index] );

			if ( span.Length < ArchStairLanes.MinLane ) {
				continue;
			}

			yaw = ArchGridService.Snap( MathF.Atan2( span.y, span.x ).RadianToDegree(), RightAngle );

			return true;
		}

		return false;
	}

	// Falls back to sidebar value when the walk stayed on one level
	static float Climbed( IReadOnlyList<Vector3> points, float climb ) {
		var walked = points[^1].z - points[0].z;

		return walked > ArchGridService.FinestSize ? walked : climb;
	}

	static List<Vector2> Local( ArchStairAxes frame, IReadOnlyList<Vector3> points ) {
		var along = frame.Along;
		var across = frame.Across;

		return points.Select( point => {
			var offset = Flat( point ) - frame.Origin;

			return new Vector2( Vector2.Dot( offset, along ), Vector2.Dot( offset, across ) );
		} ).ToList();
	}

	static Vector2 Flat( Vector3 point ) => new( point.x, point.y );

	static float Half( float width ) => MathF.Max( ArchStairLanes.MinLane, width ) * 0.5f;
}