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

#pragma once

#include "CoreMinimal.h"
#include "Containers/Ticker.h"
#include "Subsystems/GameInstanceSubsystem.h"
#include "MusicSubsystem.generated.h"

class UAudioComponent;
class UMusicPlaylistDataAsset;
class USoundBase;

DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnMusicTrackChanged, USoundBase*, NewTrack);

/**
 * Ce qui arrive au morceau en cours quand on change de scene.
 *
 * La question ne se pose que parce que le lecteur, lui, ne s'arrete jamais :
 * sans consigne, la musique du menu se poursuit telle quelle dans la partie, au
 * milieu du morceau ou elle en etait.
 */
UENUM(BlueprintType)
enum class EMusicLevelChangeMode : uint8
{
	/** Le morceau en cours poursuit sa route, comme si de rien n'etait. */
	Continue UMETA(DisplayName = "Continue"),

	/**
	 * Le morceau en cours s'eteint et le suivant demarre : nouveau tirage si
	 * l'aleatoire est actif, morceau suivant de la liste sinon. La playlist, elle,
	 * ne change pas.
	 */
	New UMETA(DisplayName = "New")
};

/**
 * Le lecteur de musique du jeu. Un seul, pour toute la duree de l'application.
 *
 * UGameInstanceSubsystem et pas un acteur : il naît avec le GameInstance et meurt
 * avec le jeu, exactement comme USurvivalGameInstance. Aucune map ne le possede,
 * donc aucun OpenLevel ne le detruit -- c'est le seul endroit d'ou une musique
 * peut traverser le passage menu -> partie sans se couper. Le moteur l'instancie
 * tout seul, il n'y a rien a poser dans une scene ni a cocher nulle part.
 *
 * Le composant audio lui-meme est cree via CreateSound2D avec
 * bPersistAcrossLevelTransition : le moteur le construit alors SANS monde et sans
 * proprietaire, et lui pose bIgnoreForFlushing -- le drapeau que le vidage du
 * peripherique audio, declenche a chaque changement de niveau, saute
 * explicitement. Sans ce parametre le son s'arreterait net au chargement, meme si
 * l'objet C++, lui, survivait.
 *
 * Effet de bord voulu : ces composants sont marques bIsUISound, donc ils
 * continuent de jouer quand le jeu est en pause. Le menu de pause garde sa
 * musique sans une ligne de plus.
 */
UCLASS()
class EMBERWILD_API UMusicSubsystem : public UGameInstanceSubsystem
{
	GENERATED_BODY()

public:
	virtual void Initialize(FSubsystemCollectionBase& Collection) override;
	virtual void Deinitialize() override;

	/** Raccourci depuis n'importe quel objet ayant un monde. */
	UFUNCTION(BlueprintPure, Category = "Musique", meta = (WorldContext = "WorldContextObject", DisplayName = "Get Music Subsystem"))
	static UMusicSubsystem* Get(const UObject* WorldContextObject);

	/**
	 * Demarre une playlist.
	 *
	 * Si c'est DEJA celle qui joue, ne fait rien : c'est ce comportement qui rend
	 * la transition menu -> jeu inaudible quand les deux maps designent la meme
	 * playlist. Si c'en est une autre, croisement entre l'ancienne qui s'eteint et
	 * la nouvelle qui entre.
	 *
	 * @param bForceRestart  Repart du debut meme si la playlist est deja en cours.
	 */
	UFUNCTION(BlueprintCallable, Category = "Musique")
	void PlayPlaylist(UMusicPlaylistDataAsset* Playlist, bool bForceRestart = false);

	/** @param FadeOutDuration  Negatif = utiliser le fondu declare par la playlist. */
	UFUNCTION(BlueprintCallable, Category = "Musique")
	void StopMusic(float FadeOutDuration = -1.f);

	/** Passe au morceau suivant, dans l'ordre courant (aleatoire ou non). */
	UFUNCTION(BlueprintCallable, Category = "Musique")
	void SkipToNextTrack();

	/**
	 * Bascule aleatoire / ordre de la liste.
	 *
	 * Ne coupe jamais le morceau en cours : seul l'enchainement change. Le curseur
	 * se recale sur le morceau qui joue, donc en repassant en mode ordonne la
	 * lecture reprend juste apres lui au lieu de sauter au debut de la liste.
	 */
	UFUNCTION(BlueprintCallable, Category = "Musique")
	void SetShuffleEnabled(bool bEnabled);

	UFUNCTION(BlueprintPure, Category = "Musique")
	bool IsShuffleEnabled() const { return bShuffleEnabled; }

	/**
	 * Ce que devient le morceau en cours au prochain changement de scene.
	 *
	 * La consigne vit ICI et pas sur l'acteur qui l'a posee : l'acteur meurt avec
	 * sa map, alors que le changement de scene se produit justement apres. Une
	 * seule map -- le menu -- suffit donc a la declarer pour tout le jeu.
	 */
	UFUNCTION(BlueprintCallable, Category = "Musique")
	void SetLevelChangeMode(EMusicLevelChangeMode Mode) { LevelChangeMode = Mode; }

	UFUNCTION(BlueprintPure, Category = "Musique")
	EMusicLevelChangeMode GetLevelChangeMode() const { return LevelChangeMode; }

	/**
	 * Attenuation propre au LECTEUR, 0 a 1. Ce n'est PAS le reglage du joueur.
	 *
	 * Le volume choisi dans les reglages passe par la SoundClass de musique (voir
	 * USurvivalAudioSettings), pas par ici : deux chemins vers le meme volume
	 * finiraient par se multiplier sans que personne ne s'en souvienne. Celui-ci
	 * reste disponible pour ce qui est de l'ordre du gameplay -- baisser la musique
	 * pendant un dialogue, par exemple.
	 */
	UFUNCTION(BlueprintCallable, Category = "Musique")
	void SetMusicVolume(float NewVolume);

	UFUNCTION(BlueprintPure, Category = "Musique")
	float GetMusicVolume() const { return MusicVolume; }

	UFUNCTION(BlueprintPure, Category = "Musique")
	bool IsPlaying() const { return bIsPlaying; }

	UFUNCTION(BlueprintPure, Category = "Musique")
	USoundBase* GetCurrentTrack() const;

	UFUNCTION(BlueprintPure, Category = "Musique")
	UMusicPlaylistDataAsset* GetCurrentPlaylist() const { return CurrentPlaylist; }

	/** Diffuse a chaque changement de morceau, nullptr a l'arret. */
	UPROPERTY(BlueprintAssignable, Category = "Musique")
	FOnMusicTrackChanged OnMusicTrackChanged;

private:
	/** Fin NATURELLE d'un morceau uniquement -- voir FadeOutAndRelease. */
	UFUNCTION()
	void HandleTrackFinished();

	/** Applique LevelChangeMode a chaque map chargee. */
	void HandlePostLoadMap(UWorld* LoadedWorld);

	/** (Re)construit l'ordre de lecture du cycle a venir. */
	void BuildPlayOrder();

	void AdvanceAndPlay(float FadeInDuration);
	void PlayTrackAtCursor(float FadeInDuration);
	UAudioComponent* CreateMusicComponent(USoundBase* Sound);

	/**
	 * Detache puis eteint un composant : apres cet appel, plus rien de ce qu'il
	 * diffuse ne revient vers nous.
	 */
	void FadeOutAndRelease(TObjectPtr<UAudioComponent>& Component, float FadeOutDuration);

	void ScheduleGap(float DelaySeconds);
	void CancelPendingGap();
	float GetEffectiveVolume() const;

	UPROPERTY()
	TObjectPtr<UMusicPlaylistDataAsset> CurrentPlaylist;

	/**
	 * UPROPERTY obligatoire : ces composants n'ont ni monde ni acteur proprietaire,
	 * cette reference est la SEULE qui les tienne hors de portee du ramasse-miettes.
	 */
	UPROPERTY()
	TObjectPtr<UAudioComponent> MusicComponent;

	/** La voie de sortie, le temps d'un croisement. Une seule a la fois. */
	UPROPERTY()
	TObjectPtr<UAudioComponent> FadingOutComponent;

	/**
	 * Index dans Tracks, deja melangee si l'aleatoire est actif.
	 *
	 * Un ordre pre-melange plutot qu'un tirage a chaque morceau : le tirage
	 * independant rejoue le meme titre deux fois de suite une fois sur N, et sur
	 * une playlist de cinq morceaux ca s'entend tout de suite.
	 */
	TArray<int32> PlayOrder;

	int32 OrderCursor = INDEX_NONE;
	int32 LastPlayedTrackIndex = INDEX_NONE;

	bool bShuffleEnabled = true;
	bool bIsPlaying = false;
	float MusicVolume = 1.f;

	EMusicLevelChangeMode LevelChangeMode = EMusicLevelChangeMode::Continue;

	/** Abonnement au chargement de map, retire a la fermeture du jeu. */
	FDelegateHandle PostLoadMapHandle;

	/**
	 * Le silence entre deux morceaux passe par le ticker de Core, pas par un
	 * FTimerHandle : un timer appartient au monde, donc il disparaîtrait au
	 * chargement de la map suivante -- et il ne tourne pas en pause. Le ticker de
	 * Core ignore les deux.
	 */
	FTSTicker::FDelegateHandle GapTickerHandle;
};
