Game/LevelProgress.cs
namespace BlockParty;

/// <summary>
/// Per-player level-map progression: which levels have been beaten and which map node was last
/// highlighted. Backed by the persisted <see cref="GameSettings"/> (so it lives in the same
/// settings file as the other player preferences), written through immediately on every change so
/// progress survives a crash mid-session.
/// </summary>
public static class LevelProgress
{
	/// <summary>Bump this to reset campaign progression for every player on their next launch. Also
	/// retires the cloud recovery mirror with it — <see cref="CloudProgress"/> bakes this version into
	/// its stat names, so an old mirror can't resurrect deliberately wiped progress.</summary>
	public const int PROGRESS_VERSION = 9;

	/// <summary>True if the given level id (see <see cref="LevelDef.Id"/>) has been beaten.</summary>
	public static bool IsBeaten( string levelId )
	{
		if ( string.IsNullOrEmpty( levelId ) )
			return false;
		return Settings.Current.BeatenLevelIds.Contains( levelId );
	}

	/// <summary>Record a level as beaten and persist. No-op if it was already recorded, so calling it
	/// after every finished run is cheap and idempotent.</summary>
	public static void MarkBeaten( string levelId )
	{
		if ( string.IsNullOrEmpty( levelId ) )
			return;

		var beaten = Settings.Current.BeatenLevelIds;
		if ( beaten.Contains( levelId ) )
			return;

		beaten.Add( levelId );
		Settings.Save();
	}

	/// <summary>Set (or clear) a level's beaten flag explicitly and persist. Used by the level-select
	/// screen's DEBUG right-click toggle to author/reset map progress while testing gating.</summary>
	public static void SetBeaten( string levelId, bool beaten )
	{
		if ( string.IsNullOrEmpty( levelId ) )
			return;

		var list = Settings.Current.BeatenLevelIds;
		bool has = list.Contains( levelId );
		if ( beaten == has )
			return; // already in the desired state

		if ( beaten )
			list.Add( levelId );
		else
			list.Remove( levelId );

		Settings.Save();
	}

	/// <summary>Flip a level's beaten flag and persist (DEBUG helper — see <see cref="SetBeaten"/>).</summary>
	public static void ToggleBeaten( string levelId ) => SetBeaten( levelId, !IsBeaten( levelId ) );

	/// <summary>Compare the map's currently selectable levels with the previous level-select arrival,
	/// persist the new snapshot, and return ids which became selectable since that visit. A null
	/// snapshot belongs to a profile predating this feature, so its current unlocks become the baseline
	/// instead of all animating retroactively.</summary>
	public static System.Collections.Generic.IReadOnlyList<string> CaptureNewlyUnlocked(
		System.Collections.Generic.IEnumerable<string> currentlyUnlocked )
	{
		var current = currentlyUnlocked
			.Where( id => !string.IsNullOrEmpty( id ) )
			.Distinct()
			.ToList();
		var previous = Settings.Current.LastSeenUnlockedLevelIds;
		var newlyUnlocked = previous is null
			? new System.Collections.Generic.List<string>()
			: current.Where( id => !previous.Contains( id ) ).ToList();

		if ( previous is null || !current.SequenceEqual( previous ) )
		{
			Settings.Current.LastSeenUnlockedLevelIds = current;
			Settings.Save();
		}
		return newlyUnlocked;
	}

	/// <summary>The level id the map should reopen focused on (null = the first major level).</summary>
	public static string LastSelected => Settings.Current.LastSelectedLevelId;

	/// <summary>Remember the map node the player last highlighted and persist. Cheap enough to call on
	/// every selection change; a no-op write is skipped so unchanged re-selects don't hit disk.</summary>
	public static void SetLastSelected( string levelId )
	{
		if ( Settings.Current.LastSelectedLevelId == levelId )
			return;

		Settings.Current.LastSelectedLevelId = levelId;
		Settings.Save();
	}
}