#include "spa/utils/list.h"
#include <json-c/json.h>
#include <pipewire/impl-metadata.h>
#include <pipewire/pipewire.h>
#include <poll.h>
#include <spa/debug/pod.h>
#include <spa/pod/iter.h>
#include <spa/pod/parser.h>
#include <spa/utils/json.h>
#include <spa/utils/result.h>
#include <stdio.h>
#include <stdlib.h>

#define LOG_MODULE "pipewire"
#define LOG_ENABLE_DBG 1
#include "../config-verify.h"
#include "../config.h"
#include "../log.h"
#include "../module.h"
#include "../particle.h"
#include "../particles/dynlist.h"
#include "../plugin.h"
#include "../yml.h"

#define ARRAY_LENGTH(x) (sizeof((x)) / sizeof((x)[0]))
/* clang-format off */
#define X_FREE_SET(t, v) do { free((t)); (t) = (v); } while (0)
/* clang-format on */
#define X_STRDUP(s) ((s) != NULL ? strdup((s)) : NULL)

struct output_informations {
    /* internal */
    uint32_t device_id;
    uint32_t card_profile_device_id;

    /* informations */
    bool muted;
    uint8_t linear_volume; /* classic volume */
    uint8_t cubic_volume;  /* volume a la pulseaudio */
    char *name;
    char *description;
    char *form_factor; /* headset, headphone, speaker, ..., can be null */
    char *bus;         /* alsa, bluetooth, etc */
    char *icon;
};
static struct output_informations const output_informations_null;

struct data;
struct private
{
    struct particle *label;
    struct data *data;

    /* pipewire related */
    struct output_informations sink_informations;
    struct output_informations source_informations;
};

/* This struct is needed because when param event occur, the function
 * `node_events_param` will receive the corresponding event about the node
 * but there's no simple way of knowing from which node the event come from */
struct node_data {
    struct data *data;
    /* otherwise is_source */
    bool is_sink;
};

/* struct data */
struct node;
struct data {
    /* yambar module */
    struct module *module;

    char *target_sink;
    char *target_source;

    struct node *binded_sink;
    struct node *binded_source;

    struct node_data node_data_sink;
    struct node_data node_data_source;

    /* proxies */
    void *metadata;
    void *node_sink;
    void *node_source;

    /* main struct */
    struct pw_main_loop *loop;
    struct pw_context *context;
    struct pw_core *core;
    struct pw_registry *registry;

    /* listener */
    struct spa_hook registry_listener;
    struct spa_hook core_listener;
    struct spa_hook metadata_listener;
    struct spa_hook node_sink_listener;
    struct spa_hook node_source_listener;

    /* list */
    struct spa_list node_list;
    struct spa_list device_list;

    int sync;
};

/* struct Route */
struct route {
    struct device *device;

    struct spa_list link;

    enum spa_direction direction; /* direction */
    int profile_device_id;        /* device */
    char *form_factor;            /* info.type */
    char *icon_name;              /* info.icon-name */
};

static void
route_free(struct route *route)
{
    free(route->form_factor);
    free(route->icon_name);
    spa_list_remove(&route->link);
    free(route);
}

/* struct Device */
struct device {
    struct data *data;

    struct spa_list link;
    uint32_t id;
    struct spa_list routes;

    void *proxy;
    struct spa_hook listener;
};

static void
device_free(struct device *device, struct data *data)
{
    struct route *route = NULL;
    spa_list_consume(route, &device->routes, link) route_free(route);

    spa_hook_remove(&device->listener);
    pw_proxy_destroy((struct pw_proxy *)device->proxy);

    spa_list_remove(&device->link);
    free(device);
}

static struct route *
route_find_or_create(struct device *device, uint32_t profile_device_id)
{
    struct route *route = NULL;
    spa_list_for_each(route, &device->routes, link)
    {
        if (route->profile_device_id == profile_device_id)
            return route;
    }

    /* route not found, let's create it */
    route = calloc(1, sizeof(struct route));
    assert(route != NULL);
    route->device = device;
    route->profile_device_id = profile_device_id;
    spa_list_append(&device->routes, &route->link);
    return route;
}

struct node {
    struct spa_list link;
    uint32_t id;
    char *name;
};

/* struct node */
static struct route *
node_find_route(struct data *data, bool is_sink)
{
    struct private *private = data->module->private;
    struct output_informations *output_informations = NULL;

    if (is_sink) {
        if (data->node_sink == NULL)
            return NULL;
        output_informations = &private->sink_informations;
    } else {
        if (data->node_source == NULL)
            return NULL;
        output_informations = &private->source_informations;
    }

    struct device *device = NULL;
    spa_list_for_each(device, &data->device_list, link)
    {
        if (device->id != output_informations->device_id)
            continue;

        struct route *route = NULL;
        spa_list_for_each(route, &device->routes, link)
        {
            if (route->profile_device_id == output_informations->card_profile_device_id)
                return route;
        }
    }

    return NULL;
}

static void
node_unhook_binded_node(struct data *data, bool is_sink)
{
    struct node **target_node = NULL;
    struct spa_hook *target_listener = NULL;
    void **target_proxy = NULL;

    if (is_sink) {
        target_node = &data->binded_sink;
        target_listener = &data->node_sink_listener;
        target_proxy = &data->node_sink;
    } else {
        target_node = &data->binded_source;
        target_listener = &data->node_source_listener;
        target_proxy = &data->node_source;
    }

    if (*target_node == NULL)
        return;

    spa_hook_remove(target_listener);
    pw_proxy_destroy(*target_proxy);

    *target_node = NULL;
    *target_proxy = NULL;
}

static void
node_free(struct node *node, struct data *data)
{
    if (data->binded_sink == node)
        node_unhook_binded_node(data, true);
    else if (data->binded_source == node)
        node_unhook_binded_node(data, false);

    spa_list_remove(&node->link);
    free(node->name);
    free(node);
}

/* Device events */
static void
device_events_info(void *userdata, const struct pw_device_info *info)
{
    struct device *device = userdata;

    /* We only want the "Route" param, which is in Params */
    if (!(info->change_mask & PW_DEVICE_CHANGE_MASK_PARAMS))
        return;

    for (size_t i = 0; i < info->n_params; ++i) {
        if (info->params[i].id == SPA_PARAM_Route) {
            pw_device_enum_params(device->proxy, 0, info->params[i].id, 0, -1, NULL);
            break;
        }
    }
}

static void
device_events_param(void *userdata, int seq, uint32_t id, uint32_t index, uint32_t next, const struct spa_pod *param)
{
    /* We should only receive ParamRoute */
    assert(spa_pod_is_object_type(param, SPA_TYPE_OBJECT_ParamRoute));

    struct route data = {0};
    struct spa_pod_prop const *prop = NULL;

    /* device must be present otherwise I can't do anything with the data */
    prop = spa_pod_find_prop(param, NULL, SPA_PARAM_ROUTE_device);
    if (prop == NULL)
        return;
    spa_pod_get_int(&prop->value, &data.profile_device_id);

    /* same for direction, required too */
    prop = spa_pod_find_prop(param, NULL, SPA_PARAM_ROUTE_direction);
    if (prop == NULL)
        return;
    char const *direction = NULL;
    spa_pod_get_string(&prop->value, &direction);
    if (spa_streq(direction, "Output"))
        data.direction = SPA_DIRECTION_OUTPUT;
    else
        data.direction = SPA_DIRECTION_INPUT;

    /* same for info, it's required */
    prop = spa_pod_find_prop(param, NULL, SPA_PARAM_ROUTE_info);
    if (prop == NULL)
        return;

    struct spa_pod *iter = NULL;
    char const *header = NULL;
    SPA_POD_STRUCT_FOREACH(&prop->value, iter)
    {
        /* no previous header */
        if (header == NULL) {
            /* headers are always string */
            if (spa_pod_is_string(iter))
                spa_pod_get_string(iter, &header);
            /* otherwise it's the first iteration (number of elements in the struct) */
            continue;
        }

        /* Values needed:
         * - (string) device.icon_name [icon_name]
         * - (string) port.type [form_factor] */
        if (spa_pod_is_string(iter)) {
            if (spa_streq(header, "device.icon_name"))
                spa_pod_get_string(iter, (char const **)&data.icon_name);
            else if (spa_streq(header, "port.type")) {
                spa_pod_get_string(iter, (char const **)&data.form_factor);
            }
        }

        header = NULL;
    }

    struct device *device = userdata;

    struct route *route = route_find_or_create(device, data.profile_device_id);
    X_FREE_SET(route->form_factor, X_STRDUP(data.form_factor));
    X_FREE_SET(route->icon_name, X_STRDUP(data.icon_name));
    route->direction = data.direction;

    /* set missing informations if possible */
    struct private *private = device->data->module->private;
    struct node *binded_node = NULL;
    struct output_informations *output_informations = NULL;

    if (route->direction == SPA_DIRECTION_INPUT) {
        binded_node = private->data->binded_source;
        output_informations = &private->source_informations;
    } else {
        binded_node = private->data->binded_sink;
        output_informations = &private->sink_informations;
    }

    /* Node not binded */
    if (binded_node == NULL)
        return;

    /* Node's device is the the same as route's device */
    if (output_informations->device_id != route->device->id)
        return;

    /* Route is not the Node's device route */
    if (output_informations->card_profile_device_id != route->profile_device_id)
        return;

    /* Update missing informations */
    X_FREE_SET(output_informations->form_factor, X_STRDUP(route->form_factor));
    X_FREE_SET(output_informations->icon, X_STRDUP(route->icon_name));

    device->data->module->bar->refresh(device->data->module->bar);
}

static struct pw_device_events const device_events = {
    PW_VERSION_DEVICE_EVENTS,
    .info = device_events_info,
    .param = device_events_param,
};

/* Node events */
static void
node_events_info(void *userdata, struct pw_node_info const *info)
{
    struct node_data *node_data = userdata;
    struct data *data = node_data->data;
    struct private *private = data->module->private;

    if (info->change_mask & PW_NODE_CHANGE_MASK_PARAMS) {
        /* We only need the Props param, so let's try to find it */
        for (size_t i = 0; i < info->n_params; ++i) {
            if (info->params[i].id == SPA_PARAM_Props) {
                void *target_node = (node_data->is_sink ? data->node_sink : data->node_source);
                /* Found it, will emit a param event, the parem will then be handled
                 * in node_events_param */
                pw_node_enum_params(target_node, 0, info->params[i].id, 0, -1, NULL);
                break;
            }
        }
    }

    if (info->change_mask & PW_NODE_CHANGE_MASK_PROPS) {
        struct output_informations *output_informations
            = (node_data->is_sink ? &private->sink_informations : &private->source_informations);
        struct spa_dict_item const *item = NULL;

        item = spa_dict_lookup_item(info->props, "node.name");
        if (item != NULL)
            X_FREE_SET(output_informations->name, X_STRDUP(item->value));

        item = spa_dict_lookup_item(info->props, "node.description");
        if (item != NULL)
            X_FREE_SET(output_informations->description, X_STRDUP(item->value));

        item = spa_dict_lookup_item(info->props, "device.id");
        if (item != NULL) {
            uint32_t value = 0;
            spa_atou32(item->value, &value, 10);
            output_informations->device_id = value;
        }

        item = spa_dict_lookup_item(info->props, "card.profile.device");
        if (item != NULL) {
            uint32_t value = 0;
            spa_atou32(item->value, &value, 10);
            output_informations->card_profile_device_id = value;
        }

        /* Device's informations has an more important priority than node's informations */
        /* icon_name */
        struct route *route = node_find_route(data, node_data->is_sink);
        if (route != NULL && route->icon_name != NULL)
            output_informations->icon = X_STRDUP(route->icon_name);
        else {
            item = spa_dict_lookup_item(info->props, "device.icon-name");
            if (item != NULL)
                X_FREE_SET(output_informations->icon, X_STRDUP(item->value));
        }
        /* form_factor */
        if (route != NULL && route->form_factor != NULL)
            output_informations->form_factor = X_STRDUP(route->form_factor);
        else {
            item = spa_dict_lookup_item(info->props, "device.form-factor");
            if (item != NULL)
                X_FREE_SET(output_informations->form_factor, X_STRDUP(item->value));
        }

        item = spa_dict_lookup_item(info->props, "device.bus");
        if (item != NULL)
            X_FREE_SET(output_informations->bus, X_STRDUP(item->value));

        data->module->bar->refresh(data->module->bar);
    }
}

static void
node_events_param(void *userdata, __attribute__((unused)) int seq, __attribute__((unused)) uint32_t id,
                  __attribute__((unused)) uint32_t index, __attribute__((unused)) uint32_t next,
                  const struct spa_pod *param)
{
    struct node_data *node_data = userdata;
    struct data *data = node_data->data;
    struct private *private = data->module->private;

    struct output_informations *output_informations
        = (node_data->is_sink ? &private->sink_informations : &private->source_informations);
    struct spa_pod_prop const *prop = NULL;

    prop = spa_pod_find_prop(param, NULL, SPA_PROP_mute);
    if (prop != NULL) {
        bool value = false;
        spa_pod_get_bool(&prop->value, &value);
        output_informations->muted = value;
    }

    prop = spa_pod_find_prop(param, NULL, SPA_PROP_channelVolumes);
    if (prop != NULL) {
        uint32_t n_values = 0;
        float *values = spa_pod_get_array(&prop->value, &n_values);
        float total = 0.0f;

        /* Array can be empty some times, assume that values have not changed */
        if (n_values != 0) {
            for (uint32_t i = 0; i < n_values; ++i)
                total += values[i];

            float base_volume = total / n_values;
            output_informations->linear_volume = ceilf(base_volume * 100);
            output_informations->cubic_volume = ceilf(cbrtf(base_volume) * 100);
        }
    }

    data->module->bar->refresh(data->module->bar);
}

static struct pw_node_events const node_events = {
    PW_VERSION_NODE_EVENTS,
    .info = node_events_info,
    .param = node_events_param,
};

/* Metadata events */
static int
metadata_property(void *userdata, __attribute__((unused)) uint32_t id, char const *key,
                  __attribute__((unused)) char const *type, char const *value)
{
    struct data *data = userdata;
    bool is_sink = false; // true for source mode
    char **target_name = NULL;

    /* We only want default.audio.sink or default.audio.source */
    if (spa_streq(key, "default.audio.sink")) {
        is_sink = true;
        target_name = &data->target_sink;
    } else if (spa_streq(key, "default.audio.source")) {
        is_sink = false; /* just to be explicit */
        target_name = &data->target_source;
    } else
        return 0;

    /* Value is NULL when the profile is set to `off`. */
    if (value == NULL) {
        node_unhook_binded_node(data, is_sink);
        free(*target_name);
        *target_name = NULL;
        data->module->bar->refresh(data->module->bar);
        return 0;
    }

    struct json_object *json = json_tokener_parse(value);
    struct json_object_iterator json_it = json_object_iter_begin(json);
    struct json_object_iterator json_it_end = json_object_iter_end(json);

    while (!json_object_iter_equal(&json_it, &json_it_end)) {
        char const *key = json_object_iter_peek_name(&json_it);
        if (!spa_streq(key, "name")) {
            json_object_iter_next(&json_it);
            continue;
        }

        /* Found name */
        struct json_object *value = json_object_iter_peek_value(&json_it);
        assert(json_object_is_type(value, json_type_string));

        char const *name = json_object_get_string(value);
        /* `auto_null` is the same as `value == NULL` see lines above. */
        if (spa_streq(name, "auto_null")) {
            node_unhook_binded_node(data, is_sink);
            free(*target_name);
            *target_name = NULL;
            data->module->bar->refresh(data->module->bar);
            break;
        }

        /* target_name is the same */
        if (spa_streq(name, *target_name))
            break;

        /* Unhook the binded_node */
        node_unhook_binded_node(data, is_sink);

        /* Update the target */
        free(*target_name);
        *target_name = strdup(name);

        /* Sync the core, core_events_done will then try to bind the good node */
        data->sync = pw_core_sync(data->core, PW_ID_CORE, data->sync);
        break;
    }

    json_object_put(json);

    return 0;
}

static struct pw_metadata_events const metadata_events = {
    PW_VERSION_METADATA_EVENTS,
    .property = metadata_property,
};

/* Registry events */
static void
registry_event_global(void *userdata, uint32_t id, __attribute__((unused)) uint32_t permissions, char const *type,
                      __attribute__((unused)) uint32_t version, struct spa_dict const *props)
{
    struct data *data = userdata;

    /* New device */
    if (spa_streq(type, PW_TYPE_INTERFACE_Device)) {
        struct device *device = calloc(1, sizeof(struct device));
        assert(device != NULL);
        device->data = data;
        device->id = id;
        spa_list_init(&device->routes);
        device->proxy = pw_registry_bind(data->registry, id, type, PW_VERSION_DEVICE, 0);
        assert(device->proxy != NULL);
        pw_device_add_listener(device->proxy, &device->listener, &device_events, device);

        spa_list_append(&data->device_list, &device->link);
    }
    /* New node */
    else if (spa_streq(type, PW_TYPE_INTERFACE_Node)) {
        /* Fill a new node */
        struct node *node = calloc(1, sizeof(struct node));
        assert(node != NULL);
        node->id = id;
        node->name = strdup(spa_dict_lookup(props, PW_KEY_NODE_NAME));

        /* Store it */
        spa_list_append(&data->node_list, &node->link);
    }
    /* New metadata */
    else if (spa_streq(type, PW_TYPE_INTERFACE_Metadata)) {
        /* A metadata has already been bind */
        if (data->metadata != NULL)
            return;

        /* Target only metadata which has "default" key */
        char const *name = spa_dict_lookup(props, PW_KEY_METADATA_NAME);
        if (name == NULL || !spa_streq(name, "default"))
            return;

        /* Bind metadata */
        data->metadata = pw_registry_bind(data->registry, id, type, PW_VERSION_METADATA, 0);
        assert(data->metadata != NULL);
        pw_metadata_add_listener(data->metadata, &data->metadata_listener, &metadata_events, data);
    }

    /* `core_events_done` will then try to bind the good node */
    data->sync = pw_core_sync(data->core, PW_ID_CORE, data->sync);
}

static void
registry_event_global_remove(void *userdata, uint32_t id)
{
    struct data *data = userdata;

    /* Try to find a node with the same `id` */
    struct node *node = NULL, *node_temp = NULL;
    spa_list_for_each_safe(node, node_temp, &data->node_list, link)
    {
        if (node->id == id) {
            node_free(node, data);
            return;
        }
    }

    /* No node with this `id` maybe it's a device */
    struct device *device = NULL, *device_temp = NULL;
    spa_list_for_each_safe(device, device_temp, &data->device_list, link)
    {
        if (device->id == id) {
            device_free(device, data);
            return;
        }
    }
}

static struct pw_registry_events const registry_events = {
    PW_VERSION_REGISTRY_EVENTS,
    .global = registry_event_global,
    .global_remove = registry_event_global_remove,
};

static void
try_to_bind_node(struct node_data *node_data, char const *target_name, struct node **target_node, void **target_proxy,
                 struct spa_hook *target_listener)
{
    /* profile deactived */
    if (target_name == NULL)
        return;

    struct data *data = node_data->data;

    struct node *node = NULL;
    spa_list_for_each(node, &data->node_list, link)
    {
        if (!spa_streq(target_name, node->name))
            continue;

        /* Found good node */

        *target_node = node;
        *target_proxy = pw_registry_bind(data->registry, node->id, PW_TYPE_INTERFACE_Node, PW_VERSION_NODE, 0);
        assert(*target_proxy != NULL);
        pw_node_add_listener(*target_proxy, target_listener, &node_events, node_data);
        break;
    }
}

/* Core events */
static void
core_events_done(void *userdata, uint32_t id, int seq)
{
    struct data *data = userdata;

    if (id != PW_ID_CORE)
        return;

    /* Not our seq */
    if (data->sync != seq)
        return;

    /* Sync ended, try to bind the node which has the targeted sink or the targeted source */

    /* Node sink not binded and target_sink is set */
    if (data->binded_sink == NULL && data->target_sink != NULL)
        try_to_bind_node(&data->node_data_sink, data->target_sink, &data->binded_sink, &data->node_sink,
                         &data->node_sink_listener);

    /* Node source not binded and target_source is set */
    if (data->binded_source == NULL && data->target_source != NULL)
        try_to_bind_node(&data->node_data_source, data->target_source, &data->binded_source, &data->node_source,
                         &data->node_source_listener);
}

static void
core_events_error(void *userdata, uint32_t id, int seq, int res, char const *message)
{
    pw_log_error("error id:%u seq:%d res:%d (%s): %s", id, seq, res, spa_strerror(res), message);

    if (id == PW_ID_CORE && res == -EPIPE) {
        struct data *data = userdata;
        pw_main_loop_quit(data->loop);
    }
}

static struct pw_core_events const core_events = {
    PW_VERSION_CORE_EVENTS,
    .done = core_events_done,
    .error = core_events_error,
};

/* init, deinit */
static struct data *
pipewire_init(struct module *module)
{
    pw_init(NULL, NULL);

    /* Data */
    struct data *data = calloc(1, sizeof(struct data));
    assert(data != NULL);

    spa_list_init(&data->node_list);
    spa_list_init(&data->device_list);

    /* Main loop */
    data->loop = pw_main_loop_new(NULL);
    assert(data->loop != NULL);

    /* Context */
    data->context = pw_context_new(pw_main_loop_get_loop(data->loop), NULL, 0);
    assert(data->context != NULL);

    /* Core */
    data->core = pw_context_connect(data->context, NULL, 0);
    assert(data->core);
    pw_core_add_listener(data->core, &data->core_listener, &core_events, data);

    /* Registry */
    data->registry = pw_core_get_registry(data->core, PW_VERSION_REGISTRY, 0);
    assert(data->registry);
    pw_registry_add_listener(data->registry, &data->registry_listener, &registry_events, data);

    /* Sync */
    data->sync = pw_core_sync(data->core, PW_ID_CORE, data->sync);

    data->module = module;

    /* node_events_param_data */
    data->node_data_sink.data = data;
    data->node_data_sink.is_sink = true;
    data->node_data_source.data = data;
    data->node_data_source.is_sink = false;

    return data;
}

static void
pipewire_deinit(struct data *data)
{
    struct node *node = NULL;
    spa_list_consume(node, &data->node_list, link) node_free(node, data);

    struct device *device = NULL;
    spa_list_consume(device, &data->device_list, link) device_free(device, data);

    if (data->metadata)
        pw_proxy_destroy((struct pw_proxy *)data->metadata);
    spa_hook_remove(&data->registry_listener);
    pw_proxy_destroy((struct pw_proxy *)data->registry);
    spa_hook_remove(&data->core_listener);
    spa_hook_remove(&data->metadata_listener);
    pw_core_disconnect(data->core);
    pw_context_destroy(data->context);
    pw_main_loop_destroy(data->loop);
    free(data->target_sink);
    free(data->target_source);
    pw_deinit();
}

static void
destroy(struct module *module)
{
    struct private *private = module->private;

    pipewire_deinit(private->data);
    private->label->destroy(private->label);

    /* sink */
    free(private->sink_informations.name);
    free(private->sink_informations.description);
    free(private->sink_informations.icon);
    free(private->sink_informations.form_factor);
    free(private->sink_informations.bus);
    /* source */
    free(private->source_informations.name);
    free(private->source_informations.description);
    free(private->source_informations.icon);
    free(private->source_informations.form_factor);
    free(private->source_informations.bus);

    free(private);
    module_default_destroy(module);
}

static char const *
description(const struct module *module)
{
    return "pipewire";
}

static struct exposable *
content(struct module *module)
{
    struct private *private = module->private;

    mtx_lock(&module->lock);

    struct exposable *exposables[2];
    size_t exposables_length = ARRAY_LENGTH(exposables);

    struct output_informations const *output_informations = NULL;

    /* sink */
    output_informations
        = (private->data->target_sink == NULL ? &output_informations_null : &private->sink_informations);

    struct tag_set sink_tag_set = (struct tag_set){
        .tags = (struct tag *[]){
            tag_new_string(module, "type", "sink"),
            tag_new_string(module, "name", output_informations->name),
            tag_new_string(module, "description", output_informations->description),
            tag_new_string(module, "icon", output_informations->icon),
            tag_new_string(module, "form_factor", output_informations->form_factor),
            tag_new_string(module, "bus", output_informations->bus),
            tag_new_bool(module, "muted", output_informations->muted),
            tag_new_int_range(module, "linear_volume", output_informations->linear_volume, 0, 100),
            tag_new_int_range(module, "cubic_volume", output_informations->cubic_volume, 0, 100),
        },
        .count = 9,
    };

    /* source */
    output_informations
        = (private->data->target_source == NULL ? &output_informations_null : &private->source_informations);

    struct tag_set source_tag_set = (struct tag_set){
        .tags = (struct tag *[]){
            tag_new_string(module, "type", "source"),
            tag_new_string(module, "name", output_informations->name),
            tag_new_string(module, "description", output_informations->description),
            tag_new_string(module, "icon", output_informations->icon),
            tag_new_string(module, "form_factor", output_informations->form_factor),
            tag_new_string(module, "bus", output_informations->bus),
            tag_new_bool(module, "muted", output_informations->muted),
            tag_new_int_range(module, "linear_volume", output_informations->linear_volume, 0, 100),
            tag_new_int_range(module, "cubic_volume", output_informations->cubic_volume, 0, 100),
        },
        .count = 9,
    };

    exposables[0] = private->label->instantiate(private->label, &sink_tag_set);
    exposables[1] = private->label->instantiate(private->label, &source_tag_set);

    tag_set_destroy(&sink_tag_set);
    tag_set_destroy(&source_tag_set);

    mtx_unlock(&module->lock);

    return dynlist_exposable_new(exposables, exposables_length, 0, 0);
}

static int
run(struct module *module)
{
    struct private *private = module->private;
    struct pw_loop *pw_loop = pw_main_loop_get_loop(private->data->loop);
    struct pollfd pollfds[] = {
        /* abort_fd */
        (struct pollfd){.fd = module->abort_fd, .events = POLLIN},
        /* pipewire */
        (struct pollfd){.fd = pw_loop_get_fd(pw_loop), .events = POLLIN},
    };

    while (true) {
        if (poll(pollfds, ARRAY_LENGTH(pollfds), -1) == -1) {
            if (errno == EINTR)
                continue;

            LOG_ERRNO("Unable to poll: %s", strerror(errno));
            break;
        }

        /* abort_fd */
        if (pollfds[0].revents & POLLIN)
            break;

        /* pipewire */
        if (!(pollfds[1].revents & POLLIN))
            /* issue happened */
            break;

        int result = pw_loop_iterate(pw_loop, 0);
        if (result < 0) {
            LOG_ERRNO("Unable to iterate pipewire loop: %s", spa_strerror(result));
            break;
        }
    }

    return 0;
}

static struct module *
pipewire_new(struct particle *label)
{
    struct private *private = calloc(1, sizeof(struct private));
    assert(private != NULL);
    private->label = label;

    struct module *module = module_common_new();
    module->private = private;
    module->run = &run;
    module->destroy = &destroy;
    module->content = &content;
    module->description = &description;

    private->data = pipewire_init(module);

    return module;
}

static struct module *
from_conf(struct yml_node const *node, struct conf_inherit inherited)
{
    struct yml_node const *content = yml_get_value(node, "content");
    return pipewire_new(conf_to_particle(content, inherited));
}

static bool
verify_conf(keychain_t *keychain, struct yml_node const *node)
{
    static struct attr_info const attrs[] = {
        MODULE_COMMON_ATTRS,
    };
    return conf_verify_dict(keychain, node, attrs);
}

struct module_iface const module_pipewire_iface = {
    .from_conf = &from_conf,
    .verify_conf = &verify_conf,
};

#if defined(CORE_PLUGINS_AS_SHARED_LIBRARIES)
extern struct module_iface const iface __attribute__((weak, alias("module_pipewire_iface")));
#endif
