/***************************************************************************
       PlayBack-PulseAudio.cpp  -  playback device for PulseAudio
                             -------------------
    begin                : Tue Sep 29 2009
    copyright            : (C) 2009 by Thomas Eschenbacher
    email                : Thomas.Eschenbacher@gmx.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 <math.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>

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

#include <KLocalizedString>
#include <KUser>

#include "libkwave/FileInfo.h"
#include "libkwave/String.h"
#include "libkwave/Utils.h"
#include "libkwave/memcpy.h"

#include "PlayBack-PulseAudio.h"

/**
 * 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 playback [ms]
 * @see open()
 */
#define TIMEOUT_CONNECT_PLAYBACK 10000

/**
 * minimum timeout for flush() [ms]
 * @see flush()
 */
#define TIMEOUT_MIN_FLUSH 1000

/**
 * minimum timeout for drain() [ms]
 * @see close()
 */
#define TIMEOUT_MIN_DRAIN 3000

//***************************************************************************
Kwave::PlayBackPulseAudio::PlayBackPulseAudio(const Kwave::FileInfo &info)
    :Kwave::PlayBackDevice(),
     m_mainloop_thread(this, QVariant()),
     m_mainloop_lock(),
     m_mainloop_signal(),
     m_info(info),
     m_rate(0),
     m_bytes_per_sample(0),
     m_buffer(nullptr),
     m_buffer_size(0),
     m_buffer_used(0),
     m_bufbase(10),
     m_pa_proplist(nullptr),
     m_pa_mainloop(nullptr),
     m_pa_context(nullptr),
     m_pa_stream(nullptr),
     m_device_list()
{
}

//***************************************************************************
Kwave::PlayBackPulseAudio::~PlayBackPulseAudio()
{
    close();
}

//***************************************************************************
void Kwave::PlayBackPulseAudio::pa_context_notify_cb(pa_context *c, void *data)
{
    Kwave::PlayBackPulseAudio *playback_plugin =
        reinterpret_cast<Kwave::PlayBackPulseAudio *>(data);
    Q_ASSERT(playback_plugin);
    if (playback_plugin) playback_plugin->notifyContext(c);
}

//***************************************************************************
void Kwave::PlayBackPulseAudio::pa_sink_info_cb(pa_context *c,
                                                const pa_sink_info *info,
                                                int eol, void *userdata)
{
    Kwave::PlayBackPulseAudio *playback_plugin =
        reinterpret_cast<Kwave::PlayBackPulseAudio *>(userdata);
    Q_ASSERT(playback_plugin);
    if (playback_plugin) playback_plugin->notifySinkInfo(c, info, eol);
}

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

//***************************************************************************
void Kwave::PlayBackPulseAudio::pa_write_cb(pa_stream *p, size_t nbytes,
                                            void *userdata)
{
    Kwave::PlayBackPulseAudio *playback_plugin =
        reinterpret_cast<Kwave::PlayBackPulseAudio *>(userdata);
    Q_ASSERT(playback_plugin);
    if (playback_plugin) playback_plugin->notifyWrite(p, nbytes);
}

//***************************************************************************
void Kwave::PlayBackPulseAudio::pa_stream_success_cb(pa_stream *s,
                                                     int success,
                                                     void *userdata)
{
    Kwave::PlayBackPulseAudio *playback_plugin =
        reinterpret_cast<Kwave::PlayBackPulseAudio *>(userdata);
    Q_ASSERT(playback_plugin);
    if (playback_plugin) playback_plugin->notifySuccess(s, success);
}

//***************************************************************************
void Kwave::PlayBackPulseAudio::notifyContext(pa_context *c)
{
    Q_ASSERT(c == m_pa_context);
    switch (pa_context_get_state(c))
    {
        case PA_CONTEXT_UNCONNECTED:
//          qDebug("PlayBackPulseAudio: PA_CONTEXT_UNCONNECTED!?");
            break;
        case PA_CONTEXT_CONNECTING:
//          qDebug("PlayBackPulseAudio: PA_CONTEXT_CONNECTING...");
            break;
        case PA_CONTEXT_AUTHORIZING:
//          qDebug("PlayBackPulseAudio: PA_CONTEXT_AUTHORIZING...");
            break;
        case PA_CONTEXT_SETTING_NAME:
//          qDebug("PlayBackPulseAudio: PA_CONTEXT_SETTING_NAME...");
            break;
        case PA_CONTEXT_READY:
//          qDebug("PlayBackPulseAudio: PA_CONTEXT_READY.");
            m_mainloop_signal.wakeAll();
            break;
        case PA_CONTEXT_TERMINATED:
            qWarning("PlayBackPulseAudio: PA_CONTEXT_TERMINATED");
            m_mainloop_signal.wakeAll();
            break;
        case PA_CONTEXT_FAILED:
            qWarning("PlayBackPulseAudio: PA_CONTEXT_FAILED");
            m_mainloop_signal.wakeAll();
            break;
        DEFAULT_IGNORE;
    }
}

//***************************************************************************
void Kwave::PlayBackPulseAudio::notifySinkInfo(pa_context *c,
                                               const pa_sink_info *info,
                                               int eol)
{
    Q_UNUSED(c)
    Q_ASSERT(c == m_pa_context);
    if (eol == 0) {
#if 0
        qDebug("PlayBackPulseAudio: [%d] sink='%s' (%s) driver='%s'"\
               "card=%d, ports=%d",
               info->index,
               info->name,
               info->description,
               info->driver,
               info->card,
               info->n_ports
        );
        for (int p = 0; p < info->n_ports; p++) {
            qDebug("                     [%2d] - '%s' (%s), prio=%d%s",
                   p,
                   info->ports[p]->name,
                   info->ports[p]->description,
                   info->ports[p]->priority,
                   (info->ports[p] == info->active_port) ? " <*>" : ""
            );
        }
#endif
        sink_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::PlayBackPulseAudio::notifyStreamState(pa_stream* stream)
{
    Q_ASSERT(stream);
    if (!stream || (stream != m_pa_stream)) return;

    pa_stream_state_t state = pa_stream_get_state(stream);
//     switch (state) {
//      case PA_STREAM_UNCONNECTED:
//          qDebug("    -> UNCONNECTED"); break;
//      case PA_STREAM_CREATING:
//          qDebug("    -> CREATING"); break;
//      case PA_STREAM_READY:
//          qDebug("    -> READY"); break;
//      case PA_STREAM_FAILED:
//          qDebug("    -> FAILED"); break;
//      case PA_STREAM_TERMINATED:
//          qDebug("    -> TERMINATED"); break;
//      default:
//          Q_ASSERT(0 && "?");
//          qDebug("    -> ???"); break;
//     }
    switch (state) {
        case PA_STREAM_UNCONNECTED:
        case PA_STREAM_CREATING:
            break;
        case PA_STREAM_READY:
        case PA_STREAM_FAILED:
        case PA_STREAM_TERMINATED:
            m_mainloop_signal.wakeAll();
            break;
        DEFAULT_IGNORE;
    }
}

//***************************************************************************
void Kwave::PlayBackPulseAudio::notifyWrite(pa_stream *stream, size_t nbytes)
{
//     qDebug("PlayBackPulseAudio::notifyWrite(stream=%p, nbytes=%u)",
//         static_cast<void *>(stream), nbytes);
    Q_UNUSED(nbytes)

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

    m_mainloop_signal.wakeAll();
}

//***************************************************************************
void Kwave::PlayBackPulseAudio::notifySuccess(pa_stream* stream, int success)
{
//     qDebug("PlayBackPulseAudio::notifySuccess(stream=%p, success=%d)",
//         static_cast<void *>(stream), success);
    Q_UNUSED(success)

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

    m_mainloop_signal.wakeAll();
}

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

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

//***************************************************************************
int Kwave::PlayBackPulseAudio::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::PlayBackPulseAudio::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("PlayBackPulseAudio: 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) {
                qDebug("PlayBackPulseAudio: context is ready :-)");
                failed = false;
            }
        }
        m_mainloop_lock.unlock();

        if (failed) {
            qWarning("PlayBackPulseAudio: 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::PlayBackPulseAudio::disconnectFromServer()
{
    // stop the main loop
    m_mainloop_thread.requestInterruption();
    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;
        qDebug("PlayBackPulseAudio: mainloop freed");
    }

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

}

//***************************************************************************
QString Kwave::PlayBackPulseAudio::open(const QString &device, double rate,
                                        unsigned int channels,
                                        unsigned int bits,
                                        unsigned int bufbase)
{
    #define SET_PROPERTY(__property__,__info__)              \
        if (m_info.contains(__info__))                       \
            pa_proplist_sets(_proplist, __property__,        \
            m_info.get(__info__).toString().toUtf8().data())

    qDebug("PlayBackPulseAudio::open(device=%s,rate=%0.1f,channels=%u,"\
        "bits=%u, bufbase=%u)",
        DBG(device.split(_("|")).at(0)), rate, channels,
        bits, bufbase);

    m_rate = rate;

    if (channels > 255)
        return i18n("%1 channels are not supported, maximum is 255", channels);

    // close the previous device
    if (m_pa_stream) close();

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

    if (!m_device_list.contains(device)) scanDevices();
    if (!m_device_list.contains(device)) {
        return i18n(
            "The PulseAudio device '%1' is unknown or no longer connected",
            device.section(QLatin1Char('|'), 0, 0));
    }
    QString pa_device = m_device_list[device].m_name;

    // determine the buffer size
    m_bytes_per_sample = sizeof(sample_t) * channels;
    m_buffer_size      = 0;
    m_buffer           = nullptr;
    m_bufbase          = bufbase;

    // build a property list for the stream
    pa_proplist *_proplist = pa_proplist_copy(m_pa_proplist);
    Q_ASSERT(_proplist);
    SET_PROPERTY(PA_PROP_MEDIA_TITLE,     Kwave::INF_NAME);
    SET_PROPERTY(PA_PROP_MEDIA_ARTIST,    Kwave::INF_AUTHOR);
#ifdef PA_PROP_MEDIA_COPYRIGHT
    SET_PROPERTY(PA_PROP_MEDIA_COPYRIGHT, Kwave::INF_COPYRIGHT);
#endif
#ifdef PA_PROP_MEDIA_SOFTWARE
    SET_PROPERTY(PA_PROP_MEDIA_SOFTWARE,  Kwave::INF_SOFTWARE);
#endif
//  SET_PROPERTY(PA_PROP_MEDIA_LANGUAGE,  Kwave::INF_...);
    SET_PROPERTY(PA_PROP_MEDIA_FILENAME,  Kwave::INF_FILENAME);
//  SET_PROPERTY(PA_PROP_MEDIA_ICON_NAME, Kwave::INF_...);

    // use Kwave's internal sample format as output
    pa_sample_spec sample_spec;
#if (Q_BYTE_ORDER == Q_BIG_ENDIAN)
    sample_spec.format = PA_SAMPLE_S24_32BE;
#else
    sample_spec.format = PA_SAMPLE_S24_32LE;
#endif
    sample_spec.channels = static_cast<uint8_t>(channels);
    sample_spec.rate     = static_cast<uint32_t>(m_rate);

    // use the current title / filename or fixed string as stream name
    QString name;
    if (m_info.contains(Kwave::INF_NAME)) // first choice: title
        name = m_info.get(Kwave::INF_NAME).toString();
    // fallback: filename
    if (!name.length() && m_info.contains(Kwave::INF_FILENAME))
        name = m_info.get(Kwave::INF_FILENAME).toString();
    if (!name.length()) // last resort: fixed string
        name = i18n("playback...");

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

    // create a new stream
    m_pa_stream = pa_stream_new_with_proplist(
        m_pa_context,
        name.toUtf8().data(),
        &sample_spec,
        nullptr /* const pa_channel_map *map */,
        _proplist);
    pa_proplist_free(_proplist);

    if (!m_pa_stream) {
        m_mainloop_lock.unlock();
        return i18n("Failed to create a PulseAudio stream (%1).",
                    QString::fromLocal8Bit(
                    pa_strerror(pa_context_errno(m_pa_context))));
    }
    qDebug("PlayBackPulseAudio::open(...) - stream created as %p",
           static_cast<void *>(m_pa_stream));

    // register callbacks for changes in stream state and write events
    pa_stream_set_state_callback(m_pa_stream, pa_stream_state_cb, this);
    pa_stream_set_write_callback(m_pa_stream, pa_write_cb, this);

    // set buffer attributes
    if (m_bufbase < 10) m_bufbase = 10;
    const int s = ((1 << m_bufbase) * m_bytes_per_sample) / m_bytes_per_sample;
    pa_buffer_attr attr;
    attr.fragsize  = -1;
    attr.maxlength =  s;
    attr.minreq    = -1;
    attr.prebuf    = -1;
    attr.tlength   = -1;

    // connect the stream in playback mode
    int result = pa_stream_connect_playback(
        m_pa_stream,
        pa_device.length() ? pa_device.toUtf8().data() : nullptr,
        &attr /* buffer attributes */,
        static_cast<pa_stream_flags_t>(
            PA_STREAM_INTERPOLATE_TIMING |
            PA_STREAM_AUTO_TIMING_UPDATE),
        nullptr /* volume */,
        nullptr /* sync stream */ );

    if (result >= 0) {
        m_mainloop_signal.wait(&m_mainloop_lock, TIMEOUT_CONNECT_PLAYBACK);
        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;
        return i18n("Failed to open a PulseAudio stream for playback (%1).",
                    QString::fromLocal8Bit(
                    pa_strerror(pa_context_errno(m_pa_context))));
    }

    return QString();
}

//***************************************************************************
int Kwave::PlayBackPulseAudio::write(const Kwave::SampleArray &samples)
{
    unsigned int bytes = m_bytes_per_sample;

    // abort if byte per sample is unknown
    Q_ASSERT(m_bytes_per_sample);
    Q_ASSERT(m_pa_mainloop);
    if (!m_bytes_per_sample || !m_pa_mainloop)
        return -EINVAL;

    // check buffer existence and size changes
    size_t current_buffer_size = (1 << m_bufbase) * m_bytes_per_sample;
    if (!m_buffer || (m_buffer_size != current_buffer_size)) {

        // get a buffer from heap (malloc once at the start is fast enough)
        m_buffer      = (m_buffer) ? realloc(m_buffer, current_buffer_size) :
                                     malloc(current_buffer_size);
        m_buffer_size = current_buffer_size;
    }

    // abort with out-of-memory if failed
    Q_ASSERT(m_buffer);
    if (!m_buffer || !m_buffer_size)
        return -ENOMEM;

    Q_ASSERT (m_buffer_used + bytes <= m_buffer_size);
    if (m_buffer_used + bytes > m_buffer_size) {
        qWarning("PlayBackPulseAudio::write(): buffer overflow ?! (%u/%u)",
                 Kwave::toUint(m_buffer_used),
                 Kwave::toUint(m_buffer_size));
        m_buffer_used = 0;
        return -EIO;
    }

    // copy the samples
    MEMCPY(reinterpret_cast<quint8 *>(m_buffer) + m_buffer_used,
           samples.constData(), bytes);
    m_buffer_used += bytes;

    // write the buffer if it is full
    if (m_buffer_used >= m_buffer_size) return flush();
    return 0;
}

//***************************************************************************
int Kwave::PlayBackPulseAudio::flush()
{
    if (!m_buffer || !m_buffer_size)
        return 0;
//     qDebug("PlayBackPulseAudio::flush(): using buffer %p (%u bytes)",
//             m_buffer, m_buffer_size);

    // calculate a reasonable time for the timeout (16 buffers)
    int samples_per_buffer = Kwave::toInt(
        m_buffer_size / m_bytes_per_sample
    );
    int ms = (!qFuzzyIsNull(m_rate)) ?
        Kwave::toInt((samples_per_buffer * 1000.0) / m_rate) : 0;
    int timeout = (ms + 1) * 16;
    if (timeout < TIMEOUT_MIN_FLUSH)
        timeout = TIMEOUT_MIN_FLUSH;

    // write out the buffer allocated before in "write"
    int result = 0;

    // start with read pointer at the start of m_buffer
    void *buffer_ptr = m_buffer;

    while (m_buffer_used && m_pa_mainloop) {
        size_t len;

        m_mainloop_lock.lock();
        while (!(len = pa_stream_writable_size(m_pa_stream))) {
            if (!PA_CONTEXT_IS_GOOD(pa_context_get_state(m_pa_context)) ||
                !PA_STREAM_IS_GOOD(pa_stream_get_state(m_pa_stream)) ||
                (static_cast<ssize_t>(len) == -1) )
            {
                qWarning("PlayBackPulseAudio::flush(): bad stream state");
                result = -1;
                break;
            }
            if (!m_mainloop_signal.wait(&m_mainloop_lock, timeout)) {
                qWarning("PlayBackPulseAudio::flush(): timed out after %u ms",
                         timeout);
                result = -1;
                break;
            }
        }
        if (result < 0) {
            m_mainloop_lock.unlock();
            break;
        }

        if (len > m_buffer_used) len = m_buffer_used;

//      qDebug("PlayBackPulseAudio::flush(): writing %u bytes...", len);
        result = pa_stream_write(
                m_pa_stream,
                buffer_ptr,
                len,
                nullptr,
                0,
                PA_SEEK_RELATIVE
        );
        m_mainloop_lock.unlock();

        if (result < 0) {
            qWarning("PlayBackPulseAudio::flush(): pa_stream_write failed");
            return -EIO;
        }

        buffer_ptr     = reinterpret_cast<quint8 *>(buffer_ptr) + len;
        m_buffer_used -= len;
    }

//     qDebug("PlayBackPulseAudio::flush(): flush done.");

    // buffer is written out now
    m_buffer_used = 0;
    m_buffer      = nullptr;

    return result;
}

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

//***************************************************************************
int Kwave::PlayBackPulseAudio::close()
{
    // set hourglass cursor, we are waiting...
    QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));

    // flush the buffer
    if (m_buffer_used) flush();

    // release the allocated memory
    if (m_buffer) {
        free(m_buffer);
        m_buffer = nullptr;
    }

    if (m_pa_mainloop && m_pa_stream) {

        m_mainloop_lock.lock();
        pa_operation *op = pa_stream_drain(m_pa_stream,
                                           pa_stream_success_cb, this);
        Q_ASSERT(op);
        if (!op) qWarning("pa_stream_drain() failed: '%s'", pa_strerror(
            pa_context_errno(m_pa_context)));

        // calculate a reasonable time for the timeout (16 buffers)
        int samples_per_buffer = Kwave::toInt(
            m_buffer_size / m_bytes_per_sample
        );
        int ms = (!qFuzzyIsNull(m_rate)) ?
            Kwave::toInt((samples_per_buffer * 1000.0) / m_rate) : 0;
        int timeout = (ms + 1) * 4;
        if (timeout < TIMEOUT_MIN_DRAIN)
            timeout = TIMEOUT_MIN_DRAIN;

        qDebug("PlayBackPulseAudio::flush(): waiting for drain to finish...");
        while (op && (pa_operation_get_state(op) != PA_OPERATION_DONE)) {
            if (!PA_CONTEXT_IS_GOOD(pa_context_get_state(m_pa_context)) ||
                !PA_STREAM_IS_GOOD(pa_stream_get_state(m_pa_stream))) {
                qWarning("PlayBackPulseAudio::close(): bad stream state");
                break;
            }
            if (!m_mainloop_signal.wait(&m_mainloop_lock, timeout)) {
                qWarning("PlayBackPulseAudio::flush(): timed out after %u ms",
                         timeout);
                break;
            }
        }
        m_mainloop_lock.unlock();

        if (m_pa_stream) {
            pa_stream_disconnect(m_pa_stream);
            pa_stream_unref(m_pa_stream);
            m_pa_stream = nullptr;
        }
    }

    disconnectFromServer();
    m_device_list.clear();

    QApplication::restoreOverrideCursor();
    return 0;
}

//***************************************************************************
void Kwave::PlayBackPulseAudio::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_sink_info = pa_context_get_sink_info_list(
        m_pa_context,
        pa_sink_info_cb,
        this
    );
    if (op_sink_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
//     qDebug("----------------------------------------");
    QMap<QString, sink_info_t> list;

    // first entry == default device
    sink_info_t i;
    pa_sample_spec s;
    s.format   = PA_SAMPLE_INVALID;
    s.rate     = 0;
    s.channels = 0;
    i.m_name        = QString();
    i.m_description = _("(server default)");
    i.m_driver      = QString();
    i.m_card        = -1;
    i.m_sample_spec = s;
    list[i18n("(Use server default)") + _("|sound_note")] = i;

    for (QMap<QString, sink_info_t>::const_iterator it =
         m_device_list.constBegin(); it != m_device_list.constEnd(); ++it)
    {
        const QString     &sink        = it.key();
        const sink_info_t &si          = it.value();
        const QString     &name        = si.m_name;
        QString           description  = si.m_description;
        QString           driver       = si.m_driver;

        // if the name is not unique, add the internal sink name
        for (QMap<QString, sink_info_t>::const_iterator it2 =
            m_device_list.constBegin(); it2 != m_device_list.constEnd(); ++it2)
        {
            if (it2.key() == sink) continue;
            if ((it2.value().m_description == description) &&
                (it2.value().m_driver      == driver))
            {
                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 (si.m_card != PA_INVALID_INDEX)
            description.append(_("|sound_device"));
        else
            description.append(_("|sound_note"));

//      qDebug("supported device: '%s'", DBG(description));
        list.insert(description, *it);
    }
//     qDebug("----------------------------------------");

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

//***************************************************************************
QStringList Kwave::PlayBackPulseAudio::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;
}

//***************************************************************************
QString Kwave::PlayBackPulseAudio::fileFilter()
{
    return _("");
}

//***************************************************************************
QList<unsigned int> Kwave::PlayBackPulseAudio::supportedBits
(
    const QString &device
)
{
    QList<unsigned int> list;

    if ( m_device_list.isEmpty() || !m_device_list.contains(device) ||
         !pa_sample_spec_valid(&m_device_list[device].m_sample_spec) )
        return list;

    list.append(Kwave::toUint(
        pa_sample_size(&m_device_list[device].m_sample_spec) * 8)
    );

    return list;
}

//***************************************************************************
int Kwave::PlayBackPulseAudio::detectChannels(const QString &device,
                                              unsigned int &min,
                                              unsigned int &max)
{
    min = max = 0;

    if (m_device_list.isEmpty() || !m_device_list.contains(device))
        return -1;

    min = max = m_device_list[device].m_sample_spec.channels;
    return 0;
}

#endif /* HAVE_PULSEAUDIO_SUPPORT */

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