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

#include "MusicSubsystem.h"

#include "Components/AudioComponent.h"
#include "Engine/Engine.h"
#include "Engine/GameInstance.h"
#include "Engine/World.h"
#include "Kismet/GameplayStatics.h"
#include "MusicPlaylistDataAsset.h"
#include "Sound/SoundBase.h"
#include "Sound/SoundClass.h"
#include "SurvivalAudioSettings.h"
#include "UObject/UObjectGlobals.h"

UMusicSubsystem* UMusicSubsystem::Get(const UObject* WorldContextObject)
{
	if (!GEngine || !WorldContextObject)
	{
		return nullptr;
	}

	const UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull);
	UGameInstance* GameInstance = World ? World->GetGameInstance() : nullptr;
	return GameInstance ? GameInstance->GetSubsystem<UMusicSubsystem>() : nullptr;
}

void UMusicSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
	Super::Initialize(Collection);

	// Le seul endroit d'ou l'on voit passer un changement de scene : le lecteur
	// survit aux maps, donc aucun BeginPlay ni aucun EndPlay ne peut jouer ce
	// role -- et la map d'arrivee n'a justement pas a connaître le sujet.
	PostLoadMapHandle = FCoreUObjectDelegates::PostLoadMapWithWorld.AddUObject(this, &UMusicSubsystem::HandlePostLoadMap);
}

void UMusicSubsystem::Deinitialize()
{
	if (PostLoadMapHandle.IsValid())
	{
		FCoreUObjectDelegates::PostLoadMapWithWorld.Remove(PostLoadMapHandle);
		PostLoadMapHandle.Reset();
	}

	CancelPendingGap();

	// Fondu a zero : on quitte le jeu, plus personne n'ecoute.
	FadeOutAndRelease(MusicComponent, 0.f);
	if (FadingOutComponent)
	{
		FadingOutComponent->Stop();
		FadingOutComponent = nullptr;
	}

	Super::Deinitialize();
}

void UMusicSubsystem::PlayPlaylist(UMusicPlaylistDataAsset* Playlist, bool bForceRestart)
{
	if (!Playlist || Playlist->Tracks.IsEmpty())
	{
		StopMusic();
		return;
	}

	// Deja en cours : on la laisse tranquille. C'est tout l'interet de faire
	// pointer la map du menu et celle du jeu vers le meme asset -- le chargement
	// devient inaudible.
	if (CurrentPlaylist == Playlist && bIsPlaying && !bForceRestart)
	{
		return;
	}

	CancelPendingGap();

	// L'ancienne musique s'eteint sur sa propre voie pendant que la nouvelle
	// entre : un composant audio ne peut pas se croiser avec lui-meme.
	FadeOutAndRelease(MusicComponent, CurrentPlaylist ? CurrentPlaylist->FadeOutDuration : Playlist->FadeOutDuration);

	CurrentPlaylist = Playlist;
	bShuffleEnabled = Playlist->bShuffle;
	LastPlayedTrackIndex = INDEX_NONE;
	bIsPlaying = true;

	BuildPlayOrder();
	if (PlayOrder.IsEmpty())
	{
		// Que des cases vides dans la liste.
		bIsPlaying = false;
		return;
	}

	PlayTrackAtCursor(Playlist->FadeInDuration);
}

void UMusicSubsystem::StopMusic(float FadeOutDuration)
{
	CancelPendingGap();

	const float Duration = (FadeOutDuration >= 0.f)
		? FadeOutDuration
		: (CurrentPlaylist ? CurrentPlaylist->FadeOutDuration : 0.f);

	FadeOutAndRelease(MusicComponent, Duration);

	bIsPlaying = false;
	CurrentPlaylist = nullptr;
	PlayOrder.Reset();
	OrderCursor = INDEX_NONE;

	OnMusicTrackChanged.Broadcast(nullptr);
}

void UMusicSubsystem::SkipToNextTrack()
{
	if (!bIsPlaying || !CurrentPlaylist)
	{
		return;
	}

	CancelPendingGap();

	// On LIBERE le composant au lieu de le reutiliser. Stop() et FadeOut()
	// rediffusent OnAudioFinished, et cette diffusion transite par le thread audio :
	// elle peut donc arriver une frame plus tard, soit APRES le demarrage du morceau
	// suivant, ce qui en sauterait un second. Un composant neuf, celui d'avant
	// detache, rend ce retour tardif sans effet.
	const float SkipFade = FMath::Min(CurrentPlaylist->FadeOutDuration, 1.f);
	FadeOutAndRelease(MusicComponent, SkipFade);

	AdvanceAndPlay(SkipFade);
}

void UMusicSubsystem::SetShuffleEnabled(bool bEnabled)
{
	if (bShuffleEnabled == bEnabled)
	{
		return;
	}

	bShuffleEnabled = bEnabled;

	if (!CurrentPlaylist)
	{
		return;
	}

	// Le morceau en cours continue ; seule la SUITE change. On recale le curseur
	// dessus pour qu'en mode ordonne la lecture reprenne juste apres lui, et non
	// au debut de la liste.
	BuildPlayOrder();
	const int32 Found = PlayOrder.IndexOfByKey(LastPlayedTrackIndex);
	OrderCursor = (Found != INDEX_NONE) ? Found : 0;
}

void UMusicSubsystem::SetMusicVolume(float NewVolume)
{
	MusicVolume = FMath::Clamp(NewVolume, 0.f, 1.f);

	if (MusicComponent)
	{
		// SetVolumeMultiplier est independant du fader utilise par FadeIn/FadeOut :
		// regler le volume au milieu d'un fondu ne l'interrompt pas.
		MusicComponent->SetVolumeMultiplier(GetEffectiveVolume());
	}
}

USoundBase* UMusicSubsystem::GetCurrentTrack() const
{
	return MusicComponent ? MusicComponent->Sound : nullptr;
}

void UMusicSubsystem::HandleTrackFinished()
{
	// N'est atteint que par une fin naturelle : toute coupure manuelle passe par
	// FadeOutAndRelease, qui detache le delegue avant d'arreter le son.
	if (!bIsPlaying || !CurrentPlaylist)
	{
		return;
	}

	const float MinGap = FMath::Max(0.f, CurrentPlaylist->MinGapBetweenTracks);
	const float MaxGap = FMath::Max(MinGap, CurrentPlaylist->MaxGapBetweenTracks);
	const float Gap = FMath::FRandRange(MinGap, MaxGap);

	if (Gap > KINDA_SMALL_NUMBER)
	{
		ScheduleGap(Gap);
	}
	else
	{
		AdvanceAndPlay(0.f);
	}
}

void UMusicSubsystem::HandlePostLoadMap(UWorld* LoadedWorld)
{
	// En PIE plusieurs instances peuvent charger des maps en parallele : on ne
	// reagit qu'a la nôtre.
	if (!LoadedWorld || LoadedWorld->GetGameInstance() != GetGameInstance())
	{
		return;
	}

	if (LevelChangeMode != EMusicLevelChangeMode::New || !bIsPlaying)
	{
		return;
	}

	// Volontairement apres le chargement et non avant : le nouveau morceau entre
	// en meme temps que la nouvelle scene s'affiche, pas pendant l'ecran de
	// chargement ou personne ne l'associe a quoi que ce soit.
	SkipToNextTrack();
}

void UMusicSubsystem::BuildPlayOrder()
{
	PlayOrder.Reset();
	OrderCursor = 0;

	if (!CurrentPlaylist)
	{
		return;
	}

	PlayOrder.Reserve(CurrentPlaylist->Tracks.Num());
	for (int32 Index = 0; Index < CurrentPlaylist->Tracks.Num(); ++Index)
	{
		// Filtre en amont : une case vide ne doit jamais devenir un morceau a jouer,
		// sinon il faudrait la sauter au moment de la lecture -- et une liste
		// entierement vide y bouclerait a l'infini.
		if (CurrentPlaylist->Tracks[Index])
		{
			PlayOrder.Add(Index);
		}
	}

	if (!bShuffleEnabled || PlayOrder.Num() < 2)
	{
		return;
	}

	// Fisher-Yates : chaque permutation equiprobable, une seule passe.
	for (int32 Index = PlayOrder.Num() - 1; Index > 0; --Index)
	{
		PlayOrder.Swap(Index, FMath::RandRange(0, Index));
	}

	// Un nouveau cycle ne redemarre jamais sur le morceau qui vient de finir.
	// C'est la difference entre "aleatoire" et "aleatoire agreable a ecouter".
	if (PlayOrder[0] == LastPlayedTrackIndex)
	{
		PlayOrder.Swap(0, FMath::RandRange(1, PlayOrder.Num() - 1));
	}
}

void UMusicSubsystem::AdvanceAndPlay(float FadeInDuration)
{
	if (!CurrentPlaylist)
	{
		return;
	}

	++OrderCursor;

	if (!PlayOrder.IsValidIndex(OrderCursor))
	{
		if (!CurrentPlaylist->bLoop)
		{
			StopMusic(CurrentPlaylist->FadeOutDuration);
			return;
		}

		BuildPlayOrder();
		if (PlayOrder.IsEmpty())
		{
			StopMusic(0.f);
			return;
		}
	}

	PlayTrackAtCursor(FadeInDuration);
}

void UMusicSubsystem::PlayTrackAtCursor(float FadeInDuration)
{
	if (!CurrentPlaylist || !PlayOrder.IsValidIndex(OrderCursor))
	{
		return;
	}

	const int32 TrackIndex = PlayOrder[OrderCursor];
	USoundBase* Track = CurrentPlaylist->Tracks.IsValidIndex(TrackIndex) ? CurrentPlaylist->Tracks[TrackIndex].Get() : nullptr;
	if (!Track)
	{
		// La playlist a ete editee pendant la lecture : on reconstruit plutot que
		// de jouer un index perime.
		BuildPlayOrder();
		return;
	}

	// Un curseur de volume ne doit RIEN faire d'autre que changer le volume.
	//
	// Sans PlayWhenSilent, une source devenue inaudible -- curseur Musique a zero --
	// est declaree TERMINEE par le moteur : OnAudioFinished part, HandleTrackFinished
	// enchaîne, et le silence entre deux morceaux (MinGapBetweenTracks) demarre. Le
	// joueur qui remonte son volume n'entend alors plus rien jusqu'a la piste
	// suivante, alors qu'il n'a fait que bouger un reglage. Avec ce mode le morceau
	// continue d'avancer sans etre rendu, et redevient simplement audible.
	//
	// Pose ICI et pas sur les assets, pour la meme raison que la SoundClass dans
	// CreateMusicComponent : ajouter une piste a une playlist ne doit rien demander
	// de plus. Ecrire ce champ ne salit pas l'asset -- rien n'appelle MarkPackageDirty.
	Track->VirtualizationMode = EVirtualizationMode::PlayWhenSilent;

	if (!MusicComponent)
	{
		MusicComponent = CreateMusicComponent(Track);
		if (!MusicComponent)
		{
			bIsPlaying = false;
			return;
		}
	}
	else
	{
		MusicComponent->SetSound(Track);
	}

	MusicComponent->SetVolumeMultiplier(GetEffectiveVolume());

	if (FadeInDuration > 0.f)
	{
		MusicComponent->FadeIn(FadeInDuration, 1.f);
	}
	else
	{
		MusicComponent->Play();
	}

	LastPlayedTrackIndex = TrackIndex;
	OnMusicTrackChanged.Broadcast(Track);
}

UAudioComponent* UMusicSubsystem::CreateMusicComponent(USoundBase* Sound)
{
	const UGameInstance* GameInstance = GetGameInstance();
	UWorld* World = GameInstance ? GameInstance->GetWorld() : nullptr;
	if (!World || !Sound)
	{
		return nullptr;
	}

	// bPersistAcrossLevelTransition : le composant est cree sans monde ni acteur
	// proprietaire et marque bIgnoreForFlushing, le seul moyen de traverser un
	// OpenLevel sans que le peripherique audio ne le coupe.
	// bAutoDestroy a faux : on veut le reutiliser d'un morceau au suivant, et
	// surtout recevoir son OnAudioFinished plutot que de le voir disparaître.
	UAudioComponent* Component = UGameplayStatics::CreateSound2D(
		World, Sound,
		/*VolumeMultiplier*/ 1.f, /*PitchMultiplier*/ 1.f, /*StartTime*/ 0.f,
		/*ConcurrencySettings*/ nullptr,
		/*bPersistAcrossLevelTransition*/ true,
		/*bAutoDestroy*/ false);

	if (!Component)
	{
		return nullptr;
	}

	// La classe est imposee ICI plutot que sur chaque asset de morceau : ajouter une
	// piste a une playlist ne doit pas demander de penser a la taguer, sinon le
	// curseur Musique cesse d'agir sur elle sans le moindre signe.
	if (USoundClass* MusicClass = USurvivalAudioSettings::Get()->GetMusicSoundClass())
	{
		Component->SoundClassOverride = MusicClass;
	}

	Component->bIsMusic = true;
	Component->OnAudioFinished.AddDynamic(this, &UMusicSubsystem::HandleTrackFinished);
	return Component;
}

void UMusicSubsystem::FadeOutAndRelease(TObjectPtr<UAudioComponent>& Component, float FadeOutDuration)
{
	if (!Component)
	{
		return;
	}

	// Detacher AVANT d'arreter : Stop() comme FadeOut() finissent par rediffuser
	// OnAudioFinished, qui enchainerait sur le morceau suivant alors qu'on cherche
	// justement a couper.
	Component->OnAudioFinished.RemoveDynamic(this, &UMusicSubsystem::HandleTrackFinished);

	// Une seule voie de sortie : si un croisement precedent n'est pas fini, il est
	// coupe net. Empiler les fondus ferait monter le nombre de voix pour rien.
	if (FadingOutComponent && FadingOutComponent != Component)
	{
		FadingOutComponent->Stop();
		FadingOutComponent = nullptr;
	}

	if (FadeOutDuration > 0.f && Component->IsPlaying())
	{
		Component->FadeOut(FadeOutDuration, 0.f);
		FadingOutComponent = Component;
	}
	else
	{
		Component->Stop();
	}

	Component = nullptr;
}

void UMusicSubsystem::ScheduleGap(float DelaySeconds)
{
	CancelPendingGap();

	TWeakObjectPtr<UMusicSubsystem> WeakThis(this);
	GapTickerHandle = FTSTicker::GetCoreTicker().AddTicker(TEXT("MusicSubsystem.Gap"), DelaySeconds,
		[WeakThis](float /*DeltaTime*/)
		{
			if (UMusicSubsystem* Self = WeakThis.Get())
			{
				Self->GapTickerHandle.Reset();
				Self->AdvanceAndPlay(0.f);
			}
			return false; // un seul declenchement
		});
}

void UMusicSubsystem::CancelPendingGap()
{
	if (GapTickerHandle.IsValid())
	{
		FTSTicker::RemoveTicker(GapTickerHandle);
		GapTickerHandle.Reset();
	}
}

float UMusicSubsystem::GetEffectiveVolume() const
{
	return MusicVolume * (CurrentPlaylist ? CurrentPlaylist->VolumeMultiplier : 1.f);
}
