Spending/Research/Research.cs
using System;
using System.Numerics;
using System.Text.Json.Nodes;

namespace PlanetMeat;

public sealed class Research( Scene scene ) : GameObjectSystem<Research>( scene ), ISaveTarget
{
	private readonly List<BaseImprovement> Improvements = [
		// drill_gun
		new IntEffectImprovement( "drill_dmg", "drill_gun", 5, 5 ) { UpgradeNameIdent = "attack.dmg" },
		new PercentageEffectImprovement( "drill_recharge", "drill_gun", 0.8f, 0.1f, 5.0f ) { UpgradeNameIdent = "attack.recharge" },
		new IntEffectImprovement( "drill_range", "drill_gun", 1000, 100, Agenda.MAX_SPAWN_DISTANCE ) { UpgradeNameIdent = "targeter.range" },
		// drill_hp
		new IntEffectImprovement( "drill_hp", "drill_hp", 100, 10 ) { UpgradeNameIdent = "target.hp" },
		new PercentageEffectImprovement( "drill_resist_perc", "drill_hp", 0.0f, 0.0025f, 0.2f ) { UpgradeNameIdent = "target.resist_perc", RequiredTech = "resists", CostOffset = 9 },
		new IntEffectImprovement( "drill_resist_flat", "drill_hp", 0, 1 ) { UpgradeNameIdent = "target.resist_flat", RequiredTech = "resists", CostOffset = 9 },
		new PercentageEffectImprovement( "drill_regen", "drill_hp", 0.0f, 0.005f, 0.1f ) { UpgradeNameIdent = "target.regen", RequiredTech = "regen", CostOffset = 9 },
		// flame
		new IntEffectImprovement( "flame_dmg", "flame", 1, 1 ) { UpgradeNameIdent = "attack.dmg", CostOffset = 4 },
		new PercentageEffectImprovement( "flame_recharge", "flame", 2.0f, 0.2f, 10.0f ) { UpgradeNameIdent = "attack.recharge" },
		new IntEffectImprovement( "flame_range", "flame", 350, 10, Agenda.MAX_SPAWN_DISTANCE / 4 ) { UpgradeNameIdent = "targeter.range" },
		// mortar
		new IntEffectImprovement( "mortar_dmg", "mortar", 8, 2 ) { UpgradeNameIdent = "attack.dmg" },
		new IntEffectImprovement( "mortar_size", "mortar", 120, 10, 250 ) { UpgradeNameIdent = "strike.size" },
		new PercentageEffectImprovement( "mortar_acc", "mortar", 0.2f, 0.05f, 0.8f ) { UpgradeNameIdent = "strike.acc" },
		new FloatEffectImprovement( "mortar_speed", "mortar", 0.25f, 0.05f, 5.0f ) { UpgradeNameIdent = "strike.speed" },
		new PercentageEffectImprovement( "mortar_cd", "mortar", 1.0f, 0.1f, 7.5f ) { UpgradeNameIdent = "attack.recharge" },
		// heli
		new IntEffectImprovement( "heli_dmg", "heli", 1, 1 ) { UpgradeNameIdent = "heli.dmg", CostOffset = 4 },
		new PercentageEffectImprovement( "heli_duration", "heli", 0.5f, 0.05f, 3.0f ) { UpgradeNameIdent = "duration" },
		new PercentageEffectImprovement( "heli_acc", "heli", 0.5f, 0.05f, 1.0f ) { UpgradeNameIdent = "heli.acc" },
		new FloatEffectImprovement( "heli_speed", "heli", 0.3f, 0.05f, 1.0f ) { UpgradeNameIdent = "strike.speed" },
		new PercentageEffectImprovement( "heli_cd", "heli", 1.0f, 0.1f, 7.5f ) { UpgradeNameIdent = "attack.recharge" },
		// pulse
		new IntEffectImprovement( "pulse_dmg", "pulse", 1, 1 ) { UpgradeNameIdent = "attack.dmg" },
		new IntEffectImprovement( "pulse_size", "pulse", 50, 10, 500 ) { UpgradeNameIdent = "pulse.size"},
		new PercentageEffectImprovement( "pulse_cd", "pulse", 1.0f, 0.1f, 7.5f ) { UpgradeNameIdent = "attack.recharge" },
		// goop_econ
		new StorageImprovement( "goop_storage", "goop_econ", 100, 25 ) { UpgradeNameIdent = "econ.storage", AccountIdent = "goop" },
		new PercentageEffectImprovement( "goop_rate", "goop_econ", 0.0f, 0.01f, 1.0f ) { UpgradeNameIdent = "econ.rate" },
		new DoubleEffectImprovement( "goop_cap", "goop_econ", 20, 10 ) { UpgradeNameIdent = "econ.cap" },
		new IntEffectImprovement( "goop_income", "goop_econ", 0, 3 ) { UpgradeNameIdent = "econ.income" },
		new PercentageEffectImprovement( "goop_bounty", "goop_econ", 1.0f, 0.05f, 5.0f ) { UpgradeNameIdent = "econ.bounty" },
		// meat_econ
		new IntEffectImprovement( "meat_income", "meat_econ", 0, 1 ) { UpgradeNameIdent = "econ.income", CostOffset = 4 },
		new PercentageEffectImprovement( "meat_protect", "meat_econ", 0.4f, 0.01f, 0.8f ) { UpgradeNameIdent = "meat.protect" },
		new PercentageEffectImprovement( "meat_rate", "meat_econ", 1.0f, 0.1f ) { UpgradeNameIdent = "meat.rate", CostStart = 49 },
		// multishot
		new PercentageEffectImprovement( "multishot_rate", "multishot", 0.05f, 0.01f, 0.5f ) { UpgradeNameIdent = "multishot.rate" },
		new IntEffectImprovement( "multishot_amt", "multishot", 1, 1, 25 ) { UpgradeNameIdent = "multishot.amt", CostOffset = 9 },
		new PercentageEffectImprovement( "multishot_dmg", "multishot", 0.2f, 0.1f, 2.5f ) { UpgradeNameIdent = "multishot.dmg" },
		// lifesteal
		new PercentageEffectImprovement( "lifesteal_dmg", "lifesteal", 0.01f, 0.01f, 0.25f ) { UpgradeNameIdent = "lifesteal.dmg" },
		new IntEffectImprovement( "lifesteal_kill_amt", "lifesteal", 1, 1, 10 ) { UpgradeNameIdent = "lifesteal.kill.amt" },
		new PercentageEffectImprovement( "lifesteal_kill_rate", "lifesteal", 0.01f, 0.01f, 0.3f ) { UpgradeNameIdent = "lifesteal.kill.rate" },
		// thorns
		new PercentageEffectImprovement( "thorns_reflect", "thorns", 0.0f, 0.01f, 2.0f ) { UpgradeNameIdent = "thorns.reflect" },
		new IntEffectImprovement( "phoenix_charges", "thorns", 0, 1, 10 ) { UpgradeNameIdent = "phoenix.charges", CostStart = 100 },
		new PercentageEffectImprovement( "phoenix_regen", "thorns", 0.25f, 0.05f, 1.0f ) { UpgradeNameIdent = "phoenix.regen" },
		new IntEffectImprovement( "phoenix_dmg", "thorns", 50, 25 ) { UpgradeNameIdent = "phoenix.dmg" },
		new IntEffectImprovement( "phoenix_range", "thorns", 500, 150, Agenda.MAX_SPAWN_DISTANCE ) { UpgradeNameIdent = "phoenix.range" },
	];
	private IEnumerable<BaseImprovement> SavedImprovements => Improvements.Append( ImproveResearch );
	private readonly Dictionary<string, BaseImprovement> FetchedImprovements = [];

	public BaseImprovement GetImprovement( string ident )
	{
		if ( FetchedImprovements.TryGetValue( ident, out BaseImprovement fetched ) )
			return fetched;

		var found = Improvements.Find( imp => imp.Ident == ident );
		if ( found is null )
			Log.Warning( $"No such improvement '{ident}'" );
		else
			FetchedImprovements[ident] = found;

		return found;
	}

	public T GetImprovementEffect<T>( string ident, T fallback = default ) where T : IComparable<T>, IAdditionOperators<T, T, T>
	{
		if ( ident != "" )
		{
			if ( GetImprovement( ident ) is BaseEffectImprovement<T> imp )
				return imp.CurrentEffect;
			Log.Warning( $"Couldn't find valid '{ident}' improvement, falling back to {fallback}" );
		}
		return fallback;
	}

	private void BakeImprovements()
	{
		Catalogue.Current.BakeUpgrades( Improvements );
	}

	private readonly Dictionary<string, BaseTech> Techs = new()
	{
		["mortar"] = new BaseTech( "mortar", 1 ) { ShowTool = true },
		["flame"] = new BaseTech( "flame", 1 ) { ShowTool = true },
		["heli"] = new BaseTech( "heli", 5 ) { ShowTool = true },
		["pulse"] = new BaseTech( "pulse", 9 ) { ShowTool = true },
		["skip_rounds"] = new BaseTech( "skip_rounds", 2 ),
		["scout"] = new BaseTech( "scout", 1 ),
		["resists"] = new BaseTech( "resists", 3 ),
		["research_speed"] = new BaseTech( "research_speed", 3 ),
		["regen"] = new BaseTech( "regen", 6 ),
		["goop_econ"] = new BaseTech( "goop_econ", 6 ),
		["multishot"] = new BaseTech( "multishot", 15 ),
		["lifesteal"] = new BaseTech( "lifesteal", 18 ),
		["meat_econ"] = new BaseTech( "meat_econ", 23 ),
		["thorns"] = new BaseTech( "thorns", 30 ),
	};

	public BaseTech GetTech( string ident ) => Techs.GetValueOrDefault( ident, null );

	public List<BaseTech> AvailableTechs
	{
		get
		{
			if ( field is null )
				BakeTechs();
			return field;
		}
		private set;
	} = null;
	public List<string> ActiveTechTools
	{
		get
		{
			if ( field is null )
				BakeTechs();
			return field;
		}
		private set;
	} = null;

	public BaseTech ResearchTarget
	{
		get => field ??= GetTech( "mortar" );
		set
		{
			CompletionAlert = false;
			IgnoreCompletion = false;

			if ( value is null )
			{
				field = AvailableTechs.FirstOrDefault( t => !t.Completed, field );
			}
			else if ( !value.Completed )
			{
				field = value;
			}
		}
	}
	public bool IgnoreCompletion { get; private set; } = false;
	public bool CompletionAlert { get; private set; } = false;
	public ImproveResearch ImproveResearch { get; } = new();

	public int ResearchSpeed => ImproveResearch.CurrentEffect;
	public int ResearchRoundsRemaining => (int)Math.Ceiling( (float)ResearchTarget.Remaining / ResearchSpeed );

	public JsonNode NextSave
	{
		get
		{
			var techs = Techs.ToSaveObject();

			var improves = new JsonObject();
			foreach ( var imp in SavedImprovements )
				improves.Add( imp.Ident, imp.NextSave );

			return new JsonObject
			{
				{ "Research", ResearchTarget.Ident },
				{ "Techs", techs },
				{ "Imps", improves }
			};
		}
	}

	public void AdvanceResearchProgress()
	{
		if ( !ResearchTarget.Completed )
		{
			ResearchTarget.AdvanceProgress( ResearchSpeed );
			if ( ResearchTarget.Completed && !IgnoreCompletion )
			{
				Career.Current.IncrementStat( "tech" );
				CompletionAlert = true;
			}
		}
	}

	private void BakeTechs()
	{
		AvailableTechs = [.. Techs.Values.Where( t => t.Visible )];
		AvailableTechs.Sort( ( a, b ) =>
			{
				var aCompleted = a.Completed;
				if ( aCompleted != b.Completed )
					return aCompleted ? 1 : -1;

				var rem = a.Remaining - b.Remaining;
				if ( rem != 0 )
					return rem;
				return Game.Language.GetPhrase( a.DisplayName ).CompareTo( Game.Language.GetPhrase( b.DisplayName ) );
			}
		);

		ActiveTechTools = [.. Techs.Values.Where( t => t.Completed && t.ShowTool ).Select( t => t.Ident )];
		ActiveTechTools.Add( "drill_gun" );
		ActiveTechTools.Sort( Catalogue.Current.SortCategories );
	}

	public bool IsTech( string ident ) => Techs.ContainsKey( ident );
	public bool HasTech( string ident ) => ident == "" || GetTech( ident ).Completed;

	public void BakeCatalogue()
	{
		CompletionAlert = false;
		IgnoreCompletion = false;
		BakeImprovements();
		BakeTechs();
		if ( ResearchTarget.Completed )
			ResearchTarget = null;
	}

	public void BakeEffects()
	{
		foreach ( var imp in Improvements )
			imp.BakeEffects();
	}

	public void IgnoreCompletedResearch()
	{
		IgnoreCompletion = ResearchTarget.Completed;
	}

	public void LoadFromSave( JsonNode save )
	{
		if ( save.AsObject() is JsonObject obj )
		{
			if ( obj.TryGetPropertyNode( "Techs", out JsonObject techs ) )
				Techs.LoadFromSaveObject( techs );

			if ( obj.TryGetPropertyNode( "Imps", out JsonObject savedImps ) )
			{
				foreach ( var imp in SavedImprovements )
				{
					if ( savedImps.TryGetPropertyNode( imp.Ident, out JsonNode savedImp ) )
						imp.LoadFromSave( savedImp );
				}
			}

			if ( obj.TryGetTypedProperty( "Research", out string researchIdent ) )
				ResearchTarget = GetTech( researchIdent );

			BakeCatalogue();
		}
	}

	public void ClearSave()
	{
		Techs.ClearSavedInfo();
		foreach ( var imp in SavedImprovements )
			imp.ClearSave();
		BakeCatalogue();
		ResearchTarget = GetTech( "mortar" );
	}
}