#ifndef ALC_CONTEXT_H
#define ALC_CONTEXT_H

#include <atomic>
#include <memory>
#include <mutex>
#include <stdint.h>
#include <utility>

#include "AL/al.h"
#include "AL/alc.h"
#include "AL/alext.h"

#include "al/listener.h"
#include "almalloc.h"
#include "alnumeric.h"
#include "atomic.h"
#include "core/context.h"
#include "intrusive_ptr.h"
#include "vector.h"

#ifdef ALSOFT_EAX
#include "al/eax/call.h"
#include "al/eax/fx_slot_index.h"
#include "al/eax/fx_slots.h"
#include "al/eax/utils.h"


using ContextDirtyFlagsValue = std::uint_least8_t;

struct ContextDirtyFlags
{
    using EaxIsBitFieldStruct = bool;

    ContextDirtyFlagsValue guidPrimaryFXSlotID : 1;
    ContextDirtyFlagsValue flDistanceFactor : 1;
    ContextDirtyFlagsValue flAirAbsorptionHF : 1;
    ContextDirtyFlagsValue flHFReference : 1;
    ContextDirtyFlagsValue flMacroFXFactor : 1;
}; // ContextDirtyFlags


struct EaxAlIsExtensionPresentResult
{
    ALboolean is_present;
    bool is_return;
}; // EaxAlIsExtensionPresentResult
#endif // ALSOFT_EAX

struct ALeffect;
struct ALeffectslot;
struct ALsource;

using uint = unsigned int;


struct SourceSubList {
    uint64_t FreeMask{~0_u64};
    ALsource *Sources{nullptr}; /* 64 */

    SourceSubList() noexcept = default;
    SourceSubList(const SourceSubList&) = delete;
    SourceSubList(SourceSubList&& rhs) noexcept : FreeMask{rhs.FreeMask}, Sources{rhs.Sources}
    { rhs.FreeMask = ~0_u64; rhs.Sources = nullptr; }
    ~SourceSubList();

    SourceSubList& operator=(const SourceSubList&) = delete;
    SourceSubList& operator=(SourceSubList&& rhs) noexcept
    { std::swap(FreeMask, rhs.FreeMask); std::swap(Sources, rhs.Sources); return *this; }
};

struct EffectSlotSubList {
    uint64_t FreeMask{~0_u64};
    ALeffectslot *EffectSlots{nullptr}; /* 64 */

    EffectSlotSubList() noexcept = default;
    EffectSlotSubList(const EffectSlotSubList&) = delete;
    EffectSlotSubList(EffectSlotSubList&& rhs) noexcept
      : FreeMask{rhs.FreeMask}, EffectSlots{rhs.EffectSlots}
    { rhs.FreeMask = ~0_u64; rhs.EffectSlots = nullptr; }
    ~EffectSlotSubList();

    EffectSlotSubList& operator=(const EffectSlotSubList&) = delete;
    EffectSlotSubList& operator=(EffectSlotSubList&& rhs) noexcept
    { std::swap(FreeMask, rhs.FreeMask); std::swap(EffectSlots, rhs.EffectSlots); return *this; }
};

struct ALCcontext : public al::intrusive_ref<ALCcontext>, ContextBase {
    const al::intrusive_ptr<ALCdevice> mALDevice;

    /* Wet buffers used by effect slots. */
    al::vector<WetBufferPtr> mWetBuffers;


    bool mPropsDirty{true};
    bool mDeferUpdates{false};

    std::mutex mPropLock;

    std::atomic<ALenum> mLastError{AL_NO_ERROR};

    DistanceModel mDistanceModel{DistanceModel::Default};
    bool mSourceDistanceModel{false};

    float mDopplerFactor{1.0f};
    float mDopplerVelocity{1.0f};
    float mSpeedOfSound{SpeedOfSoundMetersPerSec};
    float mAirAbsorptionGainHF{AirAbsorbGainHF};

    std::mutex mEventCbLock;
    ALEVENTPROCSOFT mEventCb{};
    void *mEventParam{nullptr};

    ALlistener mListener{};

    al::vector<SourceSubList> mSourceList;
    ALuint mNumSources{0};
    std::mutex mSourceLock;

    al::vector<EffectSlotSubList> mEffectSlotList;
    ALuint mNumEffectSlots{0u};
    std::mutex mEffectSlotLock;

    /* Default effect slot */
    std::unique_ptr<ALeffectslot> mDefaultSlot;

    const char *mExtensionList{nullptr};


    ALCcontext(al::intrusive_ptr<ALCdevice> device);
    ALCcontext(const ALCcontext&) = delete;
    ALCcontext& operator=(const ALCcontext&) = delete;
    ~ALCcontext();

    void init();
    /**
     * Removes the context from its device and removes it from being current on
     * the running thread or globally. Returns true if other contexts still
     * exist on the device.
     */
    bool deinit();

    /**
     * Defers/suspends updates for the given context's listener and sources.
     * This does *NOT* stop mixing, but rather prevents certain property
     * changes from taking effect. mPropLock must be held when called.
     */
    void deferUpdates() noexcept { mDeferUpdates = true; }

    /**
     * Resumes update processing after being deferred. mPropLock must be held
     * when called.
     */
    void processUpdates()
    {
        if(std::exchange(mDeferUpdates, false))
            applyAllUpdates();
    }

    /**
     * Applies all pending updates for the context, listener, effect slots, and
     * sources.
     */
    void applyAllUpdates();

#ifdef __USE_MINGW_ANSI_STDIO
    [[gnu::format(gnu_printf, 3, 4)]]
#else
    [[gnu::format(printf, 3, 4)]]
#endif
    void setError(ALenum errorCode, const char *msg, ...);

    /* Process-wide current context */
    static std::atomic<ALCcontext*> sGlobalContext;

private:
    /* Thread-local current context. */
    static thread_local ALCcontext *sLocalContext;

    /* Thread-local context handling. This handles attempting to release the
     * context which may have been left current when the thread is destroyed.
     */
    class ThreadCtx {
    public:
        ~ThreadCtx();
        void set(ALCcontext *ctx) const noexcept { sLocalContext = ctx; }
    };
    static thread_local ThreadCtx sThreadContext;

public:
    /* HACK: MinGW generates bad code when accessing an extern thread_local
     * object. Add a wrapper function for it that only accesses it where it's
     * defined.
     */
#ifdef __MINGW32__
    static ALCcontext *getThreadContext() noexcept;
    static void setThreadContext(ALCcontext *context) noexcept;
#else
    static ALCcontext *getThreadContext() noexcept { return sLocalContext; }
    static void setThreadContext(ALCcontext *context) noexcept { sThreadContext.set(context); }
#endif

    /* Default effect that applies to sources that don't have an effect on send 0. */
    static ALeffect sDefaultEffect;

    DEF_NEWDEL(ALCcontext)

#ifdef ALSOFT_EAX
public:
    bool has_eax() const noexcept { return eax_is_initialized_; }

    bool eax_is_capable() const noexcept;


    void eax_uninitialize() noexcept;

    int eax_get_version() const noexcept { return eax_version_; }

    ALenum eax_eax_set(
        const GUID* property_set_id,
        ALuint property_id,
        ALuint property_source_id,
        ALvoid* property_value,
        ALuint property_value_size);

    ALenum eax_eax_get(
        const GUID* property_set_id,
        ALuint property_id,
        ALuint property_source_id,
        ALvoid* property_value,
        ALuint property_value_size);


    void eax_update_filters();

    void eax_commit_and_update_sources();


    void eax_set_last_error() noexcept;


    EaxFxSlotIndex eax_get_primary_fx_slot_index() const noexcept
    { return eax_primary_fx_slot_index_; }

    const ALeffectslot& eax_get_fx_slot(EaxFxSlotIndexValue fx_slot_index) const
    { return eax_fx_slots_.get(fx_slot_index); }
    ALeffectslot& eax_get_fx_slot(EaxFxSlotIndexValue fx_slot_index)
    { return eax_fx_slots_.get(fx_slot_index); }

    void eax_commit_fx_slots()
    { eax_fx_slots_.commit(); }

private:
    struct Eax
    {
        EAX50CONTEXTPROPERTIES context{};
    }; // Eax


    bool eax_is_initialized_{};
    bool eax_is_tried_{};
    bool eax_are_legacy_fx_slots_unlocked_{};

    long eax_last_error_{};
    unsigned long eax_speaker_config_{};

    EaxFxSlotIndex eax_primary_fx_slot_index_{};
    EaxFxSlots eax_fx_slots_{};

    int eax_version_{};
    Eax eax_{};
    Eax eax_d_{};
    EAXSESSIONPROPERTIES eax_session_{};

    ContextDirtyFlags eax_context_dirty_flags_{};

    std::string eax_extension_list_{};


    [[noreturn]]
    static void eax_fail(
        const char* message);


    void eax_initialize_extensions();

    void eax_initialize(const EaxCall& call);


    bool eax_has_no_default_effect_slot() const noexcept;

    void eax_ensure_no_default_effect_slot() const;

    bool eax_has_enough_aux_sends() const noexcept;

    void eax_ensure_enough_aux_sends() const;

    void eax_ensure_compatibility();


    unsigned long eax_detect_speaker_configuration() const;
    void eax_update_speaker_configuration();


    void eax_set_last_error_defaults() noexcept;

    void eax_set_session_defaults() noexcept;

    void eax_set_context_defaults() noexcept;

    void eax_set_defaults() noexcept;

    void eax_initialize_sources();


    void eax_unlock_legacy_fx_slots(const EaxCall& call) noexcept;


    void eax_dispatch_fx_slot(const EaxCall& call);

    void eax_dispatch_source(const EaxCall& call);


    void eax_get_primary_fx_slot_id(const EaxCall& call);

    void eax_get_distance_factor(const EaxCall& call);

    void eax_get_air_absorption_hf(const EaxCall& call);

    void eax_get_hf_reference(const EaxCall& call);

    void eax_get_last_error(const EaxCall& call);

    void eax_get_speaker_config(const EaxCall& call);

    void eax_get_session(const EaxCall& call);

    void eax_get_macro_fx_factor(const EaxCall& call);

    void eax_get_context_all(const EaxCall& call);

    void eax_get(const EaxCall& call);


    void eax_set_primary_fx_slot_id();

    void eax_set_distance_factor();

    void eax_set_air_absorbtion_hf();

    void eax_set_hf_reference();

    void eax_set_macro_fx_factor();

    void eax_set_context();

    void eax_initialize_fx_slots(const EaxCall& call);


    void eax_update_sources();


    void eax_validate_primary_fx_slot_id(
        const GUID& primary_fx_slot_id);

    void eax_validate_distance_factor(
        float distance_factor);

    void eax_validate_air_absorption_hf(
        float air_absorption_hf);

    void eax_validate_hf_reference(
        float hf_reference);

    void eax_validate_speaker_config(
        unsigned long speaker_config);

    void eax_validate_session_eax_version(
        unsigned long eax_version);

    void eax_validate_session_max_active_sends(
        unsigned long max_active_sends);

    void eax_validate_session(
        const EAXSESSIONPROPERTIES& eax_session);

    void eax_validate_macro_fx_factor(
        float macro_fx_factor);

    void eax_validate_context_all(
        const EAX40CONTEXTPROPERTIES& context_all);

    void eax_validate_context_all(
        const EAX50CONTEXTPROPERTIES& context_all);


    void eax_defer_primary_fx_slot_id(
        const GUID& primary_fx_slot_id);

    void eax_defer_distance_factor(
        float distance_factor);

    void eax_defer_air_absorption_hf(
        float air_absorption_hf);

    void eax_defer_hf_reference(
        float hf_reference);

    void eax_defer_macro_fx_factor(
        float macro_fx_factor);

    void eax_defer_context_all(
        const EAX40CONTEXTPROPERTIES& context_all);

    void eax_defer_context_all(
        const EAX50CONTEXTPROPERTIES& context_all);


    void eax_defer_context_all(const EaxCall& call);

    void eax_defer_primary_fx_slot_id(const EaxCall& call);

    void eax_defer_distance_factor(const EaxCall& call);

    void eax_defer_air_absorption_hf(const EaxCall& call);

    void eax_defer_hf_reference(const EaxCall& call);

    void eax_set_session(const EaxCall& call);

    void eax_defer_macro_fx_factor(const EaxCall& call);

    void eax_set(const EaxCall& call);

    void eax_apply_deferred();
#endif // ALSOFT_EAX
};

#define SETERR_RETURN(ctx, err, retval, ...) do {                             \
    (ctx)->setError((err), __VA_ARGS__);                                      \
    return retval;                                                            \
} while(0)


using ContextRef = al::intrusive_ptr<ALCcontext>;

ContextRef GetContextRef(void);

void UpdateContextProps(ALCcontext *context);


extern bool TrapALError;


#ifdef ALSOFT_EAX
ALenum AL_APIENTRY EAXSet(
    const GUID* property_set_id,
    ALuint property_id,
    ALuint property_source_id,
    ALvoid* property_value,
    ALuint property_value_size) noexcept;

ALenum AL_APIENTRY EAXGet(
    const GUID* property_set_id,
    ALuint property_id,
    ALuint property_source_id,
    ALvoid* property_value,
    ALuint property_value_size) noexcept;
#endif // ALSOFT_EAX

#endif /* ALC_CONTEXT_H */
