// Copyright (c), Firelight Technologies Pty, Ltd. 2012-2025.

#pragma once

#include "Containers/Map.h"
#include "Runtime/Launch/Resources/Version.h"
#include "Sound/SoundAttenuation.h"
#include "AudioDevice.h"
#include "FMODStudioModule.h"
#include "FMODUtils.h"
#include "FMODAudioComponent.generated.h"

// Event property
UENUM()
namespace EFMODEventProperty
{
enum Type
{
    /* Priority to set on low-level channels created by this event instance (-1 to 256). */
    ChannelPriority,
    /** Schedule delay to synchronized playback for multiple tracks in DSP clocks, or -1 for default. */
    ScheduleDelay,
    /** Schedule look-ahead on the timeline in DSP clocks, or -1 for default. */
    ScheduleLookahead,
    /** Override the event's 3D minimum distance, or -1 for default. */
    MinimumDistance,
    /** Override the event's 3D maximum distance, or -1 for default. */
    MaximumDistance,
    /** Number of options */
    Count
};
}


/** Used to store callback info from FMOD thread to our event */
struct FTimelineMarkerProperties
{
    FString Name;
    int32 Position;
    FTimelineMarkerProperties()
        : Position(0)
    {}
};

/** Used to store callback info from FMOD thread to our event */
struct FTimelineBeatProperties
{
    int32 Bar;
    int32 Beat;
    int32 Position;
    float Tempo;
    int32 TimeSignatureUpper;
    int32 TimeSignatureLower;
    FTimelineBeatProperties()
        : Bar(0)
        , Beat(0)
        , Position(0)
        , Tempo(0.0f)
        , TimeSignatureUpper(0)
        , TimeSignatureLower(0)
    {}
};

USTRUCT(BlueprintType)
struct FFMODAttenuationDetails
{
    GENERATED_USTRUCT_BODY()
    /** Should we use Attenuation set in Studio or be able to modify in Editor. */
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "FMOD|Attenuation")
    uint32 bOverrideAttenuation : 1;
    /** Override the event's 3D minimum distance. */
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "FMOD|Attenuation",
        meta = (ClampMin = "0.0", UIMin = "0.0", EditCondition = "bOverrideAttenuation"))
    float MinimumDistance;
    /** Override the event's 3D maximum distance. */
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "FMOD|Attenuation",
        meta = (ClampMin = "0.0", UIMin = "0.0", EditCondition = "bOverrideAttenuation"))
    float MaximumDistance;

    FFMODAttenuationDetails()
        : bOverrideAttenuation(false)
        , MinimumDistance(1.0f)
        , MaximumDistance(10.0f)
    {}
};

USTRUCT(BlueprintType)
struct FFMODOcclusionDetails
{
    GENERATED_USTRUCT_BODY()
    /** Enable Occlusion Settings. */
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "FMOD|Occlusion")
    bool bEnableOcclusion;
    /* Which trace channel to use for audio occlusion checks. */
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "FMOD|Occlusion", meta = (EditCondition = "bEnableOcclusion"))
    TEnumAsByte<enum ECollisionChannel> OcclusionTraceChannel;
    /** Whether or not to enable complex geometry occlusion checks. */
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="FMOD|Occlusion", meta=(EditCondition = "bEnableOcclusion"))
    bool bUseComplexCollisionForOcclusion;

    FFMODOcclusionDetails()
        : bEnableOcclusion(false)
        , OcclusionTraceChannel(ECC_Visibility)
        , bUseComplexCollisionForOcclusion(false)
    {}
};

/** called when an event stops, either because it played to completion or because a Stop() call turned it off early */
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnEventStopped);
/** called when a sound stops */
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnSoundStopped);
/** called when we reach a named marker on the timeline */
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnTimelineMarker, FString, Name, int32, Position);
/** called when we reach a beat on the timeline */
DECLARE_DYNAMIC_MULTICAST_DELEGATE_SixParams(
    FOnTimelineBeat, int32, Bar, int32, Beat, int32, Position, float, Tempo, int32, TimeSignatureUpper, int32, TimeSignatureLower);

namespace FMOD
{
class DSP;
class Sound;
namespace Studio
{
class EventDescription;
class EventInstance;
}
}

struct FMOD_STUDIO_TIMELINE_MARKER_PROPERTIES;
struct FMOD_STUDIO_TIMELINE_BEAT_PROPERTIES;

/**
 * Plays FMOD Studio events.
 */
UCLASS(Blueprintable, ClassGroup = (Audio, Common), hidecategories = (Object, ActorComponent, Physics, Rendering, Mobility, LOD),
    ShowCategories = Trigger, meta = (BlueprintSpawnableComponent))
class FMODSTUDIO_API UFMODAudioComponent : public USceneComponent
{
    GENERATED_UCLASS_BODY()

    friend struct FFMODEventControlExecutionToken;
    friend struct FPlayingToken;
    friend FMOD_RESULT F_CALL UFMODAudioComponent_EventCallback(FMOD_STUDIO_EVENT_CALLBACK_TYPE type, FMOD_STUDIO_EVENTINSTANCE *event, void *parameters);

public:
    /** The event asset to use for this sound. */
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = FMODAudio)
    UFMODEvent* Event;

    /** Event parameter cache. */
    UPROPERTY(EditAnywhere, BlueprintReadWrite, SimpleDisplay, Category = FMODAudio)
    TMap<FName, float> ParameterCache;

    /** Sound name used for programmer sound.  Will look up the name in any loaded audio table. */
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = FMODAudio)
    FString ProgrammerSoundName;

    /** Enable timeline callbacks for this sound, so that OnTimelineMarker and OnTimelineBeat can be used. */
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = FMODAudio)
    uint32 bEnableTimelineCallbacks : 1;

    /** Auto destroy this component on completion. */
    UPROPERTY()
    uint32 bAutoDestroy : 1;

    /** Stop sound when owner is destroyed. */
    UPROPERTY()
    uint32 bStopWhenOwnerDestroyed : 1;

    /** Whether we apply gain and low-pass based on audio zones. */
    uint32 bApplyAmbientVolumes : 1;

    /** Whether we apply gain and low-pass based on occlusion onto a parameter. */
    uint32 bApplyOcclusionParameter:1;

    /** Called when an event stops, either because it played to completion or because a Stop() call turned it off early. */
    UPROPERTY(BlueprintAssignable)
    FOnEventStopped OnEventStopped;

    /** Called when a sound stops. */
    UPROPERTY(BlueprintAssignable)
    FOnSoundStopped OnSoundStopped;

    /** Called when we reach a named marker (if bEnableTimelineCallbacks is true). */
    UPROPERTY(BlueprintAssignable)
    FOnTimelineMarker OnTimelineMarker;

    /** Called when we reach a beat of a tempo (if bEnableTimelineCallbacks is true). */
    UPROPERTY(BlueprintAssignable)
    FOnTimelineBeat OnTimelineBeat;

    /** New Event to be used by the FMODAudioComponent. */
    UFUNCTION(BlueprintCallable, Category = "Audio|FMOD|Components")
    void SetEvent(UFMODEvent *NewEvent);

    /** Start a sound playing on an audio component. */
    UFUNCTION(BlueprintCallable, Category = "Audio|FMOD|Components")
    void Play();

    /** Stop an audio component playing its sound cue, issue any delegates if needed. */
    UFUNCTION(BlueprintCallable, Category = "Audio|FMOD|Components")
    void Stop();

    UFUNCTION(BlueprintCallable, Category = "Audio|FMOD|Components")
    void Release();

    /**  Allow an event to continue past a sustain point. */
    UFUNCTION(BlueprintCallable, Category = "Audio|FMOD|Components")
    void KeyOff();

    /** Return true if this component is currently playing an event. */
    UFUNCTION(BlueprintCallable, Category = "Audio|FMOD|Components")
    bool IsPlaying();

    /** Sets the volume level. */
    UFUNCTION(BlueprintCallable, Category = "Audio|FMOD|Components")
    void SetVolume(float volume);

    /** Sets the pitch multiplier. */
    UFUNCTION(BlueprintCallable, Category = "Audio|FMOD|Components")
    void SetPitch(float pitch);

    /** Pause/Unpause an audio component. */
    UFUNCTION(BlueprintCallable, Category = "Audio|FMOD|Components")
    void SetPaused(bool paused);

    /** Get the paused state of the audio component. Returns false if internal getPaused query fails. */
    UFUNCTION(BlueprintCallable, Category = "Audio|FMOD|Components")
    bool GetPaused();

    /** Set a parameter of the Event. */
    UFUNCTION(BlueprintCallable, Category = "Audio|FMOD|Components")
    void SetParameter(FName Name, float Value);

    /** Will be deprecated in FMOD 2.01, use `GetParameterValue(FName, float, float)` instead.
     * Get parameter value from the Event.
    */
    UFUNCTION(BlueprintCallable, Category = "Audio|FMOD|Components")
    float GetParameter(FName Name);

    /** Get parameter value from the Event.
     * @param Name - Name of parameter
     * @param UserValue - Parameter value as set from the public API.
     * @param FinalValue - Final combined parameter value.
    */
    UFUNCTION(BlueprintCallable, Category = "Audio|FMOD|Components")
    void GetParameterValue(FName Name, float &UserValue, float &FinalValue);

    /** Set a property of the Event. */
    UFUNCTION(BlueprintCallable, Category = "Audio|FMOD|Components")
    void SetProperty(EFMODEventProperty::Type Property, float Value);

    /** Get a property of the Event. */
    UFUNCTION(BlueprintCallable, Category = "Audio|FMOD|Components")
    float GetProperty(EFMODEventProperty::Type Property);

    /** Get the event length in milliseconds. */
    UFUNCTION(BlueprintCallable, Category = "Audio|FMOD|Components")
    int32 GetLength() const;

    /** Set the timeline position in milliseconds. */
    UFUNCTION(BlueprintCallable, Category = "Audio|FMOD|Components")
    void SetTimelinePosition(int32 Time);

    /** Get the timeline position in milliseconds. */
    UFUNCTION(BlueprintCallable, Category = "Audio|FMOD|Components")
    int32 GetTimelinePosition();

    /** Set the sound name to use for programmer sound.  Will look up the name in any loaded audio table. */
    UFUNCTION(BlueprintCallable, Category = "Audio|FMOD|Components")
    void SetProgrammerSoundName(FString Value);

    /** Set a programmer sound to use for this audio component.  Lifetime of sound must exceed that of the audio component. */
    void SetProgrammerSound(FMOD::Sound *Sound);

    /** FMOD Custom Attenuation Details. */
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = FMODAudio)
    struct FFMODAttenuationDetails AttenuationDetails;

    /** FMOD Custom Occlusion Details. */
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = FMODAudio)
    struct FFMODOcclusionDetails OcclusionDetails;

    /** Actual Studio instance handle. */
    FMOD::Studio::EventInstance *StudioInstance;

    // Begin UObject interface.
#if WITH_EDITOR
    virtual void PostEditChangeProperty(FPropertyChangedEvent &e) override;
#endif // WITH_EDITOR
    virtual void PostLoad() override;
    // End UObject interface.

    // Begin USceneComponent Interface
    virtual void Activate(bool bReset = false) override;
    virtual void Deactivate() override;
    /** Called when a component is registered, after Scene is set, but before CreateRenderState_Concurrent or OnCreatePhysicsState are called. */
    virtual void OnRegister() override;
    /** Called when a component is unregistered. Called after DestroyRenderState_Concurrent and OnDestroyPhysicsState are called. */
    virtual void OnUnregister() override;
    // End USceneComponent Interface

    // Begin ActorComponent interface.
    /** Overridable native event for when play begins for this actor. */
    virtual void BeginPlay() override;
    /** Overridable function called whenever this actor is being removed from a level. */
    virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
    /** Function called every frame on this ActorComponent. Only executes if the component is registered, and also PrimaryComponentTick.bCanEverTick must be set to true. */
    virtual void TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
    // End ActorComponent interface.

protected:
    // Begin UObject interface.
    virtual void OnUpdateTransform(EUpdateTransformFlags UpdateTransformFlags, ETeleportType Teleport = ETeleportType::None) override;
    // End UObject interface.

    // Begin USceneComponent Interface
    virtual FString GetDetailedInfoInternal() const override;
    // End USceneComponent Interface

private:
    bool bDefaultParameterValuesCached;

    enum PauseContext
    {
        Explicit,
        Implicit
    };

    /** Used for pausing from sequencer. */
    bool bImplicitlyPaused = false;

    /** Used for pausing from a direct call to pause. */
    bool bExplicitlyPaused = false;

    /** Stored properties to apply next time we create an instance. */
    float StoredProperties[EFMODEventProperty::Count];

    /** Internal play function which can play events in the editor. */
    void PlayInternal(EFMODSystemContext::Type Context, bool bReset = false);

    /** Pause the audio component from a sequencer call. */
    void PauseInternal(PauseContext Pauser);

    /** Resume the audio component from a sequencer call. */
    void ResumeInternal(PauseContext Pauser);

    /** Cache default event parameter values. */
    void CacheDefaultParameterValues();

    /** Check that only player driven parameters are added to the cache. */
    void UpdateCachedParameterValues();

    /** Update gain and low-pass based on interior volumes. */
    void UpdateInteriorVolumes();

    /** Update attenuation if we have it set. */
    void UpdateAttenuation();

    /** Apply Volume and LPF into event. */
    void ApplyVolumeLPF();

    /** Timeline Marker callback. */
    void EventCallbackAddMarker(struct FMOD_STUDIO_TIMELINE_MARKER_PROPERTIES *props);

    /** Timeline Beat callback. */
    void EventCallbackAddBeat(struct FMOD_STUDIO_TIMELINE_BEAT_PROPERTIES *props);

    /** Programmer Sound Create callback. */
    void EventCallbackCreateProgrammerSound(struct FMOD_STUDIO_PROGRAMMER_SOUND_PROPERTIES *props);

    /** Programmer Sound Destroy callback. */
    void EventCallbackDestroyProgrammerSound(struct FMOD_STUDIO_PROGRAMMER_SOUND_PROPERTIES *props);

    /** Called when the event has finished stopping. */
    void OnPlaybackCompleted();

    void EventCallbackSoundStopped();
    bool TriggerSoundStoppedDelegate;

#if WITH_EDITORONLY_DATA
    void UpdateSpriteTexture();
#endif

#if WITH_EDITOR
    /** Function assigned to the FMODStudioModule PreShutdown delegate to clean up before the Studio System is released. */
    void Shutdown();
#endif

    /** Release any cached parameters then the Studio Instance. */
    void ReleaseEventCache();

    /** Release the Studio Instance. */
    void ReleaseEventInstance();

    /** Check if a parameter is game controlled or automated to determine if it should be cached. */
    bool ShouldCacheParameter(const FMOD_STUDIO_PARAMETER_DESCRIPTION& ParameterDescription);

    /** Return a cached reference to the current IFMODStudioModule.*/
    IFMODStudioModule& GetStudioModule()
    {
        if (Module == nullptr)
        {
            Module = &IFMODStudioModule::Get();
        }
        return *Module;
    }
    /** Stored reference to the current IFMODStudioModule. */
    IFMODStudioModule* Module;

    // Settings for ambient volume effects.
    /** Timer used for volumes fading in and out. Used for automating volume and/or LPF with Ambient Zones. */
    double InteriorLastUpdateTime;
    /** Previous interior volume value. Used for automating volume and/or LPF with Ambient Zones. */
    float SourceInteriorVolume;
    /** Previous interior LPF value. Used for automating volume and/or LPF with Ambient Zones. */
    float SourceInteriorLPF;
    /** Current interior volume value. Used for automating volume and/or LPF with Ambient Zones. */
    float CurrentInteriorVolume;
    /** Current interior LPF value. Used for automating volume and/or LPF with Ambient Zones. */
    float CurrentInteriorLPF;
    /** Calculated Ambient volume level for that frame. Used for automating volume and/or LPF with Ambient Zones. */
    float AmbientVolume;
    /** Calculated Ambient LPF level for that frame. Used for automating volume and/or LPF with Ambient Zones. */
    float AmbientLPF;
    /** Previously set Volume value. Used for automating volume and/or LPF with Ambient Zones. */
    float LastVolume;
    /** Previously set LPF value. Used for automating volume and/or LPF with Ambient Zones. */
    float LastLPF;
    /** Was the object occluded in the previous frame. */
    bool wasOccluded;
    /** Stored ID of the Occlusion parameter of the Event (if applicable). */
    FMOD_STUDIO_PARAMETER_ID OcclusionID;
    /** Stored ID of the Volume parameter of the Event (if applicable). */
    FMOD_STUDIO_PARAMETER_ID AmbientVolumeID;
    /** Stored ID of the LPF parameter of the Event (if applicable). */
    FMOD_STUDIO_PARAMETER_ID AmbientLPFID;

    // Tempo and marker callbacks.
    /** A scope lock used specifically for callbacks. */
    FCriticalSection CallbackLock;
    /** Stores the Timeline Markers as they are triggered. */
    TArray<FTimelineMarkerProperties> CallbackMarkerQueue;
    /** Stores the Timeline Beats as they are triggered. */
    TArray<FTimelineBeatProperties> CallbackBeatQueue;

    /** Direct assignment of programmer sound from other C++ code. */
    FMOD::Sound *ProgrammerSound;
    bool NeedDestroyProgrammerSoundCallback;
    /** The length of the current Event in milliseconds. */
    int32 EventLength;

    /** To prevent restarting by delayed state restore from sequencer. */
    bool bPlayEnded;

    FVector Velocity;
    FVector LastLocation;
};