Terrain/TerrainTile.cs
using System;

namespace HC3.Terrain;

#nullable enable

/// <summary>
/// Used to look up an edge of a tile.
/// </summary>
public enum TileEdge
{
	Up = 0,
	Right = 1,
	Down = 2,
	Left = 3,

	XMin = Left,
	XMax = Right,
	YMin = Down,
	YMax = Up
}

/// <summary>
/// Used to look up a corner of a tile.
/// </summary>
public enum TileCorner
{
	XMinYMin = 0,
	XMaxYMin = 1,
	XMinYMax = 2,
	XMaxYMax = 3
}

/// <summary>
/// One tile of the terrain. Has a height and a slope shape.
/// </summary>
public readonly struct TerrainTile : IEquatable<TerrainTile>
{
	internal const int SizeBytes = sizeof( ushort ) + TileSlope.SizeBytes;

	public ushort BaseHeight { get; init; }
	public TileSlope Slope { get; init; }
	public TilePaint Paint { get; init; }

	public TerrainTile( ushort baseHeight, TileSlope slope, TilePaint paint )
	{
		BaseHeight = baseHeight;
		Slope = slope;
		Paint = paint;
	}

	public int MinHeight => BaseHeight;
	public int MaxHeight => BaseHeight + Slope.MaxHeightOffset;

	public int GetCornerHeight( TileCorner corner ) => BaseHeight + Slope.GetHeightOffset( corner );

	public void GetCornerHeights( Span<int> heights )
	{
		Slope.GetHeightOffsets( heights );

		heights[0] += BaseHeight;
		heights[1] += BaseHeight;
		heights[2] += BaseHeight;
		heights[3] += BaseHeight;
	}

	/// <summary>
	/// Gets the height of a relative position where <c>(0,0)</c> is the height at <see cref="TileCorner.XMinYMin"/>
	/// and <c>(1,1)</c> is the height at <see cref="TileCorner.XMaxYMax"/>. Position is clamped on each axis.
	/// Height is in terrain units, needs to be scaled by <see cref="GridManager.HeightStep"/> to get world units.
	/// </summary>
	public float GetHeight( Vector2 position ) =>
		BaseHeight + Slope.GetHeightOffset( position );

	public bool Equals( TerrainTile other )
	{
		return BaseHeight == other.BaseHeight && Slope == other.Slope && Paint == other.Paint;
	}

	public override bool Equals( object? obj )
	{
		return obj is TerrainTile other && Equals( other );
	}

	public override int GetHashCode()
	{
		return HashCode.Combine( BaseHeight, Slope, Paint );
	}

	public static TerrainTile LevelGround( int height, TilePaint paint = default )
	{
		height = Math.Clamp( height, 0, ushort.MaxValue );

		return new TerrainTile( (ushort)height, TileSlope.LevelGround, paint );
	}
}

public static class TileExtensions
{
	public static (TileCorner Min, TileCorner Max) GetCorners( this TileEdge edge ) => edge switch
	{
		TileEdge.XMin => (TileCorner.XMinYMin, TileCorner.XMinYMax),
		TileEdge.XMax => (TileCorner.XMaxYMin, TileCorner.XMaxYMax),
		TileEdge.YMin => (TileCorner.XMinYMin, TileCorner.XMaxYMin),
		TileEdge.YMax => (TileCorner.XMinYMax, TileCorner.XMaxYMax),
		_ => throw new ArgumentOutOfRangeException( nameof( edge ) )
	};

	public static Vector2Int GetHorizontalOffset( this TileCorner corner ) => corner switch
	{
		TileCorner.XMinYMin => new Vector2Int( 0, 0 ),
		TileCorner.XMaxYMin => new Vector2Int( 1, 0 ),
		TileCorner.XMinYMax => new Vector2Int( 0, 1 ),
		TileCorner.XMaxYMax => new Vector2Int( 1, 1 ),
		_ => throw new ArgumentOutOfRangeException( nameof( corner ) )
	};

	public static Vector2Int GetDirection( this TileEdge edge ) => edge switch
	{
		TileEdge.XMin => new Vector2Int( -1, 0 ),
		TileEdge.XMax => new Vector2Int( 1, 0 ),
		TileEdge.YMin => new Vector2Int( 0, -1 ),
		TileEdge.YMax => new Vector2Int( 0, 1 ),
		_ => throw new ArgumentOutOfRangeException( nameof( edge ) )
	};

	public static TileEdge ToTileEdge( this Rotation rotation )
	{
		var forward = rotation.Forward;

		return Math.Abs( forward.x ) > Math.Abs( forward.y )
			? forward.x > 0 ? TileEdge.XMax : TileEdge.XMin
			: forward.y > 0 ? TileEdge.YMax : TileEdge.YMin;
	}

	public static TileEdge GetOpposite( this TileEdge edge ) => edge switch
	{
		TileEdge.XMin => TileEdge.XMax,
		TileEdge.XMax => TileEdge.XMin,
		TileEdge.YMin => TileEdge.YMax,
		TileEdge.YMax => TileEdge.YMin,
		_ => throw new ArgumentOutOfRangeException( nameof( edge ) )
	};

	public static PathMask ToPathMask( this TileEdge edge ) => edge switch
	{
		TileEdge.Left => PathMask.Left,
		TileEdge.Right => PathMask.Right,
		TileEdge.Down => PathMask.Down,
		TileEdge.Up => PathMask.Up,
		_ => throw new ArgumentOutOfRangeException( nameof( edge ) )
	};

	public static TileEdge Rotate( this TileEdge edge, int clockwise90Steps )
	{
		return (TileEdge)(((int)edge + clockwise90Steps) & 3);
	}

	public static TileEdge RotateDegrees( this TileEdge edge, float yaw )
	{
		return edge.Rotate( (int)MathF.Round( yaw / 90f ) );
	}

	public static TileEdge GetTileEdge( this Vector3 dir )
	{
		if ( MathF.Abs( dir.x ) > MathF.Abs( dir.y ) )
		{
			return dir.x > 0 ? TileEdge.Right : TileEdge.Left;
		}
		else
		{
			return dir.y > 0 ? TileEdge.Up : TileEdge.Down;
		}
	}
}