// Copyright 2014-2025 Jesper Larsson
//
// This file is part of Klipspringer, <https://klipspringer.avadeaux.net/>
//
// Klipspringer is free software: you can redistribute it and/or modify it under the terms of the
// GNU General Public License as published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// Klipspringer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
// even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with Klipspringer. If
// not, see <https://www.gnu.org/licenses/>.

#include "choose_format.h"
#include "Library.h"
#include "net_avadeaux_klipspringer_alsa_AlsaDevice.h" // generated by javac -h

static snd_ctl_card_info_t *card_info;
static snd_pcm_info_t *pcm_info;
static snd_pcm_hw_params_t *hw_params = NULL;

static const unsigned rates[] = { 5512, 8000, 11025, 16000, 22050, 32000, 44100, 48000, 64000, 88200, 96000, 176400, 192000, 352800, 384000 };

// -------------------------------------------------------------------------------------------------
// Helper methods

typedef struct {
    jobject devices;
    jmethodID addDevicesMid;
} DeviceCollector;

static int report_device(JNIEnv *env, DeviceCollector *coll, jstring jcard_name, const char *dev_name, int card, int dev, int sub) {
    jstring jdev_name = (*env)->NewStringUTF(env, dev_name);
    if (jdev_name == NULL) { raiseError(env, "Failed to allocate string"); return 1; }
    (*env)->CallVoidMethod(env, coll->devices, coll->addDevicesMid, jcard_name, jdev_name, card, dev, sub);
    return (*env)->ExceptionCheck(env) ? 1 : 0;
}

static int collect_devices(JNIEnv *env, DeviceCollector *coll, snd_ctl_t *ctl, int card, jboolean input) {
    int err, first_dev = -1;

    if ((err = snd_ctl_pcm_next_device(ctl, &first_dev)) < 0) { return err; }
    if (first_dev == -1) { return 0; }

    if ((err = snd_ctl_card_info(ctl, card_info)) < 0) { return err; }
    jstring jcard_name = (*env)->NewStringUTF(env, snd_ctl_card_info_get_name(card_info));
    if (jcard_name == NULL) { raiseError(env, "Failed to allocate string"); return 1; }

    for (int dev = first_dev, next_dev; dev != -1; dev = next_dev) {
        next_dev = dev;
        if ((err = snd_ctl_pcm_next_device(ctl, &next_dev)) < 0) { return err; }

        memset(pcm_info, 0, snd_pcm_info_sizeof());
        snd_pcm_info_set_device(pcm_info, dev);
        snd_pcm_info_set_stream(pcm_info, input ? SND_PCM_STREAM_CAPTURE : SND_PCM_STREAM_PLAYBACK);

        snd_pcm_info_set_subdevice(pcm_info, 0);
        if ((err = snd_ctl_pcm_info(ctl, pcm_info)) < 0) { continue; }
        int sub = 0, sub_count = snd_pcm_info_get_subdevices_count(pcm_info);
        while (sub < sub_count) {
            const char *dev_name = snd_pcm_info_get_name(pcm_info);
            err = sub_count < 2 ?
                (dev == first_dev && next_dev == -1 ?
                 report_device(env, coll, jcard_name, dev_name, card, -1, -1)
                 : report_device(env, coll, jcard_name, dev_name, card, dev, -1))
                : report_device(env, coll, jcard_name, dev_name, card, dev, sub);
            if (err) { return err; }

            while (++sub < sub_count) { // look for next valid subdevice
                snd_pcm_info_set_subdevice(pcm_info, sub);
                if (!snd_ctl_pcm_info(ctl, pcm_info)) { break; }
            }
        }
    }
    return 0;
}

typedef struct {
    jobject device;
    jmethodID addFormatMid, addRatesMid, addChannelsMid, setPausableMid;
} FormatCollector;

static int report_format(JNIEnv *env, FormatCollector *coll, jboolean s, jint b, jint ss, jboolean be) {
    (*env)->CallVoidMethod(env, coll->device, coll->addFormatMid, s, b, ss, be);
    return (*env)->ExceptionCheck(env) ? 1 : 0;
}

static int report_channels(JNIEnv *env, FormatCollector *coll, int lo, int hi) {
    (*env)->CallVoidMethod(env, coll->device, coll->addChannelsMid, lo, hi);
    return (*env)->ExceptionCheck(env) ? 1 : 0;
}

static int report_rates(JNIEnv *env, FormatCollector *coll, int lo, int hi) {
    (*env)->CallVoidMethod(env, coll->device, coll->addRatesMid, lo, hi);
    return (*env)->ExceptionCheck(env) ? 1 : 0;
}

static int report_pausable(JNIEnv *env, FormatCollector *coll, jboolean pausable) {
    (*env)->CallVoidMethod(env, coll->device, coll->setPausableMid, pausable);
    return (*env)->ExceptionCheck(env) ? 1 : 0;
}

static int collect_formats(JNIEnv *env, FormatCollector *coll, snd_pcm_t *pcm, jboolean jinput) {
    int err;

    if ((err = snd_pcm_hw_free(pcm)) < 0
        || (err = snd_pcm_hw_params_any(pcm, hw_params)) < 0) { return err; }
    if (snd_pcm_hw_params_test_access(pcm, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED) < 0) { return 0; }

    // Check available formats.
    for (snd_pcm_format_t f = 0; f < SND_PCM_FORMAT_LAST; f++) {
        if (snd_pcm_hw_params_test_format(pcm, hw_params, f)) { continue; }
        bool signd, bigend;
        unsigned bips, byps;
        if (!split_format(f, &signd, &bips, &byps, &bigend)) { continue; }
        if ((err = report_format(env, coll, signd, bips, byps, bigend)) != 0) { return err; }
    }

    // Check available rates.
    unsigned minr, maxr;
    if ((err = snd_pcm_hw_params_get_rate_min(hw_params, &minr, NULL)) < 0
        || (err = snd_pcm_hw_params_get_rate_max(hw_params, &maxr, NULL)) < 0) { return err; }
    if (minr == maxr) {
        // One rate only.
        if ((err = report_rates(env, coll, minr, maxr)) != 0) { return err; }
    } else if (!snd_pcm_hw_params_test_rate(pcm, hw_params, minr+1, 0)) {
        // Looks like it supports any rate.
        if ((err = report_rates(env, coll, minr, maxr)) != 0) { return err; }
    } else {
        // Report min.
        if ((err = report_rates(env, coll, minr, minr)) != 0) { return err; }
        // Test common rates between min and max.
        for (int i = 0; i < sizeof rates / sizeof *rates; i++) {
            if (rates[i] <= minr) { continue; }
            if (rates[i] >= maxr) { break; }
            if (!snd_pcm_hw_params_test_rate(pcm, hw_params, rates[i], 0)) {
                if ((err = report_rates(env, coll, rates[i], rates[i])) != 0) { return err; }
            }
        }
        // Report max.
        if ((err = report_rates(env, coll, maxr, maxr)) != 0) { return err; }
    }

    // Check available channels.
    unsigned minc, maxc;
    if ((err = snd_pcm_hw_params_get_channels_min(hw_params, &minc)) < 0
        || (err = snd_pcm_hw_params_get_channels_max(hw_params, &maxc)) < 0) { return err; }
    if (minc == maxc) {
        // One channel option only.
        if ((err =report_channels(env, coll, minc, maxc)) != 0) { return err; }
    } else {
        // Loop between min and max to check for gaps, but cut short if max is very large.
        int lo = minc;
        for (unsigned c = minc+1; c < maxc; c++) {
            if (snd_pcm_hw_params_test_channels(pcm, hw_params, c)) {
                // c not available, report [lo, c) if lo has value
                if (lo > -1) {
                    if ((err = report_channels(env, coll, lo, c-1)) != 0) { return err; }
                    lo = -1;
                }
            } else {
                // c available, set lo unless it already has value
                if (lo == -1) { lo = c; }
                if (c == 10) { break; } // avoid lengthy loop: assume the rest work
            }
        }
        if ((err = report_channels(env, coll, lo > -1 ? lo : maxc, maxc)) != 0) { return err; }
    }

    // Check if pause supported.
    report_pausable(env, coll, report_pausable(env, coll, snd_pcm_hw_params_can_pause(hw_params)));

    return 0;
}

// -------------------------------------------------------------------------------------------------
// Alsa initialization method.

#define AMETHOD(name) JNICALL Java_net_avadeaux_klipspringer_alsa_Alsa_ ## name

JNIEXPORT void
AMETHOD(alsaInit) (JNIEnv *env,
                   jclass jthisClass)
{
    if (snd_ctl_card_info_malloc(&card_info)) { init_error("failed to allocate card info record"); }
    if (snd_pcm_info_malloc(&pcm_info)) { init_error("failed to allocate PCM info record"); }
    if (snd_pcm_hw_params_malloc(&hw_params)) { init_error("failed to allocate hardware params record"); }
}

// -------------------------------------------------------------------------------------------------
// Native AlsaDevice methods

#define METHOD(name) JNICALL Java_net_avadeaux_klipspringer_alsa_AlsaDevice_ ## name

JNIEXPORT void
METHOD(collectDevices) (JNIEnv *env,
                        jclass jthisclass,
                        jobject jdevices,
                        jboolean jinput)
{
    DeviceCollector coll = {
        jdevices,
        (*env)->GetMethodID(env, (*env)->GetObjectClass(env, jdevices), "addDevices", "(Ljava/lang/String;Ljava/lang/String;III)V")
    };
    if (coll.addDevicesMid == NULL) {
        raiseError(env, "failed to get addDevices method");
        return;
    }

    int err, card = -1;

    while (true) {
        snd_ctl_t *ctl;

        if ((err = snd_card_next(&card)) < 0) { raiseError(env, snd_strerror(err)); return; }
        if (card == -1) { break; }

        char card_id[32];
        sprintf(card_id, "hw:%d", card);
        if ((err = snd_ctl_open(&ctl, card_id, 0)) < 0) { raiseError(env, snd_strerror(err)); return; }

        int err = collect_devices(env, &coll, ctl, card, jinput);

        snd_ctl_close(ctl);
        if (err < 0) { raiseError(env, snd_strerror(err)); }
        if (err) { return; }    // exception has been thrown
    }
}

JNIEXPORT void
METHOD(collectFormats) (JNIEnv *env,
                        jclass jthisclass,
                        jbyteArray jdevname,
                        jobject jdevice,
                        jboolean jinput)
{
    FormatCollector coll = {
        jdevice,
        (*env)->GetMethodID(env, (*env)->GetObjectClass(env, jdevice), "addFormat", "(ZIIZ)V"),
        (*env)->GetMethodID(env, (*env)->GetObjectClass(env, jdevice), "addRates", "(II)V"),
        (*env)->GetMethodID(env, (*env)->GetObjectClass(env, jdevice), "addChannels", "(II)V"),
        (*env)->GetMethodID(env, (*env)->GetObjectClass(env, jdevice), "setPausable", "(Z)V")
    };
    if (coll.addFormatMid == NULL || coll.addRatesMid == NULL || coll.addChannelsMid == NULL || coll.setPausableMid == NULL) {
        raiseError(env, "failed to get device methods");
        return;
    }

    jbyte *devname = (*env)->GetByteArrayElements(env, jdevname, NULL);
    if (devname == NULL) { raiseError(env, "Failed to allocate device name string"); return; }

    snd_pcm_t *pcm;
    int err;

    if ((err = snd_pcm_open(&pcm, (const char *) devname, jinput ? SND_PCM_STREAM_CAPTURE : SND_PCM_STREAM_PLAYBACK, 0)) < 0) {
        raiseErrorCodeException(env, err, snd_strerror(err));
        return;
    }


    err = collect_formats(env, &coll, pcm, jinput);

    snd_pcm_close(pcm);
    if (err < 0) { raiseErrorCodeException(env, err, snd_strerror(err)); }
    if (err) { return; }    // exception has been thrown
}
