#include "config.h"

#include "AL/al.h"
#include "AL/efx.h"

#include "alc/effects/base.h"
#include "effects.h"

#if ALSOFT_EAX
#include "alnumeric.h"

#include "al/eax_exception.h"
#include "al/eax_utils.h"
#endif // ALSOFT_EAX


namespace {

void Distortion_setParami(EffectProps*, ALenum param, int)
{ throw effect_exception{AL_INVALID_ENUM, "Invalid distortion integer property 0x%04x", param}; }
void Distortion_setParamiv(EffectProps*, ALenum param, const int*)
{
    throw effect_exception{AL_INVALID_ENUM, "Invalid distortion integer-vector property 0x%04x",
        param};
}
void Distortion_setParamf(EffectProps *props, ALenum param, float val)
{
    switch(param)
    {
    case AL_DISTORTION_EDGE:
        if(!(val >= AL_DISTORTION_MIN_EDGE && val <= AL_DISTORTION_MAX_EDGE))
            throw effect_exception{AL_INVALID_VALUE, "Distortion edge out of range"};
        props->Distortion.Edge = val;
        break;

    case AL_DISTORTION_GAIN:
        if(!(val >= AL_DISTORTION_MIN_GAIN && val <= AL_DISTORTION_MAX_GAIN))
            throw effect_exception{AL_INVALID_VALUE, "Distortion gain out of range"};
        props->Distortion.Gain = val;
        break;

    case AL_DISTORTION_LOWPASS_CUTOFF:
        if(!(val >= AL_DISTORTION_MIN_LOWPASS_CUTOFF && val <= AL_DISTORTION_MAX_LOWPASS_CUTOFF))
            throw effect_exception{AL_INVALID_VALUE, "Distortion low-pass cutoff out of range"};
        props->Distortion.LowpassCutoff = val;
        break;

    case AL_DISTORTION_EQCENTER:
        if(!(val >= AL_DISTORTION_MIN_EQCENTER && val <= AL_DISTORTION_MAX_EQCENTER))
            throw effect_exception{AL_INVALID_VALUE, "Distortion EQ center out of range"};
        props->Distortion.EQCenter = val;
        break;

    case AL_DISTORTION_EQBANDWIDTH:
        if(!(val >= AL_DISTORTION_MIN_EQBANDWIDTH && val <= AL_DISTORTION_MAX_EQBANDWIDTH))
            throw effect_exception{AL_INVALID_VALUE, "Distortion EQ bandwidth out of range"};
        props->Distortion.EQBandwidth = val;
        break;

    default:
        throw effect_exception{AL_INVALID_ENUM, "Invalid distortion float property 0x%04x", param};
    }
}
void Distortion_setParamfv(EffectProps *props, ALenum param, const float *vals)
{ Distortion_setParamf(props, param, vals[0]); }

void Distortion_getParami(const EffectProps*, ALenum param, int*)
{ throw effect_exception{AL_INVALID_ENUM, "Invalid distortion integer property 0x%04x", param}; }
void Distortion_getParamiv(const EffectProps*, ALenum param, int*)
{
    throw effect_exception{AL_INVALID_ENUM, "Invalid distortion integer-vector property 0x%04x",
        param};
}
void Distortion_getParamf(const EffectProps *props, ALenum param, float *val)
{
    switch(param)
    {
    case AL_DISTORTION_EDGE:
        *val = props->Distortion.Edge;
        break;

    case AL_DISTORTION_GAIN:
        *val = props->Distortion.Gain;
        break;

    case AL_DISTORTION_LOWPASS_CUTOFF:
        *val = props->Distortion.LowpassCutoff;
        break;

    case AL_DISTORTION_EQCENTER:
        *val = props->Distortion.EQCenter;
        break;

    case AL_DISTORTION_EQBANDWIDTH:
        *val = props->Distortion.EQBandwidth;
        break;

    default:
        throw effect_exception{AL_INVALID_ENUM, "Invalid distortion float property 0x%04x", param};
    }
}
void Distortion_getParamfv(const EffectProps *props, ALenum param, float *vals)
{ Distortion_getParamf(props, param, vals); }

EffectProps genDefaultProps() noexcept
{
    EffectProps props{};
    props.Distortion.Edge = AL_DISTORTION_DEFAULT_EDGE;
    props.Distortion.Gain = AL_DISTORTION_DEFAULT_GAIN;
    props.Distortion.LowpassCutoff = AL_DISTORTION_DEFAULT_LOWPASS_CUTOFF;
    props.Distortion.EQCenter = AL_DISTORTION_DEFAULT_EQCENTER;
    props.Distortion.EQBandwidth = AL_DISTORTION_DEFAULT_EQBANDWIDTH;
    return props;
}

} // namespace

DEFINE_ALEFFECT_VTABLE(Distortion);

const EffectProps DistortionEffectProps{genDefaultProps()};

#if ALSOFT_EAX
namespace
{


using EaxDistortionEffectDirtyFlagsValue = std::uint_least8_t;

struct EaxDistortionEffectDirtyFlags
{
    using EaxIsBitFieldStruct = bool;

    EaxDistortionEffectDirtyFlagsValue flEdge : 1;
    EaxDistortionEffectDirtyFlagsValue lGain : 1;
    EaxDistortionEffectDirtyFlagsValue flLowPassCutOff : 1;
    EaxDistortionEffectDirtyFlagsValue flEQCenter : 1;
    EaxDistortionEffectDirtyFlagsValue flEQBandwidth : 1;
}; // EaxDistortionEffectDirtyFlags


class EaxDistortionEffect final :
    public EaxEffect
{
public:
    EaxDistortionEffect(
        EffectProps& al_effect_props);


    // [[nodiscard]]
    bool dispatch(
        const EaxEaxCall& eax_call) override;


private:
    EffectProps& al_effect_props_;

    EAXDISTORTIONPROPERTIES eax_{};
    EAXDISTORTIONPROPERTIES eax_d_{};
    EaxDistortionEffectDirtyFlags eax_dirty_flags_{};


    void set_eax_defaults();


    void set_efx_edge();

    void set_efx_gain();

    void set_efx_lowpass_cutoff();

    void set_efx_eq_center();

    void set_efx_eq_bandwidth();

    void set_efx_defaults();


    // [[nodiscard]]
    bool get(
        const EaxEaxCall& eax_call);


    void validate_edge(
        float flEdge);

    void validate_gain(
        long lGain);

    void validate_lowpass_cutoff(
        float flLowPassCutOff);

    void validate_eq_center(
        float flEQCenter);

    void validate_eq_bandwidth(
        float flEQBandwidth);

    void validate_all(
        const EAXDISTORTIONPROPERTIES& eax_all);


    void defer_edge(
        float flEdge);

    void defer_gain(
        long lGain);

    void defer_low_pass_cutoff(
        float flLowPassCutOff);

    void defer_eq_center(
        float flEQCenter);

    void defer_eq_bandwidth(
        float flEQBandwidth);

    void defer_all(
        const EAXDISTORTIONPROPERTIES& eax_all);


    void defer_edge(
        const EaxEaxCall& eax_call);

    void defer_gain(
        const EaxEaxCall& eax_call);

    void defer_low_pass_cutoff(
        const EaxEaxCall& eax_call);

    void defer_eq_center(
        const EaxEaxCall& eax_call);

    void defer_eq_bandwidth(
        const EaxEaxCall& eax_call);

    void defer_all(
        const EaxEaxCall& eax_call);


    // [[nodiscard]]
    bool apply_deferred();

    // [[nodiscard]]
    bool set(
        const EaxEaxCall& eax_call);
}; // EaxDistortionEffect


class EaxDistortionEffectException :
    public EaxException
{
public:
    explicit EaxDistortionEffectException(
        const char* message)
        :
        EaxException{"EAX_DISTORTION_EFFECT", message}
    {
    }
}; // EaxDistortionEffectException


EaxDistortionEffect::EaxDistortionEffect(
    EffectProps& al_effect_props)
    :
    al_effect_props_{al_effect_props}
{
    set_eax_defaults();
    set_efx_defaults();
}

// [[nodiscard]]
bool EaxDistortionEffect::dispatch(
    const EaxEaxCall& eax_call)
{
    return eax_call.is_get() ? get(eax_call) : set(eax_call);
}

void EaxDistortionEffect::set_eax_defaults()
{
    eax_.flEdge = EAXDISTORTION_DEFAULTEDGE;
    eax_.lGain = EAXDISTORTION_DEFAULTGAIN;
    eax_.flLowPassCutOff = EAXDISTORTION_DEFAULTLOWPASSCUTOFF;
    eax_.flEQCenter = EAXDISTORTION_DEFAULTEQCENTER;
    eax_.flEQBandwidth = EAXDISTORTION_DEFAULTEQBANDWIDTH;

    eax_d_ = eax_;
}

void EaxDistortionEffect::set_efx_edge()
{
    const auto edge = clamp(
        eax_.flEdge,
        AL_DISTORTION_MIN_EDGE,
        AL_DISTORTION_MAX_EDGE);

    al_effect_props_.Distortion.Edge = edge;
}

void EaxDistortionEffect::set_efx_gain()
{
    const auto gain = clamp(
        level_mb_to_gain(static_cast<float>(eax_.lGain)),
        AL_DISTORTION_MIN_GAIN,
        AL_DISTORTION_MAX_GAIN);

    al_effect_props_.Distortion.Gain = gain;
}

void EaxDistortionEffect::set_efx_lowpass_cutoff()
{
    const auto lowpass_cutoff = clamp(
        eax_.flLowPassCutOff,
        AL_DISTORTION_MIN_LOWPASS_CUTOFF,
        AL_DISTORTION_MAX_LOWPASS_CUTOFF);

    al_effect_props_.Distortion.LowpassCutoff = lowpass_cutoff;
}

void EaxDistortionEffect::set_efx_eq_center()
{
    const auto eq_center = clamp(
        eax_.flEQCenter,
        AL_DISTORTION_MIN_EQCENTER,
        AL_DISTORTION_MAX_EQCENTER);

    al_effect_props_.Distortion.EQCenter = eq_center;
}

void EaxDistortionEffect::set_efx_eq_bandwidth()
{
    const auto eq_bandwidth = clamp(
        eax_.flEdge,
        AL_DISTORTION_MIN_EQBANDWIDTH,
        AL_DISTORTION_MAX_EQBANDWIDTH);

    al_effect_props_.Distortion.EQBandwidth = eq_bandwidth;
}

void EaxDistortionEffect::set_efx_defaults()
{
    set_efx_edge();
    set_efx_gain();
    set_efx_lowpass_cutoff();
    set_efx_eq_center();
    set_efx_eq_bandwidth();
}

// [[nodiscard]]
bool EaxDistortionEffect::get(
    const EaxEaxCall& eax_call)
{
    switch (eax_call.get_property_id())
    {
        case EAXDISTORTION_NONE:
            break;

        case EAXDISTORTION_ALLPARAMETERS:
            eax_call.set_value<EaxDistortionEffectException>(eax_);
            break;

        case EAXDISTORTION_EDGE:
            eax_call.set_value<EaxDistortionEffectException>(eax_.flEdge);
            break;

        case EAXDISTORTION_GAIN:
            eax_call.set_value<EaxDistortionEffectException>(eax_.lGain);
            break;

        case EAXDISTORTION_LOWPASSCUTOFF:
            eax_call.set_value<EaxDistortionEffectException>(eax_.flLowPassCutOff);
            break;

        case EAXDISTORTION_EQCENTER:
            eax_call.set_value<EaxDistortionEffectException>(eax_.flEQCenter);
            break;

        case EAXDISTORTION_EQBANDWIDTH:
            eax_call.set_value<EaxDistortionEffectException>(eax_.flEQBandwidth);
            break;

        default:
            throw EaxDistortionEffectException{"Unsupported property id."};
    }

    return false;
}

void EaxDistortionEffect::validate_edge(
    float flEdge)
{
    eax_validate_range<EaxDistortionEffectException>(
        "Edge",
        flEdge,
        EAXDISTORTION_MINEDGE,
        EAXDISTORTION_MAXEDGE);
}

void EaxDistortionEffect::validate_gain(
    long lGain)
{
    eax_validate_range<EaxDistortionEffectException>(
        "Gain",
        lGain,
        EAXDISTORTION_MINGAIN,
        EAXDISTORTION_MAXGAIN);
}

void EaxDistortionEffect::validate_lowpass_cutoff(
    float flLowPassCutOff)
{
    eax_validate_range<EaxDistortionEffectException>(
        "Low-pass Cut-off",
        flLowPassCutOff,
        EAXDISTORTION_MINLOWPASSCUTOFF,
        EAXDISTORTION_MAXLOWPASSCUTOFF);
}

void EaxDistortionEffect::validate_eq_center(
    float flEQCenter)
{
    eax_validate_range<EaxDistortionEffectException>(
        "EQ Center",
        flEQCenter,
        EAXDISTORTION_MINEQCENTER,
        EAXDISTORTION_MAXEQCENTER);
}

void EaxDistortionEffect::validate_eq_bandwidth(
    float flEQBandwidth)
{
    eax_validate_range<EaxDistortionEffectException>(
        "EQ Bandwidth",
        flEQBandwidth,
        EAXDISTORTION_MINEQBANDWIDTH,
        EAXDISTORTION_MAXEQBANDWIDTH);
}

void EaxDistortionEffect::validate_all(
    const EAXDISTORTIONPROPERTIES& eax_all)
{
    validate_edge(eax_all.flEdge);
    validate_gain(eax_all.lGain);
    validate_lowpass_cutoff(eax_all.flLowPassCutOff);
    validate_eq_center(eax_all.flEQCenter);
    validate_eq_bandwidth(eax_all.flEQBandwidth);
}

void EaxDistortionEffect::defer_edge(
    float flEdge)
{
    eax_d_.flEdge = flEdge;
    eax_dirty_flags_.flEdge = (eax_.flEdge != eax_d_.flEdge);
}

void EaxDistortionEffect::defer_gain(
    long lGain)
{
    eax_d_.lGain = lGain;
    eax_dirty_flags_.lGain = (eax_.lGain != eax_d_.lGain);
}

void EaxDistortionEffect::defer_low_pass_cutoff(
    float flLowPassCutOff)
{
    eax_d_.flLowPassCutOff = flLowPassCutOff;
    eax_dirty_flags_.flLowPassCutOff = (eax_.flLowPassCutOff != eax_d_.flLowPassCutOff);
}

void EaxDistortionEffect::defer_eq_center(
    float flEQCenter)
{
    eax_d_.flEQCenter = flEQCenter;
    eax_dirty_flags_.flEQCenter = (eax_.flEQCenter != eax_d_.flEQCenter);
}

void EaxDistortionEffect::defer_eq_bandwidth(
    float flEQBandwidth)
{
    eax_d_.flEQBandwidth = flEQBandwidth;
    eax_dirty_flags_.flEQBandwidth = (eax_.flEQBandwidth != eax_d_.flEQBandwidth);
}

void EaxDistortionEffect::defer_all(
    const EAXDISTORTIONPROPERTIES& eax_all)
{
    defer_edge(eax_all.flEdge);
    defer_gain(eax_all.lGain);
    defer_low_pass_cutoff(eax_all.flLowPassCutOff);
    defer_eq_center(eax_all.flEQCenter);
    defer_eq_bandwidth(eax_all.flEQBandwidth);
}

void EaxDistortionEffect::defer_edge(
    const EaxEaxCall& eax_call)
{
    const auto& edge =
        eax_call.get_value<EaxDistortionEffectException, const decltype(EAXDISTORTIONPROPERTIES::flEdge)>();

    validate_edge(edge);
    defer_edge(edge);
}

void EaxDistortionEffect::defer_gain(
    const EaxEaxCall& eax_call)
{
    const auto& gain =
        eax_call.get_value<EaxDistortionEffectException, const decltype(EAXDISTORTIONPROPERTIES::lGain)>();

    validate_gain(gain);
    defer_gain(gain);
}

void EaxDistortionEffect::defer_low_pass_cutoff(
    const EaxEaxCall& eax_call)
{
    const auto& lowpass_cutoff =
        eax_call.get_value<EaxDistortionEffectException, const decltype(EAXDISTORTIONPROPERTIES::flLowPassCutOff)>();

    validate_lowpass_cutoff(lowpass_cutoff);
    defer_low_pass_cutoff(lowpass_cutoff);
}

void EaxDistortionEffect::defer_eq_center(
    const EaxEaxCall& eax_call)
{
    const auto& eq_center =
        eax_call.get_value<EaxDistortionEffectException, const decltype(EAXDISTORTIONPROPERTIES::flEQCenter)>();

    validate_eq_center(eq_center);
    defer_eq_center(eq_center);
}

void EaxDistortionEffect::defer_eq_bandwidth(
    const EaxEaxCall& eax_call)
{
    const auto& eq_bandwidth =
        eax_call.get_value<EaxDistortionEffectException, const decltype(EAXDISTORTIONPROPERTIES::flEQBandwidth)>();

    validate_eq_bandwidth(eq_bandwidth);
    defer_eq_bandwidth(eq_bandwidth);
}

void EaxDistortionEffect::defer_all(
    const EaxEaxCall& eax_call)
{
    const auto& all =
        eax_call.get_value<EaxDistortionEffectException, const EAXDISTORTIONPROPERTIES>();

    validate_all(all);
    defer_all(all);
}

// [[nodiscard]]
bool EaxDistortionEffect::apply_deferred()
{
    if (eax_dirty_flags_ == EaxDistortionEffectDirtyFlags{})
    {
        return false;
    }

    eax_ = eax_d_;

    if (eax_dirty_flags_.flEdge)
    {
        set_efx_edge();
    }

    if (eax_dirty_flags_.lGain)
    {
        set_efx_gain();
    }

    if (eax_dirty_flags_.flLowPassCutOff)
    {
        set_efx_lowpass_cutoff();
    }

    if (eax_dirty_flags_.flEQCenter)
    {
        set_efx_eq_center();
    }

    if (eax_dirty_flags_.flEQBandwidth)
    {
        set_efx_eq_bandwidth();
    }

    eax_dirty_flags_ = EaxDistortionEffectDirtyFlags{};

    return true;
}

// [[nodiscard]]
bool EaxDistortionEffect::set(
    const EaxEaxCall& eax_call)
{
    switch (eax_call.get_property_id())
    {
        case EAXDISTORTION_NONE:
            break;

        case EAXDISTORTION_ALLPARAMETERS:
            defer_all(eax_call);
            break;

        case EAXDISTORTION_EDGE:
            defer_edge(eax_call);
            break;

        case EAXDISTORTION_GAIN:
            defer_gain(eax_call);
            break;

        case EAXDISTORTION_LOWPASSCUTOFF:
            defer_low_pass_cutoff(eax_call);
            break;

        case EAXDISTORTION_EQCENTER:
            defer_eq_center(eax_call);
            break;

        case EAXDISTORTION_EQBANDWIDTH:
            defer_eq_bandwidth(eax_call);
            break;

        default:
            throw EaxDistortionEffectException{"Unsupported property id."};
    }

    if (!eax_call.is_deferred())
    {
        return apply_deferred();
    }

    return false;
}


} // namespace


EaxEffectUPtr eax_create_eax_distortion_effect(
    EffectProps& al_effect_props)
{
    return std::make_unique<EaxDistortionEffect>(al_effect_props);
}


#endif // ALSOFT_EAX