Editor/Stair/ArchStairCore.cs
using System.Text.Json.Serialization;

namespace Sunless.Architecture;

public enum StairWalk {
	Ahead,
	Back,
	Left,
	Right
}

public static class StairWalks {
	public static Vector2 Heading( this StairWalk walk ) => walk switch {
		StairWalk.Back => new Vector2( -1f, 0f ),
		StairWalk.Left => new Vector2( 0f, 1f ),
		StairWalk.Right => new Vector2( 0f, -1f ),
		_ => new Vector2( 1f, 0f )
	};

	public static Vector2 Leftward( this StairWalk walk ) {
		var heading = walk.Heading();

		return new Vector2( -heading.y, heading.x );
	}

	public static StairWalk Reversed( this StairWalk walk ) => walk switch {
		StairWalk.Back => StairWalk.Ahead,
		StairWalk.Left => StairWalk.Right,
		StairWalk.Right => StairWalk.Left,
		_ => StairWalk.Back
	};

	public static StairWalk Turned( this StairWalk walk, StairTurn turn ) => turn switch {
		StairTurn.Left => walk switch {
			StairWalk.Ahead => StairWalk.Left,
			StairWalk.Left => StairWalk.Back,
			StairWalk.Back => StairWalk.Right,
			_ => StairWalk.Ahead
		},
		StairTurn.Right => walk switch {
			StairWalk.Ahead => StairWalk.Right,
			StairWalk.Right => StairWalk.Back,
			StairWalk.Back => StairWalk.Left,
			_ => StairWalk.Ahead
		},
		StairTurn.Back => walk.Reversed(),
		_ => walk
	};

	public static StairTurn Between( this StairWalk from, StairWalk to ) {
		if ( from == to ) {
			return StairTurn.None;
		}

		if ( from.Turned( StairTurn.Left ) == to ) {
			return StairTurn.Left;
		}

		if ( from.Turned( StairTurn.Right ) == to ) {
			return StairTurn.Right;
		}

		return StairTurn.Back;
	}
}

public enum StairFill {
	Open,
	Solid
}

public enum StairStep {
	Flight,
	Landing
}

public sealed class ArchStairCore {
	public Vector2 Origin { get; set; }
	public float Yaw { get; set; }
	public float Length { get; set; } = 240f;
	public float Width { get; set; } = 120f;
	public float Rise { get; set; } = 128f;
	public StairFill Fill { get; set; }

	public ArchStairAxes Axes => new() { Origin = Origin, Yaw = Yaw };

	public Vector2 Flat( float along, float across ) => Axes.Flat( along, across );

	public List<Vector2> Outline() => Axes.Rect( 0f, Length, 0f, Width );

	public ArchStairCore Copy() => new() {
		Origin = Origin,
		Yaw = Yaw,
		Length = Length,
		Width = Width,
		Rise = Rise,
		Fill = Fill
	};

	public static (ArchStairCore Core, List<ArchStairLane> Lanes) Straight( Vector2 origin, Vector2 along, float length, float width, float rise ) {
		var direction = along.IsNearZeroLength ? new Vector2( 1f, 0f ) : along.Normal;

		var core = new ArchStairCore {
			Origin = origin,
			Yaw = MathF.Atan2( direction.y, direction.x ).RadianToDegree(),
			Length = MathF.Max( 1f, length ),
			Width = MathF.Max( 1f, width ),
			Rise = MathF.Max( 1f, rise )
		};

		var lanes = new List<ArchStairLane>
		{
			new() { AlongFrom = 0f, AlongTo = core.Length, AcrossFrom = 0f, AcrossTo = core.Width, Walk = StairWalk.Ahead }
		};

		return (core, lanes);
	}
}

public sealed class ArchStairLane {
	public int Id { get; set; }

	public StairStep Step { get; set; }

	public float AlongFrom { get; set; }
	public float AlongTo { get; set; }
	public float AcrossFrom { get; set; }
	public float AcrossTo { get; set; }
	public StairWalk Walk { get; set; }

	// Curves are chains of segments, each carrying a share of the sweep
	[JsonIgnore( Condition = JsonIgnoreCondition.WhenWritingDefault )]
	public float Bearing { get; set; }

	// Pinned flights take an exact rise; zero divides the remainder
	public float Rise { get; set; }

	// Empty = stair-level Guard setting decides
	public List<ArchStairGuardPart> Guards { get; set; } = new();

	public bool Climbs => Step == StairStep.Flight;

	public bool Guarding( StairEdge edge ) => Guards.Any( guard => guard.Edge == edge );

	// Migrated by Normalize into a real landing lane.
	[JsonIgnore( Condition = JsonIgnoreCondition.WhenWritingDefault )]
	public float LandingDepth { get; set; }

	public float AlongSpan => MathF.Max( 0f, AlongTo - AlongFrom );

	public float AcrossSpan => MathF.Max( 0f, AcrossTo - AcrossFrom );

	public bool Sideways => Walk is StairWalk.Left or StairWalk.Right;

	public float Length => Sideways ? AcrossSpan : AlongSpan;

	public float Width => Sideways ? AlongSpan : AcrossSpan;

	// The pivot a bearing turns about — stable across turns
	public Vector2 Seat => Walk switch {
		StairWalk.Back => new Vector2( AlongTo, AcrossTo ),
		StairWalk.Left => new Vector2( AlongTo, AcrossFrom ),
		StairWalk.Right => new Vector2( AlongFrom, AcrossTo ),
		_ => new Vector2( AlongFrom, AcrossFrom )
	};

	public Vector2 Foot( ArchStairCore core ) => core.Flat( Seat.x, Seat.y );

	public float Cardinal => Walk switch {
		StairWalk.Back => 180f,
		StairWalk.Left => 90f,
		StairWalk.Right => -90f,
		_ => 0f
	};

	public float Yaw( ArchStairCore core ) => core.Yaw + Cardinal + Bearing;

	public ArchStairAxes Axes( ArchStairCore core ) => new() { Origin = Foot( core ), Yaw = Yaw( core ) };

	public ArchStairAxes Frame => new() { Origin = Seat, Yaw = Cardinal + Bearing };

	public List<Vector2> Corners() => Frame.Rect( 0f, Length, 0f, Width );

	public (float AlongFrom, float AlongTo, float AcrossFrom, float AcrossTo) HeadEdge() => Walk switch {
		StairWalk.Back => (AlongFrom, AlongFrom, AcrossFrom, AcrossTo),
		StairWalk.Left => (AlongFrom, AlongTo, AcrossTo, AcrossTo),
		StairWalk.Right => (AlongFrom, AlongTo, AcrossFrom, AcrossFrom),
		_ => (AlongTo, AlongTo, AcrossFrom, AcrossTo)
	};

	public (float AlongFrom, float AlongTo, float AcrossFrom, float AcrossTo) FootEdge() => Walk switch {
		StairWalk.Back => (AlongTo, AlongTo, AcrossFrom, AcrossTo),
		StairWalk.Left => (AlongFrom, AlongTo, AcrossFrom, AcrossFrom),
		StairWalk.Right => (AlongFrom, AlongTo, AcrossTo, AcrossTo),
		_ => (AlongFrom, AlongFrom, AcrossFrom, AcrossTo)
	};

	public Vector2 Heading => Frame.Along;

	public Vector2 Leftward => Frame.Across;

	public List<Vector2> Loop( ArchStairCore core ) => Axes( core ).Rect( 0f, Length, 0f, Width );

	public StairWalk Reversed => Walk.Reversed();

	public ArchStairLane Copy() => new() {
		Id = Id,
		Step = Step,
		AlongFrom = AlongFrom,
		AlongTo = AlongTo,
		AcrossFrom = AcrossFrom,
		AcrossTo = AcrossTo,
		Walk = Walk,
		Bearing = Bearing,
		Rise = Rise,
		Guards = Guards.Select( guard => guard.Copy() ).ToList()
	};
}