// Fill out your copyright notice in the Description page of Project Settings.


#include "SurvivalUserSettings.h"

#include "AudioDevice.h"
#include "Engine/Engine.h"
#include "Engine/World.h"
#include "Misc/App.h"
#include "Sound/AudioSettings.h"
#include "Sound/SoundClass.h"
#include "Sound/SoundMix.h"
#include "SurvivalAudioSettings.h"

namespace
{
	/** Arrondit sur le palier le plus proche, puis borne. */
	float SnapToStep(float Value, float Min, float Max, float Step)
	{
		const float Snapped = Min + FMath::RoundToFloat((Value - Min) / Step) * Step;
		return FMath::Clamp(Snapped, Min, Max);
	}

	/** Pourcentage affiche -> amplitude reelle, courbe perceptive comprise. */
	float VolumePercentToAmplitude(float Percent)
	{
		using namespace SurvivalSettingsRange;

		const float Normalized = FMath::Clamp(Percent, VolumeMin, VolumeMax) / 100.f;
		return FMath::Pow(Normalized, VolumeCurveExponent);
	}

	/**
	 * Le peripherique audio a qui parler.
	 *
	 * En PIE le monde de jeu peut avoir le sien, distinct de celui de l'editeur :
	 * viser systematiquement le peripherique principal ferait regler le volume de
	 * l'editeur pendant que le jeu, lui, ne bouge pas.
	 */
	FAudioDevice* GetSettingsAudioDevice()
	{
		if (!GEngine)
		{
			return nullptr;
		}

		if (const UWorld* PlayWorld = GEngine->GetCurrentPlayWorld())
		{
			if (FAudioDevice* WorldDevice = PlayWorld->GetAudioDeviceRaw())
			{
				return WorldDevice;
			}
		}

		return GEngine->GetMainAudioDeviceRaw();
	}

	/**
	 * Le mix par lequel passent les volumes du joueur.
	 *
	 * C'est celui de Project Settings > Audio > Default Base Sound Mix, et pas une
	 * reference a nous. Deux raisons : le moteur le REPOSE a chaque chargement de
	 * map (UWorld::UpdateWorldComponents -> SetDefaultBaseSoundMix), donc il est
	 * toujours actif sans qu'on ait a le pousser nous-memes ; et un mix de base est
	 * evalue a plein regime en permanence, alors qu'un mix pousse a la main
	 * s'eteindrait au bout de sa duree.
	 */
	USoundMix* GetSettingsSoundMix()
	{
		const FSoftObjectPath& MixPath = GetDefault<UAudioSettings>()->DefaultBaseSoundMix;
		return MixPath.IsValid() ? Cast<USoundMix>(MixPath.TryLoad()) : nullptr;
	}

	/**
	 * Pose le volume d'une SoundClass, en pourcentage.
	 *
	 * bApplyToChildren merite une explication, parce que se tromper la donne un
	 * volume maitre qui n'agit que sur les sons non tagues : le moteur propage les
	 * proprietes du parent vers les enfants (ParseSoundClasses) AVANT d'appliquer
	 * les ajusteurs du mix (ApplyClassAdjusters). Un ajusteur pose sur la classe
	 * maitre arrive donc trop tard pour redescendre tout seul -- il faut demander
	 * explicitement la recursion. Les feuilles, elles, gardent false : leur
	 * ajusteur n'a personne a qui redescendre.
	 *
	 * Aucun fondu non plus, et ce n'est pas un oubli : toute duree non nulle rend le
	 * curseur inutilisable. Quand on repose un override alors que le precedent n'a
	 * pas fini son fondu, UpdateClassAdjustorOverrideEntry relance l'interpolation
	 * (FDynamicParameter::Set remet CurrTimeSec a zero) mais n'appelle PAS
	 * Update(DeltaTime) cette frame-la. Un curseur qu'on glisse repose l'override a
	 * chaque image : la rampe repart indefiniment de zero et GetValue() rend toujours
	 * la valeur precedente. Le volume ne rattrape son retard qu'une fois le geste
	 * ARRETE. A zero, Set() ecrit directement la valeur -- et le mixeur lisse deja le
	 * gain de chaque source dans son buffer de rendu, donc pas de clic pour autant.
	 */
	void ApplyClassVolume(FAudioDevice& AudioDevice, USoundMix& Mix, USoundClass* SoundClass, float Percent, bool bApplyToChildren)
	{
		if (!SoundClass)
		{
			return;
		}

		AudioDevice.SetSoundMixClassOverride(&Mix, SoundClass,
			VolumePercentToAmplitude(Percent), /*Pitch*/ 1.f, /*FadeInTime*/ 0.f, bApplyToChildren);
	}
}

USurvivalUserSettings::USurvivalUserSettings()
{
	// Le constructeur tourne sur le CDO, avant toute lecture du .ini : ces
	// valeurs sont donc bien les defauts d'usine, ecrasees ensuite par le
	// fichier s'il existe.
	SetSurvivalGameplayToDefaults();
	SetSurvivalDisplayToDefaults();
	SetSurvivalAudioToDefaults();
}

USurvivalUserSettings* USurvivalUserSettings::Get()
{
	return GEngine ? Cast<USurvivalUserSettings>(GEngine->GetGameUserSettings()) : nullptr;
}

void USurvivalUserSettings::SetSurvivalGameplayToDefaults()
{
	LookSensitivity = 35.f;
	bSeparateVerticalSensitivity = false;
	LookSensitivityVertical = 35.f;
	bInvertLookY = false;
	FieldOfView = 90.f;
	CameraShakeScale = 100.f;
	bSprintToggle = false;
	bCrouchToggle = false;
}

void USurvivalUserSettings::SetSurvivalDisplayToDefaults()
{
	Brightness = 100.f;
}

void USurvivalUserSettings::SetSurvivalAudioToDefaults()
{
	// 80 et non 100 sur le maitre : il faut laisser au joueur de la marge vers le
	// haut, sinon le seul reglage possible est de baisser.
	MasterVolume = 80.f;

	// La musique sous les effets, comme partout : elle est continue, eux sont des
	// evenements, et un evenement doit passer par-dessus le fond.
	MusicVolume = 60.f;
	EffectsVolume = 100.f;
	UiVolume = 80.f;

	// Le defaut du moteur, qu'on se contente d'exposer.
	bPlayInBackground = false;
}

void USurvivalUserSettings::SetToDefaults()
{
	// Le "Reinitialiser" global du moteur doit aussi remettre NOS valeurs :
	// sans cette surcharge, elles resteraient seules inchangees.
	Super::SetToDefaults();

	SetSurvivalGameplayToDefaults();
	SetSurvivalDisplayToDefaults();
	SetSurvivalAudioToDefaults();
}

void USurvivalUserSettings::SetBrightness(float Value)
{
	using namespace SurvivalSettingsRange;
	Brightness = SnapToStep(Value, BrightnessMin, BrightnessMax, BrightnessStep);
}

void USurvivalUserSettings::ApplySurvivalSettings()
{
	// Le gamma n'appartient pas a UGameUserSettings : il vit sur GEngine, donc
	// sur un objet GLOBAL au processus. Deux consequences :
	//
	// 1. DisplayGamma est un EXPOSANT dont la valeur moteur est 2.2, pas un
	//    multiplicateur neutre a 1. On module la reference, on ne la remplace
	//    pas.
	// 2. En editeur, cet objet est celui de l'editeur : y toucher assombrit
	//    toute l'interface, y compris apres avoir quitte le PIE. On ne l'applique
	//    donc qu'en jeu. La luminosite ne se teste pas en PIE, mais en
	//    Standalone -- qui est un processus separe.
	if (GEngine && !GIsEditor)
	{
		using namespace SurvivalSettingsRange;

		const float Normalized = FMath::Clamp(Brightness, BrightnessMin, BrightnessMax) / 100.f;
		GEngine->DisplayGamma = BaseDisplayGamma * Normalized;
	}

	OnSurvivalSettingsApplied.Broadcast();
}

void USurvivalUserSettings::ApplyAudioSettings()
{
	// Ce reglage-la ne passe pas par le mix : c'est l'application entiere qui est
	// baissee quand la fenetre perd le focus, bien en amont des SoundClass. Le
	// moteur le stocke dans son propre .ini, d'ou une valeur reposee a chaque
	// application plutot que memorisee ailleurs.
	FApp::SetUnfocusedVolumeMultiplier(bPlayInBackground ? 1.f : 0.f);

	FAudioDevice* AudioDevice = GetSettingsAudioDevice();
	if (!AudioDevice)
	{
		// Normal tres tot au demarrage, ou en serveur dedie : les valeurs sont
		// deja enregistrees, le prochain passage les posera.
		return;
	}

	USoundMix* Mix = GetSettingsSoundMix();
	if (!Mix)
	{
		UE_LOG(LogTemp, Warning, TEXT("USurvivalUserSettings : aucun Default Base Sound Mix dans Project Settings > Audio, les curseurs de volume resteront sans effet."));
		return;
	}

	USurvivalAudioSettings* AudioAssets = USurvivalAudioSettings::Get();

	// Le maitre EN PREMIER et avec recursion : il redescend sur les trois autres,
	// qui reappliquent ensuite la leur par-dessus. Les deux se multiplient, ce qui
	// est exactement ce qu'on attend d'un volume general.
	ApplyClassVolume(*AudioDevice, *Mix, AudioAssets->GetMasterSoundClass(), MasterVolume, /*bApplyToChildren*/ true);
	ApplyClassVolume(*AudioDevice, *Mix, AudioAssets->GetMusicSoundClass(), MusicVolume, /*bApplyToChildren*/ false);
	ApplyClassVolume(*AudioDevice, *Mix, AudioAssets->GetEffectsSoundClass(), EffectsVolume, /*bApplyToChildren*/ false);
	ApplyClassVolume(*AudioDevice, *Mix, AudioAssets->GetUiSoundClass(), UiVolume, /*bApplyToChildren*/ false);
}

void USurvivalUserSettings::ApplyNonResolutionSettings()
{
	Super::ApplyNonResolutionSettings();

	// ApplySettings() passe forcement par ici : brancher la diffusion a ce
	// niveau garantit que nos reglages suivent, quel que soit le chemin.
	ApplySurvivalSettings();
	ApplyAudioSettings();
}

float USurvivalUserSettings::GetLookMultiplierX() const
{
	using namespace SurvivalSettingsRange;

	const float Normalized = FMath::GetRangePct(SensitivityMin, SensitivityMax, LookSensitivity);
	return FMath::Lerp(LookMultiplierAtMin, LookMultiplierAtMax, Normalized);
}

float USurvivalUserSettings::GetLookMultiplierY() const
{
	using namespace SurvivalSettingsRange;

	// Sans axe separe, le vertical suit l'horizontal : c'est ce que veulent
	// 95 % des joueurs, et deux curseurs desynchronises par inadvertance
	// donnent une visee bancale sans qu'on comprenne pourquoi.
	if (!bSeparateVerticalSensitivity)
	{
		return GetLookMultiplierX();
	}

	const float Normalized = FMath::GetRangePct(SensitivityMin, SensitivityMax, LookSensitivityVertical);
	return FMath::Lerp(LookMultiplierAtMin, LookMultiplierAtMax, Normalized);
}

float USurvivalUserSettings::GetCameraShakeMultiplier() const
{
	return FMath::Clamp(CameraShakeScale / 100.f, 0.f, 1.f);
}

void USurvivalUserSettings::SetLookSensitivity(float Value)
{
	using namespace SurvivalSettingsRange;
	LookSensitivity = SnapToStep(Value, SensitivityMin, SensitivityMax, SensitivityStep);
}

void USurvivalUserSettings::SetSeparateVerticalSensitivity(bool bValue)
{
	// En repassant sur "non separe" on recale le vertical sur l'horizontal :
	// sinon la valeur oubliee ressortirait telle quelle a la reactivation.
	if (!bValue)
	{
		LookSensitivityVertical = LookSensitivity;
	}

	bSeparateVerticalSensitivity = bValue;
}

void USurvivalUserSettings::SetLookSensitivityVertical(float Value)
{
	using namespace SurvivalSettingsRange;
	LookSensitivityVertical = SnapToStep(Value, SensitivityMin, SensitivityMax, SensitivityStep);
}

void USurvivalUserSettings::SetInvertLookY(bool bValue)
{
	bInvertLookY = bValue;
}

void USurvivalUserSettings::SetFieldOfView(float Value)
{
	using namespace SurvivalSettingsRange;
	FieldOfView = SnapToStep(Value, FieldOfViewMin, FieldOfViewMax, FieldOfViewStep);
}

void USurvivalUserSettings::SetCameraShakeScale(float Value)
{
	using namespace SurvivalSettingsRange;
	CameraShakeScale = SnapToStep(Value, CameraShakeMin, CameraShakeMax, CameraShakeStep);
}

void USurvivalUserSettings::SetSprintToggle(bool bValue)
{
	bSprintToggle = bValue;
}

void USurvivalUserSettings::SetCrouchToggle(bool bValue)
{
	bCrouchToggle = bValue;
}

void USurvivalUserSettings::SetMasterVolume(float Value)
{
	using namespace SurvivalSettingsRange;
	MasterVolume = SnapToStep(Value, VolumeMin, VolumeMax, VolumeStep);
}

void USurvivalUserSettings::SetMusicVolume(float Value)
{
	using namespace SurvivalSettingsRange;
	MusicVolume = SnapToStep(Value, VolumeMin, VolumeMax, VolumeStep);
}

void USurvivalUserSettings::SetEffectsVolume(float Value)
{
	using namespace SurvivalSettingsRange;
	EffectsVolume = SnapToStep(Value, VolumeMin, VolumeMax, VolumeStep);
}

void USurvivalUserSettings::SetUiVolume(float Value)
{
	using namespace SurvivalSettingsRange;
	UiVolume = SnapToStep(Value, VolumeMin, VolumeMax, VolumeStep);
}

void USurvivalUserSettings::SetPlayInBackground(bool bValue)
{
	bPlayInBackground = bValue;
}

void USurvivalUserSettings::SetCharacterAppearance(const FCharacterAppearance& InAppearance)
{
	CharacterAppearance = InAppearance;
}
