
/*
 * Beat Rat -- audio device open/configure
 *
 * The PipeWire-preferring open logic here is the same approach proven
 * in Phase 1: build a "type pipewire" PCM definition entirely in
 * memory via snd_config_top()/snd_pcm_open_lconf(), rather than relying
 * on the host's alsa.conf.d wiring being correct (it wasn't, on the
 * first machine we tested against). If PipeWire's plugin genuinely
 * isn't installed, snd_pcm_open_lconf() fails cleanly and we fall back
 * to plain ALSA "default".
 */

#include "audio_device.h"
#include "log.h"

#include <errno.h>
#include <stdio.h>
#include <string.h>

static const char *BEATRAT_PW_CONF =
    "pcm.beatrat_pipewire {\n"
    "  type pipewire\n"
    "  playback_node \"-1\"\n"
    "  capture_node \"-1\"\n"
    "}\n";

snd_pcm_t *audio_open_pcm(snd_pcm_stream_t stream, const char *label) {
    snd_pcm_t *pcm = NULL;
    snd_config_t *lconf = NULL;
    snd_input_t *in = NULL;
    int err;

    if ((err = snd_config_top(&lconf)) >= 0) {
        if ((err = snd_input_buffer_open(&in, BEATRAT_PW_CONF, (ssize_t)strlen(BEATRAT_PW_CONF))) >= 0) {
            if ((err = snd_config_load(lconf, in)) >= 0) {
                err = snd_pcm_open_lconf(&pcm, "beatrat_pipewire", stream, 0, lconf);
                if (err == 0) {
                    snd_input_close(in);
                    snd_config_delete(lconf);
                    LOG_INFO("%s: opened via PipeWire", label);
                    return pcm;
                }
                LOG_WARN("%s: PipeWire open failed (%s), falling back to \"default\"", label, snd_strerror(err));
            } else {
                LOG_WARN("%s: could not load in-memory PipeWire config (%s)", label, snd_strerror(err));
            }
            snd_input_close(in);
        } else {
            LOG_WARN("%s: could not open in-memory config buffer (%s)", label, snd_strerror(err));
        }
        snd_config_delete(lconf);
    } else {
        LOG_WARN("%s: snd_config_top failed (%s)", label, snd_strerror(err));
    }

    err = snd_pcm_open(&pcm, "default", stream, 0);
    if (err < 0) {
        LOG_ERROR("%s: ALSA \"default\" open also failed: %s", label, snd_strerror(err));
        return NULL;
    }
    LOG_INFO("%s: opened via ALSA \"default\"", label);
    return pcm;
}

int audio_configure_pcm(snd_pcm_t *pcm, unsigned rate, unsigned channels,
                         snd_pcm_format_t format, unsigned latency_us,
                         const char *label) {
    int err = snd_pcm_set_params(pcm, format, SND_PCM_ACCESS_RW_INTERLEAVED,
                                  channels, rate, 1, latency_us);
    if (err < 0) {
        LOG_ERROR("%s: snd_pcm_set_params failed: %s", label, snd_strerror(err));
        return -1;
    }
    LOG_INFO("%s: configured %uHz, %u ch, latency ~%uus", label, rate, channels, latency_us);
    return 0;
}

int audio_try_realtime_priority(int priority, const char *label) {
    struct sched_param sp;
    memset(&sp, 0, sizeof(sp));
    sp.sched_priority = priority;

    int err = pthread_setschedparam(pthread_self(), SCHED_FIFO, &sp);
    if (err != 0) {
        LOG_WARN("%s: could not set SCHED_FIFO priority %d (%s) -- continuing at normal priority. "
                  "(Usually a permissions issue -- see /etc/security/limits.d for rtprio limits.)",
                  label, priority, strerror(err));
        return -1;
    }
    LOG_INFO("%s: running at SCHED_FIFO priority %d", label, priority);
    return 0;
}

/* These ALSA PCM type prefixes declare a fixed channel count
 * (surroundXX) or a digital-passthrough-only format (iec958/S-PDIF)
 * that our simple mono PCM output can never satisfy -- not even
 * through "plug" conversion, since plug adapts a real device's
 * channels/format, it can't manufacture channels that were never
 * there or bypass a passthrough-only requirement. Genuinely
 * impossible to use, so these are filtered out of the enumerated list
 * entirely -- unlike "null" (see below), there's no legitimate reason
 * to ever show them.
 *
 * "null" is NOT filtered here anymore -- it's a real, working PCM (it
 * just discards playback / generates silence for capture), useful for
 * debugging and testing even though it's never a meaningful choice for
 * actual audio I/O. It's hidden by default instead (see settings.c's
 * fresh-config seeding), which the user can unhide from the Audio
 * panel like any other device -- consistent with "the config file
 * fully replaces defaults once it exists" elsewhere in this project. */
static int is_unsuitable_pcm_profile(const char *name) {
    return strncmp(name, "surround", 8) == 0 || strncmp(name, "iec958", 6) == 0;
}

/* "default:CARD=X", "sysdefault:CARD=X" and "front:CARD=X,DEV=0" are
 * three near-identical "just play normal audio on this card" profiles
 * that ALSA lists as separate hints -- showing all three per card
 * makes the list needlessly long and confusing (the same physical
 * device appearing 2-3 times in a row). Keep only the first one seen
 * per card and skip the rest; genuinely distinct profiles for the same
 * card (hdmi:CARD=X,DEV=0 vs DEV=1, say) are untouched since they
 * don't match this prefix set. */
static int is_simple_profile_prefix(const char *name) {
    return strncmp(name, "default:", 8) == 0 || strncmp(name, "sysdefault:", 11) == 0 ||
           strncmp(name, "front:", 6) == 0;
}

static int extract_card_id(const char *name, char *out, size_t out_size) {
    const char *card_eq = strstr(name, "CARD=");
    if (!card_eq)
        return 0;
    card_eq += 5;
    const char *end = strchr(card_eq, ',');
    size_t len = end ? (size_t)(end - card_eq) : strlen(card_eq);
    if (len >= out_size) len = out_size - 1;
    memcpy(out, card_eq, len);
    out[len] = '\0';
    return 1;
}

int audio_enumerate_devices(AudioDeviceInfo *out, int max_count, int want_output) {
    void **hints;
    if (snd_device_name_hint(-1, "pcm", &hints) < 0) {
        LOG_WARN("audio: device enumeration failed");
        return 0;
    }

    /* Tracks which cards already have a "simple profile" entry
     * (default/sysdefault/front), so later duplicates for the same
     * card get skipped. */
    char seen_cards[32][64];
    int num_seen_cards = 0;

    int count = 0;
    for (void **h = hints; *h != NULL && count < max_count; h++) {
        char *ioid = snd_device_name_get_hint(*h, "IOID");
        /* NULL IOID means the device supports both directions -- include
         * it in whichever list is being asked for. */
        int matches = !ioid || (want_output ? strcmp(ioid, "Output") == 0 : strcmp(ioid, "Input") == 0);
        if (matches) {
            char *name = snd_device_name_get_hint(*h, "NAME");
            char *desc = snd_device_name_get_hint(*h, "DESC");
            int skip = !name || is_unsuitable_pcm_profile(name);

            char card_id[64];
            if (!skip && is_simple_profile_prefix(name) && extract_card_id(name, card_id, sizeof(card_id))) {
                int already_seen = 0;
                for (int i = 0; i < num_seen_cards; i++)
                    if (strcmp(seen_cards[i], card_id) == 0) { already_seen = 1; break; }
                if (already_seen) {
                    skip = 1;
                } else if (num_seen_cards < 32) {
                    snprintf(seen_cards[num_seen_cards], sizeof(seen_cards[0]), "%s", card_id);
                    num_seen_cards++;
                }
            }

            if (!skip) {
                snprintf(out[count].name, sizeof(out[count].name), "%s", name);
                /* DESC can be multi-line (a short summary + longer detail);
                 * we only want the first line for a compact UI row. */
                if (desc) {
                    char *nl = strchr(desc, '\n');
                    if (nl) *nl = '\0';
                    snprintf(out[count].desc, sizeof(out[count].desc), "%s", desc);
                } else {
                    snprintf(out[count].desc, sizeof(out[count].desc), "%s", name);
                }
                count++;
            }
            free(name);
            free(desc);
        }
        free(ioid);
    }

    snd_device_name_free_hint(hints);
    LOG_INFO("audio: found %d %s device(s)", count, want_output ? "output" : "input");
    return count;
}

snd_pcm_t *audio_open_pcm_named(snd_pcm_stream_t stream, const char *device_name, const char *label) {
    snd_pcm_t *pcm = NULL;
    int err = snd_pcm_open(&pcm, device_name, stream, 0);
    if (err < 0) {
        LOG_ERROR("%s: could not open device '%s' (%s)", label, device_name, snd_strerror(err));
        return NULL;
    }
    LOG_INFO("%s: opened device '%s'", label, device_name);
    return pcm;
}

/* Wraps an arbitrary device name in ALSA's "plug" conversion layer so
 * it can auto-adapt channels/rate/format for a device that otherwise
 * demands an exact match. Naively concatenating "plug:" + device_name
 * (an earlier version of this function did exactly that) breaks for
 * any name that already has its own type:args syntax -- "front:CARD=
 * J2,DEV=0" becomes "plug:front:CARD=J2,DEV=0", which ALSA's parser
 * can't tell apart from plug's own arguments and rejects outright.
 * Building the wrapper as an actual in-memory PCM definition (the same
 * technique audio_open_pcm() already uses for the PipeWire wrapper
 * above) sidesteps that entirely -- the device name is a slave.pcm
 * string value, never re-parsed as ALSA syntax itself. */
static snd_pcm_t *open_plugged(snd_pcm_stream_t stream, const char *device_name, const char *label) {
    char conf_str[512];
    snprintf(conf_str, sizeof(conf_str),
             "pcm.beatrat_plugged {\n"
             "  type plug\n"
             "  slave.pcm \"%s\"\n"
             "}\n", device_name);

    snd_pcm_t *pcm = NULL;
    snd_config_t *lconf = NULL;
    snd_input_t *in = NULL;
    int err;

    if ((err = snd_config_top(&lconf)) < 0) {
        LOG_WARN("%s: snd_config_top failed (%s)", label, snd_strerror(err));
        return NULL;
    }
    if ((err = snd_input_buffer_open(&in, conf_str, (ssize_t)strlen(conf_str))) < 0) {
        LOG_WARN("%s: could not open in-memory config buffer (%s)", label, snd_strerror(err));
        snd_config_delete(lconf);
        return NULL;
    }
    if ((err = snd_config_load(lconf, in)) < 0) {
        LOG_WARN("%s: could not load in-memory plug config (%s)", label, snd_strerror(err));
        snd_input_close(in);
        snd_config_delete(lconf);
        return NULL;
    }
    err = snd_pcm_open_lconf(&pcm, "beatrat_plugged", stream, 0, lconf);
    snd_input_close(in);
    snd_config_delete(lconf);
    if (err < 0) {
        LOG_ERROR("%s: could not open '%s' via plug wrapper (%s)", label, device_name, snd_strerror(err));
        return NULL;
    }
    LOG_INFO("%s: opened '%s' via plug wrapper", label, device_name);
    return pcm;
}

snd_pcm_t *audio_open_and_configure_named(snd_pcm_stream_t stream, const char *device_name,
                                           unsigned rate, unsigned channels, snd_pcm_format_t format,
                                           unsigned latency_us, const char *label) {
    snd_pcm_t *pcm = audio_open_pcm_named(stream, device_name, label);
    if (pcm) {
        if (audio_configure_pcm(pcm, rate, channels, format, latency_us, label) == 0)
            return pcm;
        snd_pcm_close(pcm);
    }

    LOG_WARN("%s: '%s' didn't accept our format directly, retrying via a plug wrapper", label, device_name);
    pcm = open_plugged(stream, device_name, label);
    if (!pcm)
        return NULL; /* open_plugged already logged why */
    if (audio_configure_pcm(pcm, rate, channels, format, latency_us, label) == 0)
        return pcm;
    snd_pcm_close(pcm);
    LOG_ERROR("%s: '%s' still rejected our format even via plug -- giving up", label, device_name);
    return NULL;
}

