Rides/TrackRides/TrackNode.cs
using System;
using System.Text.Json.Serialization;

namespace HC3.Rides;

#nullable enable

/// <summary>
/// Describes the position and orientation of a <see cref="ITrackSection"/> at the meeting point between two elements.
/// </summary>
/// <param name="Position">Position in track grid units, which are twice the resolution of terrain grid units.</param>
/// <param name="Orientation">Heading, incline and banking of the track.</param>
public readonly record struct TrackNode( Vector3Int Position, TrackOrientation Orientation )
{
	[JsonIgnore]
	public TrackNode Inverse => throw new NotImplementedException();

	public TrackNode? Append( TrackElement element )
	{
		if ( Orientation.Incline != element.StartIncline ) return null;
		if ( Orientation.Banking != element.StartBanking ) return null;
		if ( Orientation.Inverted != element.StartInverted ) return null;

		var endHeading = Orientation.Heading.ToWorld( element.EndHeading );
		var offset = element.EndOffset.Rotate( Orientation.Heading );

		return new TrackNode( Position + offset, new TrackOrientation( endHeading, element.EndIncline, element.EndBanking, element.EndInverted ) );
	}

	public TrackSplineNode ToWorld( TrackSplineNode splineNode )
	{
		return new TrackSplineNode(
			Position + splineNode.Position.Rotate( Orientation.Heading ),
			splineNode.Tangent.Rotate( Orientation.Heading ),
			splineNode.Up.Rotate( Orientation.Heading ),
			splineNode.TangentScale );
	}
}

public readonly record struct TrackSplineNode( Vector3 Position, Vector3 Tangent, Vector3 Up, float TangentScale )
{
	public TrackSplineNode Transform( Transform transform )
	{
		return new TrackSplineNode(
			transform.PointToWorld( Position ),
			transform.NormalToWorld( Tangent * transform.Scale ),
			transform.NormalToWorld( Up * transform.Scale.WithZ( 1f ) ),
			TangentScale );
	}
}

public static class TrackNodeExtensions
{
	public static float Pitch( this TrackIncline incline ) => incline switch
	{
		TrackIncline.VerticalDown => 90,
		TrackIncline.SteepDown => TrackOrientation.SteepInclineDegrees,
		TrackIncline.ShallowDown => TrackOrientation.ShallowInclineDegrees,
		TrackIncline.None => 0f,
		TrackIncline.ShallowUp => -TrackOrientation.ShallowInclineDegrees,
		TrackIncline.SteepUp => -TrackOrientation.SteepInclineDegrees,
		TrackIncline.VerticalUp => -90,
		_ => 0f
	};

	public static float Yaw( this TrackHeading heading ) =>
		(int)heading * TrackOrientation.HeadingIncrementDegrees;

	public static float Roll( this TrackBanking banking ) =>
		(int)banking * TrackOrientation.BankingIncrementDegrees;

	public static Vector2Int Forward( this TrackHeading heading ) => heading switch
	{
		TrackHeading.Forward => new Vector2Int( 1, 0 ),
		TrackHeading.ForwardRight => new Vector2Int( 1, -1 ),
		TrackHeading.Right => new Vector2Int( 0, -1 ),
		TrackHeading.BackwardRight => new Vector2Int( -1, -1 ),
		TrackHeading.Backward => new Vector2Int( -1, 0 ),
		TrackHeading.BackwardLeft => new Vector2Int( -1, 1 ),
		TrackHeading.Left => new Vector2Int( 0, 1 ),
		TrackHeading.ForwardLeft => new Vector2Int( 1, 1 ),
		_ => throw new NotImplementedException()
	};

	public static TrackHeading ToWorld( this TrackHeading heading, TrackHeading local )
	{
		return (TrackHeading)(((int)local + (int)heading) & 7);
	}

	public static TrackHeading ToLocal( this TrackHeading heading, TrackHeading world )
	{
		return (TrackHeading)(((int)world - (int)heading) & 7);
	}

	public static Vector2Int Rotate( this Vector2Int vector, int clockwise90Steps ) => (clockwise90Steps & 3) switch
	{
		0 => vector,
		1 => new Vector2Int( vector.y, -vector.x ),
		2 => new Vector2Int( -vector.x, -vector.y ),
		3 => new Vector2Int( -vector.y, vector.x ),

		_ => throw new NotImplementedException()
	};

	public static Vector2Int Rotate( this Vector2Int vector, TrackHeading heading ) => heading switch
	{
		TrackHeading.Forward => vector,
		TrackHeading.Left => new Vector2Int( -vector.y, vector.x ),
		TrackHeading.Backward => new Vector2Int( -vector.x, -vector.y ),
		TrackHeading.Right => new Vector2Int( vector.y, -vector.x ),

		_ => throw new NotImplementedException()
	};

	public static Vector2 Rotate( this Vector2 vector, TrackHeading heading ) => heading switch
	{
		TrackHeading.Forward => vector,
		TrackHeading.Left => new Vector2( -vector.y, vector.x ),
		TrackHeading.Backward => new Vector2( -vector.x, -vector.y ),
		TrackHeading.Right => new Vector2( vector.y, -vector.x ),

		_ => throw new NotImplementedException()
	};

	public static Vector3Int Rotate( this Vector3Int vector, TrackHeading heading )
	{
		var rotated2d = new Vector2Int( vector.x, vector.y ).Rotate( heading );

		return new Vector3Int( rotated2d.x, rotated2d.y, vector.z );
	}

	public static Vector3 Rotate( this Vector3 vector, TrackHeading heading )
	{
		var rotated2d = new Vector2( vector.x, vector.y ).Rotate( heading );

		return new Vector3( rotated2d.x, rotated2d.y, vector.z );
	}

	public static Rotation Rotate( this Rotation rotation, TrackHeading heading )
	{
		return Rotation.FromYaw( heading.Yaw() ) * rotation;
	}

	public static RectInt Rotate( this RectInt rect, int clockwise90Steps )
	{
		var min = rect.Position.Rotate( clockwise90Steps );
		var max = (rect.Position + rect.Size).Rotate( clockwise90Steps );

		(min, max) = (Vector2Int.Min( min, max ), Vector2Int.Max( min, max ));

		return new RectInt( min, max - min );
	}

	public static bool IsDiagonal( this TrackHeading heading )
	{
		return ((int)heading & 1) == 0;
	}

	public static TrackHeading Mirror( this TrackHeading heading ) =>
		(TrackHeading)(-(int)heading & 7);

	public static TrackHeading Inverse( this TrackHeading heading ) =>
		(TrackHeading)((int)(heading + 4) & 7);

	public static TrackBanking Inverse( this TrackBanking banking ) =>
		(TrackBanking)(-(int)banking);

	public static TrackIncline Inverse( this TrackIncline incline ) =>
		(TrackIncline)(-(int)incline);

	public static TurnDirection Inverse( this TurnDirection direction ) =>
		(TurnDirection)(-(int)direction);

	public static int Ordinal( this TurnDirection direction )
	{
		var abs = Math.Abs( (int)direction );

		return abs * 2 + ((int)direction < 0 ? 1 : 0);
	}

	public static int Ordinal( this TrackIncline incline )
	{
		var abs = Math.Abs( (int)incline );

		return abs * 2 + ((int)incline < 0 ? 1 : 0);
	}

	public static int Ordinal( this TrackBanking banking )
	{
		var abs = Math.Abs( (int)banking );

		return abs * 2 + ((int)banking < 0 ? 1 : 0);
	}

	public static bool AddTrack( this Spline spline, TrackNode start, IReadOnlyList<TrackElement> elements, List<float>? nodeDistances = null )
	{
		if ( elements.Count < 1 ) return false;

		nodeDistances?.Add( 0f );

		var prevNode = start;
		Rotation prevRotation = prevNode.Orientation with { Banking = TrackBanking.None, Inverted = false };
		var prev = new TrackSplineNode( prevNode.Position, prevRotation.Forward, prevRotation.Up, elements[0].StartTangentScale );

		foreach ( var element in elements )
		{
			if ( prevNode.Append( element ) is not { } nextNode ) return false;

			prev = prev with { TangentScale = element.StartTangentScale };

			TrackSplineNode next;

			if ( element.Definition.CustomSpline )
			{
				var transform = Transform.Zero;

				if ( element.FlipX )
				{
					// TODO
				}

				if ( element.FlipY )
				{
					transform = new Transform( 0f, Rotation.Identity, new Vector3( 1f, -1f, 1f ) ).ToWorld( transform );
				}

				if ( element.FlipZ )
				{
					transform = new Transform( 0f, Rotation.Identity, new Vector3( 1f, 1f, -1f ) ).ToWorld( transform );
				}

				var points = element.FlipX
					? element.Definition.MidPoints.AsEnumerable().Reverse()
					: element.Definition.MidPoints;

				foreach ( var point in points )
				{
					next = new TrackSplineNode( point.Position, point.Rotation.Forward, point.Rotation.Up, point.TangentScale )
						.Transform( transform );

					next = prevNode.ToWorld( next );

					spline.AddSegment( prev, next, false );

					prev = next;
				}
			}

			Rotation nextRotation = nextNode.Orientation with { Banking = TrackBanking.None, Inverted = false };
			next = new TrackSplineNode( nextNode.Position, nextRotation.Forward, nextRotation.Up, element.EndTangentScale );

			spline.AddSegment( prev, next, nextNode == start );

			nodeDistances?.Add( spline.Length );

			if ( nextNode == start )
			{
				return true;
			}

			prevNode = nextNode;
			prev = next;
		}

		return true;
	}

	private static void AddSegment( this Spline spline, TrackSplineNode prev, TrackSplineNode next, bool closeLoop )
	{
		var scale = new Vector3( TrackSection.GridSize, TrackSection.GridSize, TrackSection.HeightStep );
		var length = ((next.Position - prev.Position) * scale).Length / 2.5f;

		var tangentIn = -next.Tangent * length * next.TangentScale;
		var tangentOut = Vector3.Zero;

		if ( spline.PointCount > 0 )
		{
			var prevPoint = spline.GetPoint( spline.PointCount - 1 );

			spline.UpdatePoint( spline.PointCount - 1, prevPoint with
			{
				Out = prev.Tangent * length * prev.TangentScale
			} );

			if ( closeLoop )
			{
				var firstPoint = spline.GetPoint( 0 );

				tangentOut = firstPoint.Out;

				spline.UpdatePoint( 0, firstPoint with { In = tangentIn } );
			}
		}
		else
		{
			spline.AddPoint( new Spline.Point
			{
				Position = prev.Position * scale,
				Out = prev.Tangent * length * prev.TangentScale,
				Up = prev.Up,
				Mode = Spline.HandleMode.Split
			} );
		}

		spline.AddPoint( new Spline.Point
		{
			Position = next.Position * scale,
			In = tangentIn,
			Out = tangentOut,
			Up = next.Up,
			Mode = Spline.HandleMode.Split
		} );
	}
}