#ifndef EAX_EFFECT_INCLUDED
#define EAX_EFFECT_INCLUDED


#include <cassert>
#include <memory>

#include "alnumeric.h"
#include "AL/al.h"
#include "core/effects/base.h"
#include "call.h"

struct EaxEffectErrorMessages
{
    static constexpr auto unknown_property_id() noexcept { return "Unknown property id."; }
    static constexpr auto unknown_version() noexcept { return "Unknown version."; }
}; // EaxEffectErrorMessages

/* TODO: Use std::variant (C++17). */
enum class EaxEffectType {
    None, Reverb, Chorus, Autowah, Compressor, Distortion, Echo, Equalizer, Flanger,
    FrequencyShifter, Modulator, PitchShifter, VocalMorpher
};
struct EaxEffectProps {
    EaxEffectType mType;
    union {
        EAXREVERBPROPERTIES mReverb;
        EAXCHORUSPROPERTIES mChorus;
        EAXAUTOWAHPROPERTIES mAutowah;
        EAXAGCCOMPRESSORPROPERTIES mCompressor;
        EAXDISTORTIONPROPERTIES mDistortion;
        EAXECHOPROPERTIES mEcho;
        EAXEQUALIZERPROPERTIES mEqualizer;
        EAXFLANGERPROPERTIES mFlanger;
        EAXFREQUENCYSHIFTERPROPERTIES mFrequencyShifter;
        EAXRINGMODULATORPROPERTIES mModulator;
        EAXPITCHSHIFTERPROPERTIES mPitchShifter;
        EAXVOCALMORPHERPROPERTIES mVocalMorpher;
    };
};

class EaxEffect {
public:
    EaxEffect(ALenum type, int eax_version) noexcept
        : al_effect_type_{type}, version_{eax_version}
    { }
    virtual ~EaxEffect() = default;

    const ALenum al_effect_type_;
    EffectProps al_effect_props_{};

    using Props1 = EAX_REVERBPROPERTIES;
    using Props2 = EAX20LISTENERPROPERTIES;
    using Props3 = EAXREVERBPROPERTIES;
    using Props4 = EaxEffectProps;

    struct State1 {
        Props1 i; // Immediate.
        Props1 d; // Deferred.
    };

    struct State2 {
        Props2 i; // Immediate.
        Props2 d; // Deferred.
    };

    struct State3 {
        Props3 i; // Immediate.
        Props3 d; // Deferred.
    };

    struct State4 {
        Props4 i; // Immediate.
        Props4 d; // Deferred.
    };

    int version_;
    bool changed_{};
    Props4 props_{};
    State1 state1_{};
    State2 state2_{};
    State3 state3_{};
    State4 state4_{};
    State4 state5_{};

    virtual void dispatch(const EaxCall& call) = 0;

    // Returns "true" if any immediated property was changed.
    /*[[nodiscard]]*/ virtual bool commit() = 0;
}; // EaxEffect

// Base class for EAX4+ effects.
template<typename TException>
class EaxEffect4 : public EaxEffect
{
public:
    EaxEffect4(ALenum type, int eax_version)
        : EaxEffect{type, clamp(eax_version, 4, 5)}
    { }

    void initialize()
    {
        set_defaults();
        set_efx_defaults();
    }

    void dispatch(const EaxCall& call) override
    {
        call.is_get() ? get(call) : set(call);
        version_ = call.get_version();
    }

    bool commit() final
    {
        switch (version_)
        {
            case 4: return commit_state(state4_);
            case 5: return commit_state(state5_);
            default: fail_unknown_version();
        }
    }

protected:
    using Exception = TException;

    template<typename TValidator, typename TProperty>
    static void defer(const EaxCall& call, TProperty& property)
    {
        const auto& value = call.get_value<Exception, const TProperty>();
        TValidator{}(value);
        property = value;
    }

    virtual void set_defaults(Props4& props) = 0;
    virtual void set_efx_defaults() = 0;

    virtual void get(const EaxCall& call, const Props4& props) = 0;
    virtual void set(const EaxCall& call, Props4& props) = 0;

    virtual bool commit_props(const Props4& props) = 0;

    [[noreturn]] static void fail(const char* message)
    {
        throw Exception{message};
    }

    [[noreturn]] static void fail_unknown_property_id()
    {
        fail(EaxEffectErrorMessages::unknown_property_id());
    }

    [[noreturn]] static void fail_unknown_version()
    {
        fail(EaxEffectErrorMessages::unknown_version());
    }

private:
    void set_defaults()
    {
        set_defaults(props_);
        state4_.i = props_;
        state4_.d = props_;
        state5_.i = props_;
        state5_.d = props_;
    }

    void get(const EaxCall& call)
    {
        switch (call.get_version())
        {
            case 4: get(call, state4_.i); break;
            case 5: get(call, state5_.i); break;
            default: fail_unknown_version();
        }
    }

    void set(const EaxCall& call)
    {
        switch (call.get_version())
        {
            case 4: set(call, state4_.d); break;
            case 5: set(call, state5_.d); break;
            default: fail_unknown_version();
        }
    }

    bool commit_state(State4& state)
    {
        const auto props = props_;
        state.i = state.d;
        props_ = state.d;
        return commit_props(props);
    }
}; // EaxEffect4

using EaxEffectUPtr = std::unique_ptr<EaxEffect>;

// Creates EAX4+ effect.
template<typename TEffect>
EaxEffectUPtr eax_create_eax4_effect(int eax_version)
{
    auto effect = std::make_unique<TEffect>(eax_version);
    effect->initialize();
    return effect;
}

EaxEffectUPtr eax_create_eax_null_effect(int eax_version);
EaxEffectUPtr eax_create_eax_chorus_effect(int eax_version);
EaxEffectUPtr eax_create_eax_distortion_effect(int eax_version);
EaxEffectUPtr eax_create_eax_echo_effect(int eax_version);
EaxEffectUPtr eax_create_eax_flanger_effect(int eax_version);
EaxEffectUPtr eax_create_eax_frequency_shifter_effect(int eax_version);
EaxEffectUPtr eax_create_eax_vocal_morpher_effect(int eax_version);
EaxEffectUPtr eax_create_eax_pitch_shifter_effect(int eax_version);
EaxEffectUPtr eax_create_eax_ring_modulator_effect(int eax_version);
EaxEffectUPtr eax_create_eax_auto_wah_effect(int eax_version);
EaxEffectUPtr eax_create_eax_compressor_effect(int eax_version);
EaxEffectUPtr eax_create_eax_equalizer_effect(int eax_version);
EaxEffectUPtr eax_create_eax_reverb_effect(int eax_version);

#endif // !EAX_EFFECT_INCLUDED