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

namespace HC3.Rides;

#nullable enable

/// <summary>
/// Size category to show in the track builder UI when selecting elements.
/// </summary>
public enum TrackElementSize : byte
{
	None = 0,
	Tiny = 1,
	Small = 2,
	Medium = 3,
	Large = 4,
	Giant = 5
}

/// <summary>
/// How a particular <see cref="TrackElement"/> is transformed compared to its original <see cref="TrackElementDefinition"/>.
/// </summary>
[Flags]
public enum TrackElementFlags : byte
{
	FlipX = 1,
	FlipY = 2,
	FlipZ = 4
}

/// <summary>
/// Describes a special function for a <see cref="TrackElement"/>.
/// </summary>
public enum TrackFeature
{
	/// <summary>
	/// Don't show this element in the track builder.
	/// </summary>
	Disabled = -1,

	/// <summary>
	/// Normal piece of track.
	/// </summary>
	None = 0,

	/// <summary>
	/// Part of a station for trains to load / unload guests.
	/// </summary>
	Station,

	/// <summary>
	/// Pulls trains up inclines.
	/// </summary>
	ChainLift,

	/// <summary>
	/// TODO
	/// </summary>
	Break,

	/// <summary>
	/// TODO
	/// </summary>
	BlockBreak,

	/// <summary>
	/// TODO
	/// </summary>
	Booster,

	/// <summary>
	/// For elements like inversions and s-bends.
	/// </summary>
	Special
}

/// <summary>
/// Used for filtering the direction of a track element.
/// </summary>
public enum TurnDirection
{
	/// <summary>
	/// Track performs a left U-turn.
	/// </summary>
	[Icon( "u_turn_left" )]
	UTurnLeft = -2,

	/// <summary>
	/// Track performs a left turn.
	/// </summary>
	[Icon( "turn_left" )]
	Left = -1,

	/// <summary>
	/// Track doesn't turn left or right.
	/// </summary>
	[Icon( "straight" )]
	Straight = 0,

	/// <summary>
	/// Track performs a right turn.
	/// </summary>
	[Icon( "turn_right" )]
	Right = 1,

	/// <summary>
	/// Track performs a right U-turn.
	/// </summary>
	[Icon( "u_turn_right" )]
	UTurnRight = 2,
}

/// <summary>
/// The smallest piece of track that you can build with.
/// A <see cref="ITrackSection"/> is a list of these connected end to end.
/// </summary>
/// <param name="Definition">Describes the general shape of this element.</param>
/// <param name="Flags">Describes how this element is modified compared to <see cref="Definition"/>.</param>
/// <param name="StartBanking">Banking at the very start of this element.</param>
/// <param name="EndBanking">Banking at the very end of this element.</param>
public readonly record struct TrackElement(
	TrackElementDefinition Definition,
	TrackElementFlags Flags,
	TrackBanking StartBanking,
	TrackBanking EndBanking ) : IComparable<TrackElement>
{
	public readonly record struct RpcSafe(
		TrackElementDefinition Definition,
		TrackElementFlags Flags,
		TrackBanking StartBanking,
		TrackBanking EndBanking )
	{
		public static implicit operator TrackElement( RpcSafe value )
		{
			return new TrackElement( value.Definition, value.Flags, value.StartBanking, value.EndBanking );
		}

		public static implicit operator RpcSafe( TrackElement value )
		{
			return new RpcSafe( value.Definition, value.Flags, value.StartBanking, value.EndBanking );
		}
	}

	/// <summary>
	/// Was this element flipped on the X axis relative to its <see cref="Definition"/>?
	/// </summary>
	[JsonIgnore]
	public bool FlipX => (Flags & TrackElementFlags.FlipX) != 0;

	/// <summary>
	/// Was this element flipped on the Y axis relative to its <see cref="Definition"/>?
	/// </summary>
	[JsonIgnore]
	public bool FlipY => (Flags & TrackElementFlags.FlipY) != 0;

	/// <summary>
	/// Was this element flipped on the Z axis relative to its <see cref="Definition"/>?
	/// </summary>
	[JsonIgnore]
	public bool FlipZ => (Flags & TrackElementFlags.FlipZ) != 0;

	/// <summary>
	/// Gets the starting incline of this element. Elements can only connect if the
	/// previous element's <see cref="EndIncline"/> matches the next element's <see cref="StartIncline"/>.
	/// </summary>
	[JsonIgnore]
	public TrackIncline StartIncline
	{
		get
		{
			var incline = FlipX ? Definition.EndIncline.Inverse() : Definition.StartIncline;
			return FlipZ ? incline.Inverse() : incline;
		}
	}

	/// <summary>
	/// Gets the final incline of this element. Elements can only connect if the
	/// previous element's <see cref="EndIncline"/> matches the next element's <see cref="StartIncline"/>.
	/// </summary>
	[JsonIgnore]
	public TrackIncline EndIncline
	{
		get
		{
			var incline = FlipX ? Definition.StartIncline.Inverse() : Definition.EndIncline;
			return FlipZ ? incline.Inverse() : incline;
		}
	}

	[JsonIgnore]
	public bool StartInverted => FlipX && Definition.EndInverted;

	[JsonIgnore]
	public bool EndInverted => !FlipX && Definition.EndInverted;

	/// <summary>
	/// For filtering track elements by incline in the track builder.
	/// </summary>
	public TrackIncline GetInclineCategory( TrackBuildKind buildKind ) =>
		buildKind == TrackBuildKind.Append
			? Definition.CustomSpline ? FlipX != FlipZ ? Definition.CustomIncline.Inverse() : Definition.CustomIncline : EndIncline
			: Definition.CustomSpline ? FlipX != FlipZ ? Definition.CustomIncline : Definition.CustomIncline.Inverse() : StartIncline.Inverse();

	/// <summary>
	/// Gets the final heading of this element, relative to <see cref="TrackHeading.Forward"/>.
	/// </summary>
	[JsonIgnore]
	public TrackHeading EndHeading => FlipY
		? Definition.EndHeading.Mirror()
		: Definition.EndHeading;

	/// <summary>
	/// Gets how much this element moves the track spline, in track units.
	/// Track grid units are at twice the resolution of terrain grid units.
	/// </summary>
	[JsonIgnore]
	public Vector3Int EndOffset
	{
		get
		{
			var offset = Definition.EndOffset;

			if ( FlipX )
			{
				offset.z = -offset.z;
			}

			if ( FlipY )
			{
				offset.y = -offset.y;
			}

			if ( FlipZ )
			{
				offset.z = -offset.z;
			}

			return offset;
		}
	}

	/// <summary>
	/// Descriptive size to help categorize track elements in the <see cref="TrackBuilder"/>.
	/// </summary>
	[JsonIgnore]
	public TrackElementSize Size => Definition?.Size ?? TrackElementSize.None;

	/// <summary>
	/// Descriptive turn direction to help filter track elements in the <see cref="TrackBuilder"/>.
	/// </summary>
	[JsonIgnore]
	public TurnDirection Direction => GetTurnDirection( EndHeading, EndOffset, StartInverted, EndInverted, FlipY );

	/// <summary>
	/// Does this element have a special function, like a lift hill or station platform?
	/// </summary>
	[JsonIgnore]
	public TrackFeature Feature => Definition?.Feature ?? TrackFeature.Disabled;

	[JsonIgnore]
	public float StartTangentScale => Definition?.CustomSpline is true
		? FlipX ? Definition.EndTangentScale : Definition.StartTangentScale
		: 1f;

	[JsonIgnore]
	public float EndTangentScale => Definition?.CustomSpline is true
		? FlipX ? Definition.StartTangentScale : Definition.EndTangentScale
		: 1f;

	/// <summary>
	/// Player-facing name of this element.
	/// </summary>
	[JsonIgnore]
	public string Title =>
		$"{(Definition.Size != TrackElementSize.None ? $"{Definition.Size} " : "")}" +
		$"{(Direction != TurnDirection.Straight ? $"{Direction} " : "")}" +
		$"{Definition.Title}";

	[JsonIgnore]
	public float Length => Definition.Length;

	[JsonIgnore]
	public float Length2D => Definition.Length2D;

	[JsonIgnore]
	public IEnumerable<float> TileBoundaries
	{
		get
		{
			var length = Length;
			return FlipX ? Definition.TileBoundaries.Reverse().Select( x => length - x ) : Definition.TileBoundaries;
		}
	}

	public override string ToString()
	{
		return Flags == 0 ? "Default" : $"Flip{(FlipX ? "X" : "")}{(FlipY ? "Y" : "")}{(FlipZ ? "Z" : "")}";
	}

	public int CompareTo( TrackElement other )
	{
		return Definition.Size.CompareTo( other.Definition.Size );
	}

	internal static TurnDirection GetTurnDirection( TrackHeading endHeading, Vector3Int endOffset, bool startInverted, bool endInverted, bool flipY ) =>
		(endHeading, endOffset.y) switch
		{
			(TrackHeading.ForwardLeft or TrackHeading.Left or TrackHeading.BackwardLeft, _ ) => TurnDirection.Left,
			(TrackHeading.ForwardRight or TrackHeading.Right or TrackHeading.BackwardRight, _ ) => TurnDirection.Right,

			(TrackHeading.Backward, > 0 ) => TurnDirection.UTurnLeft,
			(TrackHeading.Backward, < 0 ) => TurnDirection.UTurnRight,

			(_, > 0 ) => TurnDirection.Left,
			(_, < 0 ) => TurnDirection.Right,

			_ => startInverted && !flipY || endInverted && flipY ? TurnDirection.Left
				: endInverted && !flipY || startInverted && flipY ? TurnDirection.Right
				: TurnDirection.Straight
		};
}