Extensions.cs
using System;
using System.Numerics;
using System.Text.Json.Nodes;

namespace PlanetMeat;

public static partial class Extensions
{
	public static T FindNearestUpwards<T>( this Component comp )
	{
		return comp.Components.Get<T>( FindMode.Enabled | FindMode.InSelf | FindMode.InAncestors );
	}
	public static T FindNearestUpwards<T>( this GameObject go )
	{
		return go.Components.Get<T>( FindMode.Enabled | FindMode.InSelf | FindMode.InAncestors );
	}

	public static T AddCapped<T>( this T a, T b, T cap ) where T : IComparable<T>, IAdditionOperators<T, T, T>, ISubtractionOperators<T, T, T>
	{
		if ( b.CompareTo( cap - a ) >= 0 )
			return cap;
		return a + b;
	}
	public static T AddCapped<T>( this T a, T b ) where T : IMinMaxValue<T>, IComparable<T>, IAdditionOperators<T, T, T>, ISubtractionOperators<T, T, T>
	{
		return AddCapped( a, b, T.MaxValue );
	}

	private static T MultiplyCappedPositive<T>( T a, T b, T cap ) where T : IComparable<T>, IMultiplyOperators<T, T, T>, IDivisionOperators<T, T, T>
	{
		if ( b.CompareTo( cap / a ) >= 0 )
			return cap;
		return a * b;
	}

	public static T MultiplyCapped<T>( this T a, T b, T cap ) where T : IComparable<T>, IMultiplyOperators<T, T, T>, IDivisionOperators<T, T, T>, IUnaryNegationOperators<T, T>
	{
		var aToDefault = a.CompareTo( default );
		var bToDefault = b.CompareTo( default );
		if ( aToDefault == 0 || bToDefault == 0 )
			return default;

		if ( aToDefault == bToDefault )
			return MultiplyCappedPositive( a, b, cap );
		return -MultiplyCappedPositive( -a, b, cap );
	}

	public static T MultiplyCapped<T>( this T a, T b ) where T : IMinMaxValue<T>, IComparable<T>, IMultiplyOperators<T, T, T>, IDivisionOperators<T, T, T>, IUnaryNegationOperators<T, T>
	{
		return MultiplyCapped( a, b, T.MaxValue );
	}

	public static double MultiplyCapped( this double a, double b )
	{
		if ( a.IsNearlyZero() || b.IsNearlyZero() )
			return 0.0;
		return MultiplyCapped( a, b, double.MaxValue );
	}

	public static float MultiplyCapped( this float a, float b )
	{
		if ( a.AlmostEqual( 0.0f, 0.001f ) || b.AlmostEqual( 0.0f, 0.001f ) )
			return 0.0f;
		return MultiplyCapped( a, b, float.MaxValue );
	}

	public static float ToPercentage( this float f ) => 100.0f * f;
	public static double ToPercentage( this double d ) => 100.0 * d;
	public static int ToWholePercentage( this float f ) => (int)ToPercentage( f );
	public static int ToWholePercentage( this double d ) => (int)ToPercentage( d );

	public static int CountDecimalPlaces( this float f )
	{
		var d = Math.Abs( (decimal)f );
		d -= (int)d;
		var places = 0;
		while ( d > 0 )
		{
			places++;
			d *= 10;
			d -= (int)d;
		}
		return places;
	}

	public static string ToNString( this float f, int decimals = 2 ) => f.ToString( "N" + decimals );
	public static string ToAutoDecimalString( this float f, int maxPlaces = 2 ) => f.ToNString( Math.Min( maxPlaces, f.CountDecimalPlaces() ) );

	public static bool IsNearlyZero( this double d, double e = 0.001 ) => d < e && d > -e;

	public static Vector3? IntersectRay( this Sandbox.Plane p, Ray r, float epsilon = 0.001f )
	{
		var denom = p.Normal.Dot( r.Forward );
		if ( !denom.AlmostEqual( 0.0f, epsilon ) )
		{
			var dist = (p.Position - r.Position).Dot( p.Normal ) / denom;
			if ( dist >= 0 )
				return r.Project( dist );
		}
		return null;
	}

	public static T Lowest<T, C>( this IEnumerable<T> items, Func<T, C> calculate ) where C : IComparable<C>
	{
		T bestCandidate = items.First();
		C bestVal = calculate.Invoke( bestCandidate );
		foreach ( var i in items )
		{
			var iVal = calculate.Invoke( i );
			if ( iVal.CompareTo( bestVal ) < 0 )
			{
				bestCandidate = i;
				bestVal = iVal;
			}
		}
		return bestCandidate;
	}

	public static int HashContents<T>( this IEnumerable<T> items )
	{
		var hash = new HashCode();
		foreach ( var item in items )
			hash.Add( item );
		return hash.ToHashCode();
	}

	public static bool IsSubsetOf<T>( this IEnumerable<T> a, IEnumerable<T> b ) => !a.Except( b ).Any();
	public static bool SameContentsAs<T>( this IEnumerable<T> a, IEnumerable<T> b ) => a.IsSubsetOf( b ) && b.IsSubsetOf( a );

	public static bool IsSingle<T>( this IEnumerable<T> col ) => col.Take( 2 ).Count() == 1;

	public static void Shuffle<T>( this IList<T> list, Random rng )
	{
		for ( var i = list.Count - 1; i > 0; i-- )
		{
			var j = rng.Next( i + 1 );
			(list[i], list[j]) = (list[j], list[i]);
		}
	}

	public static bool TryGetTypedProperty<T>( this JsonObject json, string key, out T value )
	{
		if ( json.TryGetPropertyValue( key, out var p ) )
		{
			value = p.GetValue<T>();
			return true;
		}
		value = default;
		return false;
	}

	public static bool TryGetPropertyNode<T>( this JsonObject json, string key, out T value ) where T : JsonNode
	{
		if ( json.TryGetPropertyValue( key, out var node ) && node is T tn )
		{
			value = tn;
			return true;
		}
		value = default;
		return false;
	}
}