#include <cava/common.h>
#include <gio/gio.h>

#include "astal-cava.h"
#include "cava/common.h"
#include "cava/config.h"
#include "glib-object.h"
#include "glib.h"
#include "glibconfig.h"

struct _AstalCavaCava {
    GObject parent_instance;

    gint bars;
    gboolean autosens;
    gboolean stereo;
    gdouble noise_reduction;
    gint framerate;
    AstalCavaInput input;
    gchar* audio_source;
    gboolean active;
    gint channels;
    gint low_cutoff;
    gint high_cutoff;
    gint samplerate;

    GArray* values;
};

typedef struct {
    struct cava_plan* plan;
    struct config_params cfg;
    struct audio_data audio_data;
    struct audio_raw audio_raw;
    ptr input_src;

    gboolean constructed;
    GThread* input_thread;
    guint timer_id;

} AstalCavaCavaPrivate;

G_DEFINE_ENUM_TYPE(AstalCavaInput, astal_cava_input,
                   G_DEFINE_ENUM_VALUE(ASTAL_CAVA_INPUT_FIFO, "fifo"),
                   G_DEFINE_ENUM_VALUE(ASTAL_CAVA_INPUT_PORTAUDIO, "portaudio"),
                   G_DEFINE_ENUM_VALUE(ASTAL_CAVA_INPUT_PIPEWIRE, "pipewire"),
                   G_DEFINE_ENUM_VALUE(ASTAL_CAVA_INPUT_ALSA, "alsa"),
                   G_DEFINE_ENUM_VALUE(ASTAL_CAVA_INPUT_PULSE, "pulse"),
                   G_DEFINE_ENUM_VALUE(ASTAL_CAVA_INPUT_SNDIO, "sndio"),
                   G_DEFINE_ENUM_VALUE(ASTAL_CAVA_INPUT_SHMEM, "shmem"),
                   G_DEFINE_ENUM_VALUE(ASTAL_CAVA_INPUT_WINSCAP, "winscap"));

G_DEFINE_TYPE_WITH_PRIVATE(AstalCavaCava, astal_cava_cava, G_TYPE_OBJECT)

typedef enum {
    ASTAL_CAVA_CAVA_PROP_VALUES = 1,
    ASTAL_CAVA_CAVA_PROP_ACTIVE,
    ASTAL_CAVA_CAVA_PROP_BARS,
    ASTAL_CAVA_CAVA_PROP_AUTOSENS,
    ASTAL_CAVA_CAVA_PROP_STEREO,
    ASTAL_CAVA_CAVA_PROP_NOISE,
    ASTAL_CAVA_CAVA_PROP_FRAMERATE,
    ASTAL_CAVA_CAVA_PROP_INPUT,
    ASTAL_CAVA_CAVA_PROP_SOURCE,
    ASTAL_CAVA_CAVA_PROP_CHANNELS,
    ASTAL_CAVA_CAVA_PROP_LOW_CUTOFF,
    ASTAL_CAVA_CAVA_PROP_HIGH_CUTOFF,
    ASTAL_CAVA_CAVA_PROP_SAMPLERATE,
    ASTAL_CAVA_CAVA_N_PROPERTIES
} AstalCavaProperties;

static GParamSpec* astal_cava_cava_properties[ASTAL_CAVA_CAVA_N_PROPERTIES] = {
    NULL,
};

static gboolean exec_cava(AstalCavaCava* self) {
    AstalCavaCavaPrivate* priv = astal_cava_cava_get_instance_private(self);

    pthread_mutex_lock(&priv->audio_data.lock);
    cava_execute(priv->audio_data.cava_in, priv->audio_data.samples_counter,
                 priv->audio_raw.cava_out, priv->plan);
    if (priv->audio_data.samples_counter > 0) priv->audio_data.samples_counter = 0;
    pthread_mutex_unlock(&priv->audio_data.lock);

    g_array_remove_range(self->values, 0, priv->audio_raw.number_of_bars);
    g_array_insert_vals(self->values, 0, priv->audio_raw.cava_out, priv->audio_raw.number_of_bars);

    g_object_notify(G_OBJECT(self), "values");

    return G_SOURCE_CONTINUE;
}

static void astal_cava_cava_cleanup(AstalCavaCava* self) {
    AstalCavaCavaPrivate* priv = astal_cava_cava_get_instance_private(self);

    g_source_remove(priv->timer_id);
    pthread_mutex_lock(&priv->audio_data.lock);
    priv->audio_data.terminate = 1;
    pthread_mutex_unlock(&priv->audio_data.lock);
    g_thread_join(priv->input_thread);

    cava_destroy(priv->plan);
    audio_raw_clean(&priv->audio_raw);
    free(priv->audio_data.cava_in);
    g_free(priv->audio_data.source);

    free_config(&priv->cfg);
}

static void astal_cava_cava_start(AstalCavaCava* self) {
    AstalCavaCavaPrivate* priv = astal_cava_cava_get_instance_private(self);

    if (self->framerate < 1) {
        self->framerate = 1;
    }

    if (self->low_cutoff < 1) {
        self->low_cutoff = 1;
    }

    if (self->high_cutoff < self->low_cutoff) {
        self->high_cutoff = self->low_cutoff + 1;
    }

    if (self->samplerate / 2 <= self->high_cutoff) {
        self->samplerate = self->high_cutoff * 2 + 1;
    }

    if (self->bars < 1) {
        self->bars = 1;
    }

    if (self->channels < 1 || self->channels > 2) {
        self->channels = 2;
    }

    priv->cfg = (struct config_params){
        .inAtty = 0,
        .output = OUTPUT_RAW,
        .raw_target = strdup("/dev/stdout"),
        .data_format = strdup("binary"),

        .fixedbars = self->bars,
        .autosens = self->autosens,
        .stereo = self->stereo,
        .noise_reduction = self->noise_reduction,
        .framerate = self->framerate,
        .input = (enum input_method)self->input,
        .channels = self->channels,
        .lower_cut_off = self->low_cutoff,
        .upper_cut_off = self->high_cutoff,
        .samplerate = self->samplerate,

        // not needed in this lib
        .mono_opt = AVERAGE,
        .waves = 0,
        .userEQ = NULL,
        .userEQ_keys = 0,
        .userEQ_enabled = 0,
        .samplebits = 16,
        .waveform = 0,
        .monstercat = 0,
        .sens = 1,
        .autoconnect = 2,
        .reverse = 0,
        .sleep_timer = 0,
        .show_idle_bar_heads = 1,
        .continuous_rendering = 0,
        .sdl_width = 1000,
        .sdl_height = 500,
        .sdl_x = -1,
        .sdl_y = -1,
        .sdl_full_screen = 0,
        .draw_and_quit = 0,
        .zero_test = 0,
        .non_zero_test = 0,
        .sync_updates = 0,
        .disable_blanking = 1,
        .bar_height = 32,
        .col = 0,
        .bgcol = 0,
        .autobars = 0,
        .raw_format = FORMAT_BINARY,
        .ascii_range = 1000,
        .bit_format = 16,
        .gradient = 0,
        .gradient_count = 0,
        .bar_width = 2,
        .bar_spacing = 1,
        .xaxis = NONE,
        .orientation = ORIENT_BOTTOM,
        .color = NULL,
        .bcolor = NULL,
        .gradient_colors = NULL,
        .vertex_shader = NULL,
        .fragment_shader = NULL,
        .bar_delim = ';',
        .frame_delim = '\n',
    };

    if (g_strcmp0(self->audio_source, "auto") == 0) {
        switch (priv->cfg.input) {
            case INPUT_ALSA:
                priv->cfg.audio_source = g_strdup("hw:Loopback,1");
                break;
            case INPUT_FIFO:
                priv->cfg.audio_source = g_strdup("/tmp/mpd.fifo");
                break;
            case INPUT_PULSE:
                priv->cfg.audio_source = g_strdup("auto");
                break;
            case INPUT_PIPEWIRE:
                priv->cfg.audio_source = g_strdup("auto");
                break;
            case INPUT_SNDIO:
                priv->cfg.audio_source = g_strdup("default");
                break;
            case INPUT_OSS:
                priv->cfg.audio_source = g_strdup("/dev/dsp");
                break;
            case INPUT_JACK:
                priv->cfg.audio_source = g_strdup("default");
                break;
            case INPUT_SHMEM:
                priv->cfg.audio_source = g_strdup("/squeezelite-00:00:00:00:00:00");
                break;
            case INPUT_PORTAUDIO:
                priv->cfg.audio_source = g_strdup("auto");
                break;
            default:
                g_critical("unsupported audio source");
        }
    } else {
        priv->cfg.audio_source = g_strdup(self->audio_source);
    }

    priv->audio_data = (struct audio_data){
        .cava_in = NULL,
        .input_buffer_size = BUFFER_SIZE * priv->cfg.channels,
        .cava_buffer_size = BUFFER_SIZE * priv->cfg.channels * 8,
        .format = -1,
        .rate = 0,
        .channels = priv->cfg.channels,
        .source = NULL,
        .terminate = 0,
        .samples_counter = 0,
        .IEEE_FLOAT = 0,
        .suspendFlag = false,
    };

    priv->input_src = get_input(&priv->audio_data, &priv->cfg);
    audio_raw_init(&priv->audio_data, &priv->audio_raw, &priv->cfg, &priv->plan);
    priv->input_thread = g_thread_new("cava_input", priv->input_src, &priv->audio_data);

    priv->timer_id = g_timeout_add(1000 / priv->cfg.framerate, G_SOURCE_FUNC(exec_cava), self);
}

static void astal_cava_cava_restart(AstalCavaCava* self) {
    if (!self->active) return;
    astal_cava_cava_cleanup(self);
    astal_cava_cava_start(self);
}

gboolean astal_cava_cava_get_active(AstalCavaCava* self) { return self->active; }

void astal_cava_cava_set_active(AstalCavaCava* self, gboolean active) {
    if (self->active == active) return;
    AstalCavaCavaPrivate* priv = astal_cava_cava_get_instance_private(self);

    self->active = active;

    if (!priv->constructed) return;
    if (!active)
        astal_cava_cava_cleanup(self);
    else
        astal_cava_cava_start(self);
}

/**
 * astal_cava_cava_get_values
 * @self: the AstalCavaCava object
 *
 * Returns: (transfer none) (element-type gdouble): a list of values
 *
 */
GArray* astal_cava_cava_get_values(AstalCavaCava* self) { return self->values; }

gint astal_cava_cava_get_bars(AstalCavaCava* self) { return self->bars; }

void astal_cava_cava_set_bars(AstalCavaCava* self, gint bars) {
    AstalCavaCavaPrivate* priv = astal_cava_cava_get_instance_private(self);
    self->bars = bars;
    if (priv->constructed) {
        g_array_set_size(self->values, self->bars);
        astal_cava_cava_restart(self);
    }
}

gboolean astal_cava_cava_get_autosens(AstalCavaCava* self) { return self->autosens; }

void astal_cava_cava_set_autosens(AstalCavaCava* self, gboolean autosens) {
    AstalCavaCavaPrivate* priv = astal_cava_cava_get_instance_private(self);
    self->autosens = autosens;
    if (priv->constructed) astal_cava_cava_restart(self);
}

gboolean astal_cava_cava_get_stereo(AstalCavaCava* self) { return self->stereo; }

void astal_cava_cava_set_stereo(AstalCavaCava* self, gboolean stereo) {
    AstalCavaCavaPrivate* priv = astal_cava_cava_get_instance_private(self);
    self->stereo = stereo;
    if (priv->constructed) astal_cava_cava_restart(self);
}

gdouble astal_cava_cava_get_noise_reduction(AstalCavaCava* self) { return self->noise_reduction; }

void astal_cava_cava_set_noise_reduction(AstalCavaCava* self, gdouble noise) {
    AstalCavaCavaPrivate* priv = astal_cava_cava_get_instance_private(self);
    self->noise_reduction = noise;
    if (priv->constructed) astal_cava_cava_restart(self);
}

gint astal_cava_cava_get_framerate(AstalCavaCava* self) { return self->framerate; }

void astal_cava_cava_set_framerate(AstalCavaCava* self, gint framerate) {
    AstalCavaCavaPrivate* priv = astal_cava_cava_get_instance_private(self);
    self->framerate = framerate;
    if (priv->constructed) astal_cava_cava_restart(self);
}

AstalCavaInput astal_cava_cava_get_input(AstalCavaCava* self) { return self->input; }

void astal_cava_cava_set_input(AstalCavaCava* self, AstalCavaInput input) {
    AstalCavaCavaPrivate* priv = astal_cava_cava_get_instance_private(self);
    self->input = input;
    if (priv->constructed) astal_cava_cava_restart(self);
}

gchar* astal_cava_cava_get_source(AstalCavaCava* self) { return self->audio_source; }

void astal_cava_cava_set_source(AstalCavaCava* self, const gchar* source) {
    AstalCavaCavaPrivate* priv = astal_cava_cava_get_instance_private(self);
    g_free(self->audio_source);
    self->audio_source = g_strdup(source);
    if (priv->constructed) astal_cava_cava_restart(self);
}

gint astal_cava_cava_get_channels(AstalCavaCava* self) { return self->channels; }

void astal_cava_cava_set_channels(AstalCavaCava* self, gint channels) {
    AstalCavaCavaPrivate* priv = astal_cava_cava_get_instance_private(self);
    self->channels = channels;
    if (priv->constructed) astal_cava_cava_restart(self);
}

gint astal_cava_cava_get_low_cutoff(AstalCavaCava* self) { return self->low_cutoff; }

void astal_cava_cava_set_low_cutoff(AstalCavaCava* self, gint low_cutoff) {
    AstalCavaCavaPrivate* priv = astal_cava_cava_get_instance_private(self);
    self->low_cutoff = low_cutoff;
    if (priv->constructed) astal_cava_cava_restart(self);
}

gint astal_cava_cava_get_high_cutoff(AstalCavaCava* self) { return self->high_cutoff; }

void astal_cava_cava_set_high_cutoff(AstalCavaCava* self, gint high_cutoff) {
    AstalCavaCavaPrivate* priv = astal_cava_cava_get_instance_private(self);
    self->high_cutoff = high_cutoff;
    if (priv->constructed) astal_cava_cava_restart(self);
}

gint astal_cava_cava_get_samplerate(AstalCavaCava* self) { return self->samplerate; }

void astal_cava_cava_set_samplerate(AstalCavaCava* self, gint samplerate) {
    AstalCavaCavaPrivate* priv = astal_cava_cava_get_instance_private(self);
    self->samplerate = samplerate;
    if (priv->constructed) astal_cava_cava_restart(self);
}

static void astal_cava_cava_set_property(GObject* object, guint property_id, const GValue* value,
                                         GParamSpec* pspec) {
    AstalCavaCava* self = ASTAL_CAVA_CAVA(object);

    switch (property_id) {
        case ASTAL_CAVA_CAVA_PROP_BARS:
            astal_cava_cava_set_bars(self, g_value_get_int(value));
            break;
        case ASTAL_CAVA_CAVA_PROP_ACTIVE:
            astal_cava_cava_set_active(self, g_value_get_boolean(value));
            break;
        case ASTAL_CAVA_CAVA_PROP_AUTOSENS:
            astal_cava_cava_set_autosens(self, g_value_get_boolean(value));
            break;
        case ASTAL_CAVA_CAVA_PROP_NOISE:
            astal_cava_cava_set_noise_reduction(self, g_value_get_double(value));
            break;
        case ASTAL_CAVA_CAVA_PROP_STEREO:
            astal_cava_cava_set_stereo(self, g_value_get_boolean(value));
            break;
        case ASTAL_CAVA_CAVA_PROP_FRAMERATE:
            astal_cava_cava_set_framerate(self, g_value_get_int(value));
            break;
        case ASTAL_CAVA_CAVA_PROP_INPUT:
            astal_cava_cava_set_input(self, g_value_get_enum(value));
            break;
        case ASTAL_CAVA_CAVA_PROP_SOURCE:
            g_free(self->audio_source);
            astal_cava_cava_set_source(self, g_value_get_string(value));
            break;
        case ASTAL_CAVA_CAVA_PROP_CHANNELS:
            astal_cava_cava_set_channels(self, g_value_get_int(value));
            break;
        case ASTAL_CAVA_CAVA_PROP_LOW_CUTOFF:
            astal_cava_cava_set_low_cutoff(self, g_value_get_int(value));
            break;
        case ASTAL_CAVA_CAVA_PROP_HIGH_CUTOFF:
            astal_cava_cava_set_high_cutoff(self, g_value_get_int(value));
            break;
        case ASTAL_CAVA_CAVA_PROP_SAMPLERATE:
            astal_cava_cava_set_samplerate(self, g_value_get_int(value));
            break;
        default:
            G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
            break;
    }
}

static void astal_cava_cava_get_property(GObject* object, guint property_id, GValue* value,
                                         GParamSpec* pspec) {
    AstalCavaCava* self = ASTAL_CAVA_CAVA(object);

    switch (property_id) {
        case ASTAL_CAVA_CAVA_PROP_ACTIVE:
            g_value_set_boolean(value, self->active);
            break;
        case ASTAL_CAVA_CAVA_PROP_BARS:
            g_value_set_int(value, self->bars);
            break;
        case ASTAL_CAVA_CAVA_PROP_VALUES:
            g_value_set_boxed(value, self->values);
            break;
        case ASTAL_CAVA_CAVA_PROP_AUTOSENS:
            g_value_set_boolean(value, self->autosens);
            break;
        case ASTAL_CAVA_CAVA_PROP_NOISE:
            g_value_set_double(value, self->noise_reduction);
            break;
        case ASTAL_CAVA_CAVA_PROP_STEREO:
            g_value_set_boolean(value, self->stereo);
            break;
        case ASTAL_CAVA_CAVA_PROP_FRAMERATE:
            g_value_set_int(value, self->framerate);
            break;
        case ASTAL_CAVA_CAVA_PROP_INPUT:
            g_value_set_enum(value, self->input);
            break;
        case ASTAL_CAVA_CAVA_PROP_SOURCE:
            g_value_set_string(value, self->audio_source);
            break;
        case ASTAL_CAVA_CAVA_PROP_CHANNELS:
            g_value_set_int(value, self->channels);
            break;
        case ASTAL_CAVA_CAVA_PROP_LOW_CUTOFF:
            g_value_set_int(value, self->low_cutoff);
            break;
        case ASTAL_CAVA_CAVA_PROP_HIGH_CUTOFF:
            g_value_set_int(value, self->high_cutoff);
            break;
        case ASTAL_CAVA_CAVA_PROP_SAMPLERATE:
            g_value_set_int(value, self->samplerate);
            break;
        default:
            G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
            break;
    }
}

static void astal_cava_cava_constructed(GObject* object) {
    AstalCavaCava* self = ASTAL_CAVA_CAVA(object);
    AstalCavaCavaPrivate* priv = astal_cava_cava_get_instance_private(self);

    gdouble* data = calloc(self->bars, sizeof(gdouble));
    memset(data, 0, self->bars * sizeof(gdouble));
    self->values = g_array_new_take(data, self->bars, TRUE, sizeof(gdouble));

    priv->constructed = true;

    if (self->active) astal_cava_cava_start(self);
}

static void astal_cava_cava_init(AstalCavaCava* self) {
    AstalCavaCavaPrivate* priv = astal_cava_cava_get_instance_private(self);
    priv->constructed = false;
    self->low_cutoff = 50;
    self->high_cutoff = 10000;
    self->samplerate = 44100;
}

/**
 * astal_cava_get_default
 *
 * gets the default Cava object.
 *
 * Returns: (nullable) (transfer none):
 */
AstalCavaCava* astal_cava_get_default() { return astal_cava_cava_get_default(); }

/**
 * astal_cava_cava_get_default
 *
 * gets the default Cava object.
 *
 * Returns: (nullable) (transfer none):
 */
AstalCavaCava* astal_cava_cava_get_default() {
    static AstalCavaCava* self = NULL;

    if (self == NULL) self = g_object_new(ASTAL_CAVA_TYPE_CAVA, NULL);

    return self;
}

static void astal_cava_cava_dispose(GObject* object) {
    AstalCavaCava* self = ASTAL_CAVA_CAVA(object);

    if (self->active) astal_cava_cava_cleanup(self);
    G_OBJECT_CLASS(astal_cava_cava_parent_class)->dispose(object);
}

static void astal_cava_cava_finalize(GObject* object) {
    AstalCavaCava* self = ASTAL_CAVA_CAVA(object);

    g_array_free(self->values, TRUE);

    G_OBJECT_CLASS(astal_cava_cava_parent_class)->finalize(object);
}

static void astal_cava_cava_class_init(AstalCavaCavaClass* class) {
    GObjectClass* object_class = G_OBJECT_CLASS(class);
    object_class->get_property = astal_cava_cava_get_property;
    object_class->set_property = astal_cava_cava_set_property;
    object_class->constructed = astal_cava_cava_constructed;
    object_class->dispose = astal_cava_cava_dispose;
    object_class->finalize = astal_cava_cava_finalize;

    /**
     * AstalCavaCava:values: (type GArray(gdouble))
     *
     * A list of values, each represent the height of one bar. The values are generally between 0
     * and 1 but can overshoot occasionally, in which case the sensitivity will be decreased
     * automatically if [property@AstalCava.Cava:autosens] is set. The array will have
     * [property@AstalCava.Cava:bars] entries. If [property@AstalCava.Cava:stereo] is set, the first
     * half of the array will represent the left channel and the second half the right channel, so
     * there will be only bars/2 bars per channel. If the number of bars is odd, the last value will
     * be 0.
     */
    astal_cava_cava_properties[ASTAL_CAVA_CAVA_PROP_VALUES] =
        g_param_spec_boxed("values", "values", "a list of values", G_TYPE_ARRAY, G_PARAM_READABLE);
    /**
     * AstalCavaCava:active:
     *
     * whether or not the audio capture and visualization is running. if false the values array will
     * not be updated.
     */
    astal_cava_cava_properties[ASTAL_CAVA_CAVA_PROP_ACTIVE] = g_param_spec_boolean(
        "active", "active", "active", TRUE, G_PARAM_READWRITE | G_PARAM_CONSTRUCT);
    /**
     * AstalCavaCava:bars:
     *
     * the number of bars the visualizer should create.
     */
    astal_cava_cava_properties[ASTAL_CAVA_CAVA_PROP_BARS] =
        g_param_spec_int("bars", "bars", "number of bars per channel", 1, G_MAXINT, 20,
                         G_PARAM_READWRITE | G_PARAM_CONSTRUCT);
    /**
     * AstalCavaCava:autosens:
     *
     * When set, the sensitivity will automatically be adjusted.
     */
    astal_cava_cava_properties[ASTAL_CAVA_CAVA_PROP_AUTOSENS] =
        g_param_spec_boolean("autosens", "autosens", "dynamically adjust sensitivity", TRUE,
                             G_PARAM_READWRITE | G_PARAM_CONSTRUCT);
    /**
     * AstalCavaCava:cava:
     *
     * When set the output will contain visualization data for both channels.
     */
    astal_cava_cava_properties[ASTAL_CAVA_CAVA_PROP_STEREO] = g_param_spec_boolean(
        "stereo", "stereo", "stereo", FALSE, G_PARAM_READWRITE | G_PARAM_CONSTRUCT);
    /**
     * AstalCavaCava:noise-reduction:
     *
     * adjusts the noise-reduction filter. low values are fast and noisy, large values are slow and
     * smooth.
     */
    astal_cava_cava_properties[ASTAL_CAVA_CAVA_PROP_NOISE] =
        g_param_spec_double("noise_reduction", "noise_reduction", "noise reduction", 0, 1, 0.77,
                            G_PARAM_READWRITE | G_PARAM_CONSTRUCT);
    /**
     * AstalCavaCava:framerate:
     *
     * how often the values should be updated
     */
    astal_cava_cava_properties[ASTAL_CAVA_CAVA_PROP_FRAMERATE] =
        g_param_spec_int("framerate", "framerate", "framerate", 1, G_MAXINT, 60,
                         G_PARAM_READWRITE | G_PARAM_CONSTRUCT);
    /**
     * AstalCavaCava:channels:
     *
     * how many input channels to consider
     */
    astal_cava_cava_properties[ASTAL_CAVA_CAVA_PROP_CHANNELS] = g_param_spec_int(
        "channels", "channels", "channels", 1, 2, 2, G_PARAM_READWRITE | G_PARAM_CONSTRUCT);
    /**
     * AstalCavaCava:low-cutoff:
     *
     * cut off frequencies below this value
     */
    astal_cava_cava_properties[ASTAL_CAVA_CAVA_PROP_LOW_CUTOFF] =
        g_param_spec_int("low-cutoff", "low-cutoff", "lower frequency cutoff", 1, G_MAXINT, 50,
                         G_PARAM_READWRITE | G_PARAM_CONSTRUCT);
    /**
     * AstalCavaCava:high-cutoff:
     *
     * cut off frequencies above this value
     */
    astal_cava_cava_properties[ASTAL_CAVA_CAVA_PROP_HIGH_CUTOFF] =
        g_param_spec_int("high-cutoff", "high-cutoff", "higher frequency cutoff", 1, G_MAXINT,
                         10000, G_PARAM_READWRITE | G_PARAM_CONSTRUCT);
    /**
     * AstalCavaCava:samplerate:
     *
     * the samplerate of the input
     */
    astal_cava_cava_properties[ASTAL_CAVA_CAVA_PROP_SAMPLERATE] =
        g_param_spec_int("samplerate", "samplerate", "samplerate", 1, G_MAXINT, 44100,
                         G_PARAM_READWRITE | G_PARAM_CONSTRUCT);
    /**
     * AstalCavaCava:input: (type AstalCavaInput)
     *
     * specifies which audio server should be used.
     */
    astal_cava_cava_properties[ASTAL_CAVA_CAVA_PROP_INPUT] =
        g_param_spec_enum("input", "input", "input", ASTAL_CAVA_TYPE_INPUT,
                          ASTAL_CAVA_INPUT_PIPEWIRE, G_PARAM_READWRITE | G_PARAM_CONSTRUCT);
    /**
     * AstalCavaCava:source:
     *
     * specifies which audio source should be used. Refer to the cava docs on how to use this
     * property.
     */
    astal_cava_cava_properties[ASTAL_CAVA_CAVA_PROP_SOURCE] = g_param_spec_string(
        "source", "source", "source", "auto", G_PARAM_READWRITE | G_PARAM_CONSTRUCT);
    g_object_class_install_properties(object_class, ASTAL_CAVA_CAVA_N_PROPERTIES,
                                      astal_cava_cava_properties);
}
