Game/Numbers.cs
namespace DesertPump;

/// <summary>
/// Clicker numbers get big fast, so everything the player sees goes through here.
/// </summary>
public static class Numbers
{
	/// <summary>
	/// Short scale, far enough to cover the last pump in tier 12 - which costs about
	/// 2.5e51. Anything past the end of this falls back to scientific notation.
	/// </summary>
	static readonly string[] Suffixes =
	{
		"", "K", "M", "B", "T",
		"Qa", "Qi", "Sx", "Sp", "Oc", "No",
		"Dc", "UDc", "DDc", "TDc", "QaDc", "QiDc", "SxDc", "SpDc", "OcDc", "NoDc",
		"Vg", "UVg", "DVg", "TVg", "QaVg", "QiVg", "SxVg", "SpVg", "OcVg", "NoVg",
		"Tg"
	};

	/// <summary>
	/// 950 -> "950", 1250 -> "1.25K", 3_400_000 -> "3.40M".
	/// </summary>
	public static string Short( double value )
	{
		if ( double.IsNaN( value ) || double.IsInfinity( value ) ) return "0";

		var negative = value < 0;
		value = Math.Abs( value );

		if ( value < 1000 )
		{
			var whole = Math.Floor( value );
			var text = value < 10 && whole != value
				? value.ToString( "0.#" )
				: whole.ToString( "0" );

			return negative ? $"-{text}" : text;
		}

		var tier = 0;
		while ( value >= 1000 && tier < Suffixes.Length - 1 )
		{
			value /= 1000;
			tier++;
		}

		// Ran off the end of the names - past here nobody minds an exponent.
		if ( value >= 1000 )
		{
			var raw = negative ? -value : value;
			return (raw * Math.Pow( 1000, tier )).ToString( "0.00e+0" );
		}

		var format = value < 10 ? "0.00" : value < 100 ? "0.0" : "0";
		var result = value.ToString( format ) + Suffixes[tier];

		return negative ? $"-{result}" : result;
	}

	/// <summary>
	/// A hash bucket for a number the player is looking at, one bucket per visible
	/// change - three significant figures, which is exactly what <see cref="Short"/> shows.
	/// </summary>
	/// <remarks>
	/// Panels used to hash their numbers as <c>(long)Coins</c>. That works until tier 9,
	/// where coins pass long.MaxValue and every out-of-range cast lands on the same
	/// value - so the hash stops moving, the tree stops rebuilding, and the wallet
	/// freezes on screen while the real number keeps climbing. Water and tree water go
	/// the same way, sooner, because they were cast to int.
	///
	/// Bucketing by mantissa and exponent has no ceiling at all, so it holds for the
	/// whole ladder. The mantissa is rounded rather than truncated so a bucket turns
	/// over on exactly the value <see cref="Short"/> starts printing differently -
	/// truncating left the last digit of the readout a fraction of a step stale.
	/// </remarks>
	public static int Bucket( double value )
	{
		if ( double.IsNaN( value ) ) return 0;
		if ( double.IsInfinity( value ) ) return int.MaxValue;

		var negative = value < 0;
		value = Math.Abs( value );

		if ( value < 1e-6 ) return 0;

		var exponent = (int)Math.Floor( Math.Log10( value ) );

		// 100-1000, three significant figures. 1000 only comes up when 999.5 rounds up,
		// and it can't collide with the next decade because a mantissa is never zero.
		var mantissa = (int)Math.Round( value / Math.Pow( 10, exponent - 2 ) );

		var bucket = exponent * 1000 + mantissa;

		return negative ? -bucket : bucket;
	}

	/// <summary>Litres per second, trimmed so it doesn't jitter in the HUD.</summary>
	public static string Rate( double value )
	{
		if ( value <= 0 ) return "0";
		if ( value < 10 ) return value.ToString( "0.#" );

		return Short( value );
	}

	/// <summary>"1 coin", "2 coins" - only exactly one is singular.</summary>
	public static string Plural( double value, string singular, string plural )
	{
		return value == 1d ? singular : plural;
	}
}