Spending/Upgrades/BaseUpgrades.cs
using System;
using System.Numerics;

namespace PlanetMeat;

[Group( "Planet Meat - Upgrades" ), Title( "Base Upgrade" ), Icon( "settings_suggest" )]
public abstract class BaseUpgrade : Component, IPurchaseableUpgrade
{
	public UpgradeCategory Category => this.FindNearestUpwards<UpgradeCategory>();
	public string CategoryIdent => Category?.Ident ?? "";

	[Property] public double CostOffset { get; set; } = 1.0;
	[Property] public double CostStep { get; set; } = 1.0;
	public virtual double NextCost => CostStep * Rank + CostOffset;

	public int Rank
	{
		get;
		private set
		{
			field = value;
			OnRankChanged();
		}
	} = 0;

	[Property] public string Ident { get; set; } = "";
	[Property] protected virtual string RequiredTech { get; set; } = "";

	private bool _needsTechCheck = true;
	private bool HasTech
	{
		get
		{
			if ( _needsTechCheck )
			{
				_needsTechCheck = false;
				field = Research.Current.HasTech( RequiredTech );
			}
			return field;
		}
	}

	public virtual bool Visible => HasTech;
	public virtual bool Purchaseable => true;

	protected override void OnStart()
	{
		base.OnStart();
		OnRankChanged();
	}

	protected override void OnDestroy()
	{
		Rank = 0;
		base.OnDestroy();
	}

	public bool TryPurchase()
	{
		if ( Purchaseable && Bank.Current.GetAccount( "goop" ).TrySpend( NextCost ) )
		{
			Rank++;
			return true;
		}
		return false;
	}

	public abstract string GetDisplayedEffectAt( int rank );
	public string DisplayedCurrentEffect => GetDisplayedEffectAt( Rank );
	public string DisplayedNextEffect => GetDisplayedEffectAt( Rank + 1 );
	public string AdvancementText => DisplayedCurrentEffect + " -> " + DisplayedNextEffect;
	public string DisplayName => "upgrade." + Ident + ".title";

	protected abstract void OnRankChanged();
}

public abstract class BaseEffectUpgrade<T> : BaseUpgrade where T : IAdditionOperators<T, T, T>, IComparable<T>, IConvertible, IMinMaxValue<T>, ISubtractionOperators<T, T, T>
{
	[Property] public string ImprovementIdent { get; set; } = "";
	protected BaseEffectImprovement<T> ImprovementBasis => Research.Current.GetImprovement( ImprovementIdent ) is BaseEffectImprovement<T> imp ? imp : null;
	protected override string RequiredTech
	{
		get
		{
			var imp = ImprovementBasis;
			return imp is null ? base.RequiredTech : imp.RequiredTech;
		}
		set => base.RequiredTech = value;
	}

	protected abstract T EffectBase { get; }
	protected abstract T GetEffectAt( int rank );
	public T CurrentEffect => GetEffectAt( Rank );

	public bool IsIncrease => GetEffectAt( Rank ).CompareTo( GetEffectAt( Rank + 1 ) ) < 0;

	public virtual T ImprovementEffect => Research.Current.GetImprovementEffect<T>( ImprovementIdent );
	public virtual T EffectMax
	{
		get
		{
			var imp = ImprovementBasis;
			return imp is null ? default : imp.EffectMax;
		}
	}
	protected virtual T EffectPerRank
	{
		get
		{
			var imp = ImprovementBasis;
			return imp is null ? default : imp.EffectPerRank;
		}
	}
	public virtual bool HasCap => EffectMax.CompareTo( default ) > 0;
	protected virtual bool IsUnderCap => OverallEffect.CompareTo( EffectMax ) < 0;
	public override bool Purchaseable => base.Purchaseable && IsIncrease && (!HasCap || IsUnderCap);

	protected T OverallEffect => GetOveralEffect( Rank );
	protected T GetOveralEffect( int rank ) => EffectBase.AddCapped( GetEffectAt( rank ) );

	protected int DecimalPlaces => ImprovementBasis is FloatEffectImprovement p ? p.DecimalPlaces : FloatEffectImprovement.DEFAULT_DECIMAL_PLACES;
	public override string GetDisplayedEffectAt( int rank )
	{
		var effect = GetOveralEffect( rank );
		if ( effect is double d )
			return LargeNumberString.ToCompact( d );
		if ( effect is float f )
			return f.ToNString( DecimalPlaces );
		return effect.ToString();
	}
}

public abstract class BaseComponentUpgrade<C, T> : BaseEffectUpgrade<T> where T : ISubtractionOperators<T, T, T>, IAdditionOperators<T, T, T>, IComparable<T>, IConvertible, IMinMaxValue<T>
{
	protected C TargetComponent => field ??= this.FindNearestUpwards<C>();
}

public abstract class BaseAggregateUpgrade<C, T> : BaseComponentUpgrade<C, T> where T : ISubtractionOperators<T, T, T>, IAdditionOperators<T, T, T>, IComparable<T>, IConvertible, IMinMaxValue<T>
{
	private ValueAggregator<T> Aggregate => field ??= FindAggregate;
	protected abstract ValueAggregator<T> FindAggregate { get; }

	protected override T EffectBase
	{
		get
		{
			var agg = Aggregate;
			return agg is null ? default : agg.Without( this );
		}
	}

	protected override void OnRankChanged()
	{
		Aggregate?.SetContribution( this, CurrentEffect );
	}
}

public abstract class IntAggregateUpgrade<C> : BaseAggregateUpgrade<C, int>
{
	protected override int GetEffectAt( int rank ) => EffectPerRank.MultiplyCapped( rank );
}

public abstract class FloatAggregateUpgrade<C> : BaseAggregateUpgrade<C, float>
{
	protected override bool IsUnderCap => FloatEffectImprovement.TestCap( OverallEffect, EffectMax );
	protected override float GetEffectAt( int rank ) => EffectPerRank.MultiplyCapped( rank );
}

public abstract class DoubleAggregateUpgrade<C> : BaseAggregateUpgrade<C, double>
{
	protected override double GetEffectAt( int rank ) => EffectPerRank.MultiplyCapped( rank );
}

public abstract class PercentageAggregateUpgrade<C> : FloatAggregateUpgrade<C>
{
	public override string GetDisplayedEffectAt( int rank ) => PercentageEffectImprovement.GetDisplayedPercentage( GetOveralEffect( rank ), DecimalPlaces );
}