/*************************************************************************
  Record-PulseAudio.cpp  -  device for audio recording via PulesAudio
                             -------------------
    begin                : Sun Okt 20 2013
    copyright            : (C) 2014 by Joerg-Christian Boehme
    email                : joerg@chaosdorf.de
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   This program 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 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 ***************************************************************************/

#include "config.h"
#ifdef HAVE_PULSEAUDIO_SUPPORT

#include <errno.h>
#include <signal.h>
#include <unistd.h>

#include <limits>

#include <pulse/thread-mainloop.h>

#include <QApplication>
#include <QCursor>
#include <QFileInfo>
#include <QLatin1Char>
#include <QLocale>
#include <QString>
#include <QVariant>
#include <QtGlobal>

#include <KLocalizedString>
#include <KUser>

#include "libkwave/Compression.h"
#include "libkwave/SampleFormat.h"
#include "libkwave/String.h"
#include "libkwave/Utils.h"
#include "libkwave/memcpy.h"

#include "Record-PulseAudio.h"

/** helper macro: returns the number of elements in an array */
#define ELEMENTS_OF(__array__) (sizeof(__array__) / sizeof(__array__[0]))

/**
 * timeout for the device scan [ms]
 * @see scanDevices()
 */
#define TIMEOUT_WAIT_DEVICE_SCAN 10000

/**
 * timeout to wait for the connection to the server [ms]
 * @see connectToServer()
 */
#define TIMEOUT_CONNECT_TO_SERVER 20000

/**
 * timeout to wait for record [ms]
 * @see open()
 */
#define TIMEOUT_CONNECT_RECORD 10000

/**
 * timeout to wait for disconnecting the recording stream [ms]
 * @see close()
 */
#define TIMEOUT_DISCONNECT_STREAM 10000

/**
 * Global list of all known sample formats.
 * @note this list should be sorted so that the most preferable formats
 *       come first in the list. When searching for a format that matches
 *       a given set of parameters, the first entry is taken.
 *
 *       The sort order should be:
 *       - compression:      none -> ulaw -> alaw -> adpcm -> mpeg ...
 *       - bits per sample:  ascending
 *       - sample format:    signed -> unsigned -> float -> double ...
 *       - endianness:       cpu -> little -> big
 *       - bytes per sample: ascending
 */
static const pa_sample_format_t _known_formats[] =
{
    /* 8 bit */
    PA_SAMPLE_U8,

    /* 16 bit */
    PA_SAMPLE_S16LE, PA_SAMPLE_S16BE,

    /* 24 bit */
    PA_SAMPLE_S24LE, PA_SAMPLE_S24BE,

    /* 24 bit in LSB of 32 bit */
    PA_SAMPLE_S24_32LE, PA_SAMPLE_S24_32BE,

    /* 32 bit */
    PA_SAMPLE_S32LE, PA_SAMPLE_S32BE,

    /* float 32 bit */
    PA_SAMPLE_FLOAT32LE, PA_SAMPLE_FLOAT32BE,

    /* ULAW */
    PA_SAMPLE_ULAW,

    /* ALAW */
    PA_SAMPLE_ALAW
};

//***************************************************************************
/** find out the SampleFormat of an PulseAudio format */
static Kwave::SampleFormat::Format sample_format_of(pa_sample_format_t fmt)
{
    Kwave::SampleFormat::Format sampleFormat = Kwave::SampleFormat::Unknown;
    switch (fmt) {
        case PA_SAMPLE_FLOAT32LE: /* FALLTHROUGH */
        case PA_SAMPLE_FLOAT32BE:
            sampleFormat = Kwave::SampleFormat::Float;
            break;
        case PA_SAMPLE_U8:
            sampleFormat = Kwave::SampleFormat::Unsigned;
            break;
        default:
            sampleFormat = Kwave::SampleFormat::Signed;
            break;
    }
    return sampleFormat;
}

//***************************************************************************
/** find out the endianness of an PulseAudio format */
static Kwave::byte_order_t endian_of(pa_sample_format_t fmt)
{
    if (pa_sample_format_is_le(fmt) == 1)
        return Kwave::LittleEndian;
    if (pa_sample_format_is_be(fmt) == 1)
        return Kwave::BigEndian;
    return Kwave::CpuEndian;
}

//***************************************************************************
static Kwave::Compression::Type compression_of(pa_sample_format_t fmt)
{
    Kwave::Compression::Type compression = Kwave::Compression::NONE;
    switch (fmt) {
        case PA_SAMPLE_ULAW:
            compression = Kwave::Compression::G711_ULAW;
            break;
        case PA_SAMPLE_ALAW:
            compression = Kwave::Compression::G711_ALAW;
            break;
        default:
            compression = Kwave::Compression::NONE;
            break;
    }
    return compression;
}

//***************************************************************************
static int bits_of(pa_sample_format_t fmt)
{
    int bits = 0;
    switch (fmt) {
        case PA_SAMPLE_ULAW:    /* FALLTHROUGH */
        case PA_SAMPLE_ALAW:    /* FALLTHROUGH */
        case PA_SAMPLE_U8:
            bits = 8;
            break;
        case PA_SAMPLE_S16LE:    /* FALLTHROUGH */
        case PA_SAMPLE_S16BE:
            bits = 16;
            break;
        case PA_SAMPLE_S24LE:    /* FALLTHROUGH */
        case PA_SAMPLE_S24BE:    /* FALLTHROUGH */
        case PA_SAMPLE_S24_32LE: /* FALLTHROUGH */
        case PA_SAMPLE_S24_32BE:
            bits = 24;
            break;
        case PA_SAMPLE_S32LE:     /* FALLTHROUGH */
        case PA_SAMPLE_S32BE:     /* FALLTHROUGH */
        case PA_SAMPLE_FLOAT32LE: /* FALLTHROUGH */
        case PA_SAMPLE_FLOAT32BE:
            bits = 32;
            break;
        default:
            bits = 0;
            break;
    }
    return bits;
}

//***************************************************************************
Kwave::RecordPulseAudio::RecordPulseAudio()
    :Kwave::RecordDevice(),
    m_mainloop_thread(this, QVariant()),
    m_mainloop_lock(),
    m_mainloop_signal(),
    m_sample_format(Kwave::SampleFormat::Unknown),
    m_tracks(0),
    m_rate(0.0),
    m_compression(Kwave::Compression::NONE),
    m_bits_per_sample(0),
    m_supported_formats(),
    m_initialized(false),
    m_pa_proplist(nullptr),
    m_pa_mainloop(nullptr),
    m_pa_context(nullptr),
    m_pa_stream(nullptr),
    m_pa_device(),
    m_name(i18n("Kwave record")),
    m_device_list()
{
}

//***************************************************************************
Kwave::RecordPulseAudio::~RecordPulseAudio()
{
    disconnectFromServer();
    m_device_list.clear();
}

//***************************************************************************
void Kwave::RecordPulseAudio::detectSupportedFormats(const QString &device)
{
    // start with an empty list
    m_supported_formats.clear();

    // lookup in the device list
    if (!m_device_list.contains(device))
        return;

    const pa_sample_spec     &sampleSpec = m_device_list[device].m_sample_spec;
    const pa_sample_format_t &formatSpec = sampleSpec.format;

    // try all known formats
    qDebug("--- list of supported formats --- ");
    for(unsigned int i = 0; i < ELEMENTS_OF(_known_formats); ++i) {
        const pa_sample_format_t &fmt = _known_formats[i];

        if (formatSpec < _known_formats[i])
            continue;

        // TODO: avoid duplicate entries that differ only in endianness,
        //       prefer our own (native) endianness

        Kwave::Compression t(compression_of(fmt));
        Kwave::SampleFormat::Map sf;
        qDebug("#%2u, %2d bit [%d byte], %s, '%s', '%s'",
            i,
            bits_of(fmt),
            (bits_of(fmt) + 7) >> 3,
            endian_of(fmt) == Kwave::CpuEndian ? "CPU" :
            (endian_of(fmt) == Kwave::LittleEndian ? "LE " : "BE "),
            DBG(sf.description(sf.findFromData(sample_format_of(fmt)), true)),
            DBG(t.name())
        );

        m_supported_formats.append(fmt);
    }
    qDebug("--------------------------------- ");
}

//***************************************************************************
void Kwave::RecordPulseAudio::pa_read_cb(pa_stream *p, size_t nbytes,
                                         void *userdata)
{
    Kwave::RecordPulseAudio *record_plugin =
        reinterpret_cast<Kwave::RecordPulseAudio *>(userdata);
    Q_ASSERT(record_plugin);
    if (record_plugin) record_plugin->notifyRead(p, nbytes);
}

//***************************************************************************
void Kwave::RecordPulseAudio::notifyRead(pa_stream *stream, size_t nbytes)
{
    Q_UNUSED(nbytes)
    Q_ASSERT(stream);

    if (!stream || (stream != m_pa_stream)) return;

    m_mainloop_signal.wakeAll();
}

//***************************************************************************
void Kwave::RecordPulseAudio::pa_stream_state_cb(pa_stream *p, void *userdata)
{
    Kwave::RecordPulseAudio *record_plugin =
        reinterpret_cast<Kwave::RecordPulseAudio *>(userdata);
    Q_ASSERT(record_plugin);
    if (record_plugin) record_plugin->notifyStreamState(p);
}

//***************************************************************************
void Kwave::RecordPulseAudio::notifyStreamState(pa_stream* stream)
{
    Q_ASSERT(stream);
    if (!stream || (stream != m_pa_stream)) return;

    pa_stream_state_t state = pa_stream_get_state(stream);

#ifdef DEBUG
    #define DBG_CASE(x) case x: qDebug("RecordPulseAudio -> " #x ); break
    switch (state)
    {
        DBG_CASE(PA_STREAM_CREATING);
        DBG_CASE(PA_STREAM_UNCONNECTED);
        DBG_CASE(PA_STREAM_FAILED);
        DBG_CASE(PA_STREAM_TERMINATED);
        DBG_CASE(PA_STREAM_READY);
    }
    #undef DBG_CASE
#endif /* DEBUG */

    switch (state) {
        case PA_STREAM_CREATING:
            break;
        case PA_STREAM_UNCONNECTED: /* FALLTHROUGH */
        case PA_STREAM_FAILED:      /* FALLTHROUGH */
        case PA_STREAM_TERMINATED:  /* FALLTHROUGH */
        case PA_STREAM_READY:
            m_mainloop_signal.wakeAll();
            break;
        default:
            Q_ASSERT(0 && "?");
            break;
    }
}

//***************************************************************************
void Kwave::RecordPulseAudio::pa_context_notify_cb(pa_context *c,
                                                   void       *userdata)
{
    Kwave::RecordPulseAudio *record_plugin =
        reinterpret_cast<Kwave::RecordPulseAudio *>(userdata);
    Q_ASSERT(record_plugin);
    if (record_plugin) record_plugin->notifyContext(c);
}

//***************************************************************************
void Kwave::RecordPulseAudio::notifyContext(pa_context *c)
{
    Q_ASSERT(c == m_pa_context);
    const pa_context_state_t state = pa_context_get_state(c);

#ifdef DEBUG
    #define DBG_CASE(x) case x: qDebug("RecordPulseAudio -> " #x ); break
    switch (state)
    {
        DBG_CASE(PA_CONTEXT_UNCONNECTED);
        DBG_CASE(PA_CONTEXT_CONNECTING);
        DBG_CASE(PA_CONTEXT_AUTHORIZING);
        DBG_CASE(PA_CONTEXT_SETTING_NAME);
        DBG_CASE(PA_CONTEXT_READY);
        DBG_CASE(PA_CONTEXT_TERMINATED);
        DBG_CASE(PA_CONTEXT_FAILED);
    }
    #undef DBG_CASE
#endif /* DEBUG */

    switch (state)
    {
        case PA_CONTEXT_UNCONNECTED: /* FALLTHROUGH */
        case PA_CONTEXT_CONNECTING:  /* FALLTHROUGH */
        case PA_CONTEXT_AUTHORIZING: /* FALLTHROUGH */
        case PA_CONTEXT_SETTING_NAME:
            break;
        case PA_CONTEXT_READY:       /* FALLTHROUGH */
        case PA_CONTEXT_TERMINATED:  /* FALLTHROUGH */
        case PA_CONTEXT_FAILED:
            m_mainloop_signal.wakeAll();
            break;
        DEFAULT_IGNORE;
    }
}

//***************************************************************************
pa_sample_format_t Kwave::RecordPulseAudio::mode2format(
    int compression, int bits, Kwave::SampleFormat::Format sample_format)
{
    // loop over all supported formats and keep only those that are
    // compatible with the given compression, bits and sample format
    foreach (const pa_sample_format_t &fmt, m_supported_formats)
    {
        if (compression_of(fmt) != compression) continue;
        if (bits_of(fmt) != bits) continue;
        if (!(sample_format_of(fmt) == sample_format)) continue;

        // mode is compatible
        // As the list of known formats is already sorted so that
        // the simplest formats come first, we don't have a lot
        // of work -> just take the first entry ;-)
        return fmt;
    }

    qWarning("RecordPulesAudio::mode2format -> no match found !?");
    return PA_SAMPLE_INVALID;
}

//***************************************************************************
Kwave::byte_order_t Kwave::RecordPulseAudio::endianness()
{
    pa_sample_format_t fmt = mode2format(m_compression, m_bits_per_sample,
                                         m_sample_format);
    return (fmt != PA_SAMPLE_INVALID) ?
        endian_of(fmt) : Kwave::UnknownEndian;
}

//***************************************************************************
Kwave::SampleFormat::Format Kwave::RecordPulseAudio::sampleFormat()
{
    return m_sample_format;
}

//***************************************************************************
int Kwave::RecordPulseAudio::setSampleFormat(
    Kwave::SampleFormat::Format new_format)
{
    if (m_sample_format == new_format)
        return 0;
    close();
    m_sample_format = new_format;
    return 0;
}

//***************************************************************************
QList<Kwave::SampleFormat::Format>
    Kwave::RecordPulseAudio::detectSampleFormats()
{
    QList<Kwave::SampleFormat::Format> list;

    // try all known sample formats
    foreach (const pa_sample_format_t &fmt, m_supported_formats)
    {
        const Kwave::SampleFormat::Format sample_format = sample_format_of(fmt);

        // only accept bits/sample if compression types
        // and bits per sample match
        if (compression_of(fmt) != m_compression) continue;
        if (bits_of(fmt) != Kwave::toInt(m_bits_per_sample))
            continue;

        // do not produce duplicates
        if (list.contains(sample_format)) continue;

        list.append(sample_format);
    }

    return list;
}

//***************************************************************************
int Kwave::RecordPulseAudio::bitsPerSample()
{
    return m_bits_per_sample;
}

//***************************************************************************
int Kwave::RecordPulseAudio::setBitsPerSample(unsigned int new_bits)
{
    if (m_bits_per_sample == new_bits)
        return 0;
    close();
    m_bits_per_sample = new_bits;
    return 0;
}

//***************************************************************************
QList< unsigned int > Kwave::RecordPulseAudio::supportedBits()
{
    QList<unsigned int> list;

    // try all known sample formats
    foreach(const pa_sample_format_t &fmt, m_supported_formats)
    {
        const unsigned int bits = bits_of(fmt);

        // 0  bits means invalid/does not apply
        if (!bits) continue;

        // only accept bits/sample if compression matches
        if (compression_of(fmt) != m_compression) continue;

        // do not produce duplicates
        if (list.contains(bits)) continue;

        list.append(bits);
    }

    return list;
}

//***************************************************************************
Kwave::Compression::Type Kwave::RecordPulseAudio::compression()
{
    return m_compression;
}

//***************************************************************************
int Kwave::RecordPulseAudio::setCompression(
    Kwave::Compression::Type new_compression
)
{
    if (m_compression != new_compression) {
        close();
        m_compression = new_compression;
    }
    return 0;
}

//***************************************************************************
QList<Kwave::Compression::Type> Kwave::RecordPulseAudio::detectCompressions()
{
    QList<Kwave::Compression::Type> list;

    // try all known sample formats
    foreach (const pa_sample_format_t &fmt, m_supported_formats)
    {
        Kwave::Compression::Type compression = compression_of(fmt);

        // do not produce duplicates
        if (list.contains(compression)) continue;

        Kwave::Compression t(compression);
        list.append(compression);
    }

    return list;
}

//***************************************************************************
double Kwave::RecordPulseAudio::sampleRate()
{
    return m_rate;
}

//***************************************************************************
int Kwave::RecordPulseAudio::setSampleRate(double& new_rate)
{
    if (qFuzzyCompare(new_rate, m_rate))
        return 0;
    close();
    m_rate = new_rate;
    return 0;
}

//***************************************************************************
QList< double > Kwave::RecordPulseAudio::detectSampleRates()
{
    QList<double> list;

    static const unsigned int known_rates[] = {
          1,
          1000, // (just for testing)
          2000, // (just for testing)
          4000, // standard OSS
          5125, // seen in Harmony driver (HP712, 715/new)
          5510, // seen in AD1848 driver
          5512, // seen in ES1370 driver
          6215, // seen in ES188X driver
          6615, // seen in Harmony driver (HP712, 715/new)
          6620, // seen in AD1848 driver
          7350, // seen in AWACS and Burgundy sound driver
          8000, // standard OSS
          8820, // seen in AWACS and Burgundy sound driver
          9600, // seen in AD1848 driver
         11025, // soundblaster
         14700, // seen in AWACS and Burgundy sound driver
         16000, // standard OSS
         17640, // seen in AWACS and Burgundy sound driver
         18900, // seen in Harmony driver (HP712, 715/new)
         22050, // soundblaster
         24000, // seen in NM256 driver
         27428, // seen in Harmony driver (HP712, 715/new)
         29400, // seen in AWACS and Burgundy sound driver
         32000, // standard OSS
         32768, // seen in CS4299 driver
         33075, // seen in Harmony driver (HP712, 715/new)
         37800, // seen in Harmony driver (HP712, 715/new)
         44100, // soundblaster
         48000, // AC97
         64000, // AC97
         88200, // seen in RME96XX driver
         96000, // AC97
        128000, // (just for testing)
        192000 // AC97
    };

    pa_sample_spec sampleSpec = m_device_list[m_device].m_sample_spec;
    uint32_t rate = sampleSpec.rate;
    for (unsigned int i = 0; i < ELEMENTS_OF(known_rates); i++) {
        if(known_rates[i] <= rate) {
            list.append(known_rates[i]);
        }
    }

    return list;
}

//***************************************************************************
int Kwave::RecordPulseAudio::tracks()
{
    return m_tracks;
}

//***************************************************************************
int Kwave::RecordPulseAudio::setTracks(unsigned int &tracks)
{
    const quint8 max_tracks = std::numeric_limits<quint8>::max();

    if (tracks > max_tracks) {
        tracks = max_tracks;
        return -1;
    }

    if (tracks == m_tracks)
        return 0;

    close();
    m_tracks = static_cast<quint8>(tracks);

    return 0;
}

//***************************************************************************
int Kwave::RecordPulseAudio::detectTracks(unsigned int &min, unsigned int &max)
{
    pa_sample_spec sampleSpec = m_device_list[m_device].m_sample_spec;
    unsigned int channels = sampleSpec.channels;

    min = 1;
    max = qBound<unsigned int>(min, channels, PA_CHANNELS_MAX);
    return 0;
}

//***************************************************************************
int Kwave::RecordPulseAudio::close()
{
    if (m_pa_stream) {
        pa_stream_drop(m_pa_stream);

        m_mainloop_lock.lock();
        pa_stream_disconnect(m_pa_stream);
        qDebug("RecordPulseAudio::close() - waiting for stream disconnect...");
        m_mainloop_signal.wait(&m_mainloop_lock, TIMEOUT_DISCONNECT_STREAM);
        m_mainloop_lock.unlock();
        qDebug("RecordPulseAudio::close() - stream disconnect DONE");

        pa_stream_unref(m_pa_stream);
    }
    m_pa_stream = nullptr;

    // we need to re-initialize the next time
    m_initialized = false;
    return 0;
}

//***************************************************************************
int Kwave::RecordPulseAudio::read(QByteArray& buffer, unsigned int offset)
{
    if (buffer.isNull() || buffer.isEmpty())
        return 0; // no buffer, nothing to do

    unsigned int length = static_cast<unsigned int>(buffer.size());

    // we configure our device at a late stage, not on the fly like in OSS
    if (!m_initialized) {
        int err = initialize(length);
        if (err < 0) return err;
    }

    m_mainloop_lock.lock();

    size_t freeBytes = length - offset;
    size_t readableSize = pa_stream_readable_size(m_pa_stream);
    if (readableSize > freeBytes) {
        size_t additional_size = readableSize - freeBytes;
        buffer.resize(length + additional_size);
    }

    size_t readLength = 0;
    if (readableSize > 0) {
        const void *audioBuffer = nullptr;
        pa_stream_peek(m_pa_stream, &audioBuffer, &readLength);

        if (offset + readLength > Kwave::toUint(buffer.length())) {
            pa_stream_drop(m_pa_stream);
            m_mainloop_lock.unlock();
            return -EIO; // peek returned invalid length
        }

        char *data = buffer.data() + offset;
        if (audioBuffer) {
            MEMCPY(data, audioBuffer, readLength); // real data
        } else {
            memset(data, 0x00, readLength); // there was a gap
        }

        pa_stream_drop(m_pa_stream);

    } else {
        m_mainloop_lock.unlock();
        return -EAGAIN;
    }
    m_mainloop_lock.unlock();

    return Kwave::toInt(readLength);
}

//***************************************************************************
int Kwave::RecordPulseAudio::initialize(uint32_t buffer_size)
{
    Q_ASSERT(!m_initialized);

    // make sure that we are connected to the sound server
    if (!connectToServer()) {
        qWarning("Connecting to the PulseAudio server failed!");
        return -1;
    }

    pa_sample_format_t fmt = mode2format(m_compression, m_bits_per_sample,
                                         m_sample_format);
    if (fmt == PA_SAMPLE_INVALID) {
        Kwave::SampleFormat::Map sf;

        qWarning("format: no matching format for compression '%s', "
            "%u bits/sample, format '%s'",
            DBG(sf.description(sf.findFromData(m_sample_format), true)),
            m_bits_per_sample,
            DBG(Kwave::Compression(m_compression).name())
        );
        return -EINVAL;
    }

    pa_sample_spec sample_spec;
    sample_spec.channels = m_tracks;
    sample_spec.format = fmt;
    sample_spec.rate = static_cast<quint32>(m_rate);

    if(!pa_sample_spec_valid(&sample_spec)) {
        Kwave::SampleFormat::Map sf;

        qWarning("no valid pulse audio format: %d, rate: %0.3g, channels: %d",
                 static_cast<int>(fmt), m_rate, m_tracks);
        return -EINVAL;
    }

    // run with mainloop locked from here on...
    m_mainloop_lock.lock();

    pa_channel_map channel_map;
    pa_channel_map_init_extend(&channel_map, sample_spec.channels,
                               PA_CHANNEL_MAP_DEFAULT);

    if (!pa_channel_map_compatible(&channel_map, &sample_spec)) {
        qWarning("Channel map doesn't match sample specification!");
    }

    // create a new stream
    m_pa_stream = pa_stream_new(
        m_pa_context,
        m_name.toUtf8().constData(),
        &sample_spec,
        &channel_map);

    if (!m_pa_stream) {
        m_mainloop_lock.unlock();
        qWarning("Failed to create a PulseAudio stream %s",
                 pa_strerror(pa_context_errno(m_pa_context)));
        return -1;
    }

    pa_stream_set_state_callback(m_pa_stream, pa_stream_state_cb, this);
    pa_stream_set_read_callback(m_pa_stream, pa_read_cb, this);

    pa_buffer_attr attr;
    attr.fragsize  = buffer_size;
    attr.maxlength = static_cast<uint32_t>(-1);
    attr.minreq    = static_cast<uint32_t>(-1);
    attr.prebuf    = static_cast<uint32_t>(-1);
    attr.tlength   = static_cast<uint32_t>(-1);
    int flags      = PA_STREAM_ADJUST_LATENCY;

    // connect the stream in record mode
    int result = pa_stream_connect_record(
        m_pa_stream,
        m_pa_device.toUtf8().constData(),
        &attr,
        static_cast<pa_stream_flags_t>(flags));

    if (result >= 0) {
        m_mainloop_signal.wait(&m_mainloop_lock, TIMEOUT_CONNECT_RECORD);
        if (pa_stream_get_state(m_pa_stream) != PA_STREAM_READY)
            result = -1;
    }

    m_mainloop_lock.unlock();

    if (result < 0) {
        pa_stream_unref(m_pa_stream);
        m_pa_stream = nullptr;
        qWarning("Failed to open a PulseAudio stream for record %s",
                 pa_strerror(pa_context_errno(m_pa_context)));
        return -1;
    }

    m_initialized = true;
    return 0;
}

//***************************************************************************
QString Kwave::RecordPulseAudio::open(const QString& device)
{
    // close the previous device
    if (m_pa_stream) close();

    QString pa_device;
    if (m_device_list.contains(device))
        pa_device = m_device_list[device].m_name;

    if (!pa_device.length())
        return QString::number(ENODEV);

    m_pa_device = pa_device;
    m_device    = device;

    // detect all formats the device knows
    detectSupportedFormats(device);

    return QString();
}

//***************************************************************************
QStringList Kwave::RecordPulseAudio::supportedDevices()
{
    QStringList list;

    // re-validate the list if necessary
    scanDevices();

    if (!m_pa_mainloop || !m_pa_context) return list;

    list = m_device_list.keys();
    if (!list.isEmpty()) list.prepend(_("#TREE#"));

    return list;
}

//***************************************************************************
void Kwave::RecordPulseAudio::run_wrapper(const QVariant &params)
{
    Q_UNUSED(params)
    m_mainloop_lock.lock();
    pa_mainloop_run(m_pa_mainloop, nullptr);
    m_mainloop_lock.unlock();
    qDebug("RecordPulseAudio::run_wrapper - done.");
}

//***************************************************************************
static int poll_func(struct pollfd *ufds, unsigned long nfds,
                     int timeout, void *userdata)
{
    Kwave::RecordPulseAudio *dev =
        static_cast<Kwave::RecordPulseAudio *>(userdata);
    Q_ASSERT(dev);
    if (!dev) return -1;

    return dev->mainloopPoll(ufds, nfds, timeout);
}

//***************************************************************************
int Kwave::RecordPulseAudio::mainloopPoll(struct pollfd *ufds,
                                            unsigned long int nfds,
                                            int timeout)
{
    m_mainloop_lock.unlock();
    int retval = poll(ufds, nfds, timeout);
    m_mainloop_lock.lock();

    return retval;
}

//***************************************************************************
bool Kwave::RecordPulseAudio::connectToServer()
{
    if (m_pa_context) return true; // already connected

    // set hourglass cursor, we are waiting...
    QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));

    // create a property list for this application
    m_pa_proplist = pa_proplist_new();
    Q_ASSERT(m_pa_proplist);

    pa_proplist_sets(m_pa_proplist, PA_PROP_APPLICATION_LANGUAGE,
                     UTF8(QLocale::system().name()));
    pa_proplist_sets(m_pa_proplist, PA_PROP_APPLICATION_NAME,
                     UTF8(qApp->applicationName()));
    pa_proplist_sets(m_pa_proplist, PA_PROP_APPLICATION_ICON_NAME,
                     "kwave");
    pa_proplist_sets(m_pa_proplist, PA_PROP_APPLICATION_PROCESS_BINARY,
                     "kwave");
    pa_proplist_setf(m_pa_proplist, PA_PROP_APPLICATION_PROCESS_ID,
                    "%ld", static_cast<long int>(qApp->applicationPid()));
    KUser user;
    pa_proplist_sets(m_pa_proplist, PA_PROP_APPLICATION_PROCESS_USER,
                     UTF8(user.loginName()));
    pa_proplist_sets(m_pa_proplist, PA_PROP_APPLICATION_VERSION,
                     UTF8(qApp->applicationVersion()));

    pa_proplist_sets(m_pa_proplist, PA_PROP_MEDIA_ROLE, "production");

    // ignore SIGPIPE in this context
#ifdef HAVE_SIGNAL_H
    signal(SIGPIPE, SIG_IGN);
#endif

    m_pa_mainloop = pa_mainloop_new();
    Q_ASSERT(m_pa_mainloop);
    pa_mainloop_set_poll_func(m_pa_mainloop, poll_func, this);

    m_pa_context = pa_context_new_with_proplist(
        pa_mainloop_get_api(m_pa_mainloop),
        "Kwave",
        m_pa_proplist
    );

    // set the callback for getting informed about the context state
    pa_context_set_state_callback(m_pa_context, pa_context_notify_cb, this);

    // connect to the pulse audio server server
    bool failed = false;
    int error = pa_context_connect(
        m_pa_context,                       // context
        nullptr,                          // server
        static_cast<pa_context_flags_t>(0), // flags
        nullptr                           // API
    );
    if (error < 0)
    {
        qWarning("RecordPulseAudio: pa_contect_connect failed (%s)",
                  pa_strerror(pa_context_errno(m_pa_context)));
        failed = true;
    }

    if (!failed) {
        m_mainloop_lock.lock();
        m_mainloop_thread.start();

        // wait until the context state is either connected or failed
        failed = true;
        if ( m_mainloop_signal.wait(&m_mainloop_lock,
                                    TIMEOUT_CONNECT_TO_SERVER) )
        {
            if (pa_context_get_state(m_pa_context) == PA_CONTEXT_READY) {
                failed = false;
            }
        }
        m_mainloop_lock.unlock();

        if (failed) {
            qWarning("RecordPulseAudio: context FAILED (%s):-(",
                     pa_strerror(pa_context_errno(m_pa_context)));
        }
    }

    // if the connection failed, clean up
    if (failed) {
        disconnectFromServer();
    }

    QApplication::restoreOverrideCursor();

    return !failed;
}

//***************************************************************************
void Kwave::RecordPulseAudio::disconnectFromServer()
{
    close();

    // stop the main loop
    m_mainloop_thread.isInterruptionRequested();
    if (m_pa_mainloop) {
        m_mainloop_lock.lock();
        pa_mainloop_quit(m_pa_mainloop, 0);
        m_mainloop_lock.unlock();
    }
    m_mainloop_thread.stop();

    // disconnect the pulse context
    if (m_pa_context) {
        pa_context_disconnect(m_pa_context);
        pa_context_unref(m_pa_context);
        m_pa_context  = nullptr;
    }

    // stop and free the main loop
    if (m_pa_mainloop) {
        pa_mainloop_free(m_pa_mainloop);
        m_pa_mainloop = nullptr;
    }

    // release the property list
    if (m_pa_proplist) {
        pa_proplist_free(m_pa_proplist);
        m_pa_proplist = nullptr;
    }

}

//***************************************************************************
void Kwave::RecordPulseAudio::pa_source_info_cb(pa_context *c,
                                                const pa_source_info *info,
                                                int eol, void *userdata)
{
    Kwave::RecordPulseAudio *record_plugin =
        reinterpret_cast<Kwave::RecordPulseAudio *>(userdata);
    Q_ASSERT(record_plugin);
    if (record_plugin) record_plugin->notifySourceInfo(c, info, eol);
}

//***************************************************************************
void Kwave::RecordPulseAudio::notifySourceInfo(pa_context *c,
                                               const pa_source_info *info,
                                               int eol)
{
    Q_UNUSED(c)
    Q_ASSERT(c == m_pa_context);

    if (eol == 0) {
        source_info_t i;
        i.m_name        = QString::fromUtf8(info->name);
        i.m_description = QString::fromUtf8(info->description);
        i.m_driver      = QString::fromUtf8(info->driver);
        i.m_card        = info->card;
        i.m_sample_spec = info->sample_spec;

        QString name    = QString::number(m_device_list.count());
        m_device_list[name] = i;
    } else {
        m_mainloop_signal.wakeAll();
    }
}

//***************************************************************************
void Kwave::RecordPulseAudio::scanDevices()
{
    if (!m_pa_context) connectToServer();
    if (!m_pa_context) return;

    // fetch the device list from the PulseAudio server
    m_mainloop_lock.lock();
    m_device_list.clear();
    const pa_operation *op_source_info = pa_context_get_source_info_list(
        m_pa_context,
        pa_source_info_cb,
        this
    );
    if (op_source_info) {
        // set hourglass cursor, we have a long timeout...
        QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
        m_mainloop_signal.wait(&m_mainloop_lock, TIMEOUT_WAIT_DEVICE_SCAN);
        QApplication::restoreOverrideCursor();
    }

    // create a list with final names
    QMap<QString, source_info_t> list;
    for (QMap<QString, source_info_t>::const_iterator it =
         m_device_list.constBegin();
         it != m_device_list.constEnd(); ++it)
    {
        const QString       &source     = it.key();
        const source_info_t &inf        = it.value();
        const QString       &name       = inf.m_name;
        QString             description = inf.m_description;
        QString             driver      = inf.m_driver;

        // if the name is not unique, add the internal source name
        for (QMap<QString, source_info_t>::const_iterator it2 =
             m_device_list.constBegin();
             it2 != m_device_list.constEnd(); ++it2)
        {
            const QString       &s = it2.key();
            const source_info_t &i = it2.value();
            if (s == source) continue;
            if ((i.m_description == description) &&
                (i.m_driver      == driver))
            {
                // not unique
                description += _(" [") + name + _("]");
                break;
            }
        }

        // mangle the driver name, e.g.
        // "module-alsa-sink.c" -> "alsa sink"
        QFileInfo f(driver);
        driver = f.baseName();
        driver.replace(_("-"), _(" "));
        driver.replace(_("_"), _(" "));
        if (driver.startsWith(_("module "), Qt::CaseInsensitive))
            driver.remove(0, 7);
        description.prepend(driver + _("|sound_card||"));

        // add the leaf node
        if (inf.m_card != PA_INVALID_INDEX)
            description.append(_("|sound_device"));
        else
            description.append(_("|sound_note"));

        list.insert(description, *it);
    }

    m_device_list.clear();
    m_device_list = list;
    m_mainloop_lock.unlock();
}

#endif /* HAVE_PULSEAUDIO_SUPPORT */

//***************************************************************************
//***************************************************************************
