/****************************************************************************
**
** Copyright (C) 2016 Research In Motion
** Contact: https://www.qt.io/licensing/
**
** This file is part of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 3 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL3 included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 3 requirements
** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 2.0 or (at your option) the GNU General
** Public license version 3 or any later version approved by the KDE Free
** Qt Foundation. The licenses are as published by the Free Software
** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-2.0.html and
** https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/

#include "qnxaudiooutput.h"

#include "qnxaudioutils.h"

#include <private/qaudiohelpers_p.h>

#pragma GCC diagnostic ignored "-Wvla"

QT_BEGIN_NAMESPACE

QnxAudioOutput::QnxAudioOutput()
    : m_source(0)
    , m_pushSource(false)
    , m_notifyInterval(1000)
    , m_error(QAudio::NoError)
    , m_state(QAudio::StoppedState)
    , m_volume(1.0)
    , m_periodSize(0)
    , m_pcmHandle(0)
    , m_bytesWritten(0)
    , m_intervalOffset(0)
#if _NTO_VERSION >= 700
    , m_pcmNotifier(0)
#endif
{
    m_timer.setSingleShot(false);
    m_timer.setInterval(20);
    connect(&m_timer, SIGNAL(timeout()), this, SLOT(pullData()));
}

QnxAudioOutput::~QnxAudioOutput()
{
    stop();
}

void QnxAudioOutput::start(QIODevice *source)
{
    if (m_state != QAudio::StoppedState)
        stop();

    m_error = QAudio::NoError;
    m_source = source;
    m_pushSource = false;

    if (open()) {
        setState(QAudio::ActiveState);
        m_timer.start();
    } else {
        setError(QAudio::OpenError);
        setState(QAudio::StoppedState);
    }
}

QIODevice *QnxAudioOutput::start()
{
    if (m_state != QAudio::StoppedState)
        stop();

    m_error = QAudio::NoError;
    m_source = new QnxPushIODevice(this);
    m_source->open(QIODevice::WriteOnly|QIODevice::Unbuffered);
    m_pushSource = true;

    if (open())
        setState(QAudio::IdleState);
    else {
        setError(QAudio::OpenError);
        setState(QAudio::StoppedState);
    }

    return m_source;
}

void QnxAudioOutput::stop()
{
    if (m_state == QAudio::StoppedState)
        return;

    setError(QAudio::NoError);
    setState(QAudio::StoppedState);
    close();
}

void QnxAudioOutput::reset()
{
    if (m_pcmHandle)
#if SND_PCM_VERSION < SND_PROTOCOL_VERSION('P',3,0,2)
        snd_pcm_playback_drain(m_pcmHandle);
#else
        snd_pcm_channel_drain(m_pcmHandle, SND_PCM_CHANNEL_PLAYBACK);
#endif
    stop();
}

void QnxAudioOutput::suspend()
{
    snd_pcm_playback_pause(m_pcmHandle);
    if (state() != QAudio::InterruptedState)
        suspendInternal(QAudio::SuspendedState);
}

void QnxAudioOutput::resume()
{
    snd_pcm_playback_resume(m_pcmHandle);
    if (state() != QAudio::InterruptedState)
        resumeInternal();
}

int QnxAudioOutput::bytesFree() const
{
    if (m_state != QAudio::ActiveState && m_state != QAudio::IdleState)
        return 0;

    snd_pcm_channel_status_t status;
    memset(&status, 0, sizeof(status));
    status.channel = SND_PCM_CHANNEL_PLAYBACK;
    const int errorCode = snd_pcm_plugin_status(m_pcmHandle, &status);

    if (errorCode)
        return 0;
    else
        return status.free;
}

int QnxAudioOutput::periodSize() const
{
     return m_periodSize;
}

void QnxAudioOutput::setNotifyInterval(int ms)
{
    m_notifyInterval = ms;
}

int QnxAudioOutput::notifyInterval() const
{
    return m_notifyInterval;
}

qint64 QnxAudioOutput::processedUSecs() const
{
    return qint64(1000000) * m_format.framesForBytes(m_bytesWritten) / m_format.sampleRate();
}

qint64 QnxAudioOutput::elapsedUSecs() const
{
    if (m_state == QAudio::StoppedState)
        return 0;
    else
        return m_startTimeStamp.elapsed() * qint64(1000);
}

QAudio::Error QnxAudioOutput::error() const
{
    return m_error;
}

QAudio::State QnxAudioOutput::state() const
{
    return m_state;
}

void QnxAudioOutput::setFormat(const QAudioFormat &format)
{
    if (m_state == QAudio::StoppedState)
        m_format = format;
}

QAudioFormat QnxAudioOutput::format() const
{
    return m_format;
}

void QnxAudioOutput::setVolume(qreal volume)
{
    m_volume = qBound(qreal(0.0), volume, qreal(1.0));
}

qreal QnxAudioOutput::volume() const
{
    return m_volume;
}

void QnxAudioOutput::setCategory(const QString &category)
{
    m_category = category;
}

QString QnxAudioOutput::category() const
{
    return m_category;
}

void QnxAudioOutput::pullData()
{
    if (m_state == QAudio::StoppedState
            || m_state == QAudio::SuspendedState
            || m_state == QAudio::InterruptedState)
        return;

    const int bytesAvailable = bytesFree();
    const int frames = m_format.framesForBytes(bytesAvailable);

    if (frames == 0 || bytesAvailable < periodSize())
        return;

    // The buffer is placed on the stack so no more than 64K or 1 frame
    // whichever is larger.
    const int maxFrames = qMax(m_format.framesForBytes(64 * 1024), 1);
    const int bytesRequested = m_format.bytesForFrames(qMin(frames, maxFrames));

    char buffer[bytesRequested];
    const int bytesRead = m_source->read(buffer, bytesRequested);

    // reading can take a while and stream may have been stopped
    if (!m_pcmHandle)
        return;

    if (bytesRead > 0) {
        // Got some data to output
        if (m_state != QAudio::ActiveState)
            return;

        const qint64 bytesWritten = write(buffer, bytesRead);
        if (bytesWritten != bytesRead)
            m_source->seek(m_source->pos()-(bytesRead-bytesWritten));

    } else {
        // We're done
        close();
        if (bytesRead != 0)
            setError(QAudio::IOError);
        setState(QAudio::StoppedState);
    }

    if (m_state != QAudio::ActiveState)
        return;

    if (m_notifyInterval > 0 && (m_intervalTimeStamp.elapsed() + m_intervalOffset) > m_notifyInterval) {
        emit notify();
        m_intervalOffset = m_intervalTimeStamp.elapsed() + m_intervalOffset - m_notifyInterval;
        m_intervalTimeStamp.restart();
    }
}

bool QnxAudioOutput::open()
{
    if (!m_format.isValid() || m_format.sampleRate() <= 0) {
        if (!m_format.isValid())
            qWarning("QnxAudioOutput: open error, invalid format.");
        else
            qWarning("QnxAudioOutput: open error, invalid sample rate (%d).", m_format.sampleRate());

        return false;
    }

    int errorCode = 0;

    int card = 0;
    int device = 0;
    if ((errorCode = snd_pcm_open_preferred(&m_pcmHandle, &card, &device, SND_PCM_OPEN_PLAYBACK)) < 0) {
        qWarning("QnxAudioOutput: open error, couldn't open card (0x%x)", -errorCode);
        return false;
    }

    if ((errorCode = snd_pcm_nonblock_mode(m_pcmHandle, 0)) < 0) {
        qWarning("QnxAudioOutput: open error, couldn't set non block mode (0x%x)", -errorCode);
        close();
        return false;
    }

    addPcmEventFilter();

    // Necessary so that bytesFree() which uses the "free" member of the status struct works
    snd_pcm_plugin_set_disable(m_pcmHandle, PLUGIN_MMAP);

    snd_pcm_channel_info_t info;
    memset(&info, 0, sizeof(info));
    info.channel = SND_PCM_CHANNEL_PLAYBACK;
    if ((errorCode = snd_pcm_plugin_info(m_pcmHandle, &info)) < 0) {
        qWarning("QnxAudioOutput: open error, couldn't get channel info (0x%x)", -errorCode);
        close();
        return false;
    }

    snd_pcm_channel_params_t params = QnxAudioUtils::formatToChannelParams(m_format, QAudio::AudioOutput, info.max_fragment_size);
    setTypeName(&params);

    if ((errorCode = snd_pcm_plugin_params(m_pcmHandle, &params)) < 0) {
        qWarning("QnxAudioOutput: open error, couldn't set channel params (0x%x)", -errorCode);
        close();
        return false;
    }

    if ((errorCode = snd_pcm_plugin_prepare(m_pcmHandle, SND_PCM_CHANNEL_PLAYBACK)) < 0) {
        qWarning("QnxAudioOutput: open error, couldn't prepare channel (0x%x)", -errorCode);
        close();
        return false;
    }

    snd_pcm_channel_setup_t setup;
    memset(&setup, 0, sizeof(setup));
    setup.channel = SND_PCM_CHANNEL_PLAYBACK;
    if ((errorCode = snd_pcm_plugin_setup(m_pcmHandle, &setup)) < 0) {
        qWarning("QnxAudioOutput: open error, couldn't get channel setup (0x%x)", -errorCode);
        close();
        return false;
    }

    m_periodSize = qMin(2048, setup.buf.block.frag_size);
    m_startTimeStamp.restart();
    m_intervalTimeStamp.restart();
    m_intervalOffset = 0;
    m_bytesWritten = 0;

    createPcmNotifiers();

    return true;
}

void QnxAudioOutput::close()
{
    m_timer.stop();

    destroyPcmNotifiers();

    if (m_pcmHandle) {
#if SND_PCM_VERSION < SND_PROTOCOL_VERSION('P',3,0,2)
        snd_pcm_plugin_flush(m_pcmHandle, SND_PCM_CHANNEL_PLAYBACK);
#else
        snd_pcm_plugin_drop(m_pcmHandle, SND_PCM_CHANNEL_PLAYBACK);
#endif
        snd_pcm_close(m_pcmHandle);
        m_pcmHandle = 0;
    }

    if (m_pushSource) {
        delete m_source;
        m_source = 0;
    }
}

void QnxAudioOutput::setError(QAudio::Error error)
{
    if (m_error != error) {
        m_error = error;
        emit errorChanged(error);
    }
}

void QnxAudioOutput::setState(QAudio::State state)
{
    if (m_state != state) {
        m_state = state;
        emit stateChanged(state);
    }
}

qint64 QnxAudioOutput::write(const char *data, qint64 len)
{
    if (!m_pcmHandle)
        return 0;

    // Make sure we're writing (N * frame) worth of bytes
    const int size = m_format.bytesForFrames(qBound(qint64(0), qint64(bytesFree()), len) / m_format.bytesPerFrame());

    if (size == 0)
        return 0;

    int written = 0;

    if (m_volume < 1.0f) {
        char out[size];
        QAudioHelperInternal::qMultiplySamples(m_volume, m_format, data, out, size);
        written = snd_pcm_plugin_write(m_pcmHandle, out, size);
    } else {
        written = snd_pcm_plugin_write(m_pcmHandle, data, size);
    }

    if (written > 0) {
        m_bytesWritten += written;
        setError(QAudio::NoError);
        setState(QAudio::ActiveState);
        return written;
    } else {
        close();
        setError(QAudio::FatalError);
        setState(QAudio::StoppedState);
        return 0;
    }
}

void QnxAudioOutput::suspendInternal(QAudio::State suspendState)
{
    m_timer.stop();
    setState(suspendState);
}

void QnxAudioOutput::resumeInternal()
{
    if (m_pushSource) {
        setState(QAudio::IdleState);
    } else {
        setState(QAudio::ActiveState);
        m_timer.start();
    }
}

#if _NTO_VERSION >= 700

QAudio::State suspendState(const snd_pcm_event_t &event)
{
    Q_ASSERT(event.type == SND_PCM_EVENT_AUDIOMGMT_STATUS);
    Q_ASSERT(event.data.audiomgmt_status.new_status == SND_PCM_STATUS_SUSPENDED);
    return event.data.audiomgmt_status.flags & SND_PCM_STATUS_EVENT_HARD_SUSPEND
            ? QAudio::InterruptedState : QAudio::SuspendedState;
}

void QnxAudioOutput::addPcmEventFilter()
{
    /* Enable PCM events */
    snd_pcm_filter_t filter;
    memset(&filter, 0, sizeof(filter));
    filter.enable = (1<<SND_PCM_EVENT_AUDIOMGMT_STATUS) |
                    (1<<SND_PCM_EVENT_AUDIOMGMT_MUTE) |
                    (1<<SND_PCM_EVENT_OUTPUTCLASS);
    snd_pcm_set_filter(m_pcmHandle, SND_PCM_CHANNEL_PLAYBACK, &filter);
}

void QnxAudioOutput::createPcmNotifiers()
{
    // QSocketNotifier::Read for poll based event dispatcher.  Exception for
    // select based event dispatcher.
    m_pcmNotifier = new QSocketNotifier(snd_pcm_file_descriptor(m_pcmHandle,
                                                                SND_PCM_CHANNEL_PLAYBACK),
                                        QSocketNotifier::Read, this);
    connect(m_pcmNotifier, &QSocketNotifier::activated,
            this, &QnxAudioOutput::pcmNotifierActivated);
}

void QnxAudioOutput::destroyPcmNotifiers()
{
    if (m_pcmNotifier) {
        delete m_pcmNotifier;
        m_pcmNotifier = 0;
    }
}

void QnxAudioOutput::setTypeName(snd_pcm_channel_params_t *params)
{
    if (m_category.isEmpty())
        return;

    QByteArray latin1Category = m_category.toLatin1();

    if (QString::fromLatin1(latin1Category) != m_category) {
        qWarning("QnxAudioOutput: audio category name isn't a Latin1 string.");
        return;
    }

    if (latin1Category.size() >= static_cast<int>(sizeof(params->audio_type_name))) {
        qWarning("QnxAudioOutput: audio category name too long.");
        return;
    }

    strcpy(params->audio_type_name, latin1Category.constData());
}

void QnxAudioOutput::pcmNotifierActivated(int socket)
{
    Q_UNUSED(socket);

    snd_pcm_event_t pcm_event;
    memset(&pcm_event, 0, sizeof(pcm_event));
    while (snd_pcm_channel_read_event(m_pcmHandle, SND_PCM_CHANNEL_PLAYBACK, &pcm_event) == 0) {
        if (pcm_event.type == SND_PCM_EVENT_AUDIOMGMT_STATUS) {
            if (pcm_event.data.audiomgmt_status.new_status == SND_PCM_STATUS_SUSPENDED)
                suspendInternal(suspendState(pcm_event));
            else if (pcm_event.data.audiomgmt_status.new_status == SND_PCM_STATUS_RUNNING)
                resumeInternal();
            else if (pcm_event.data.audiomgmt_status.new_status == SND_PCM_STATUS_PAUSED)
                suspendInternal(QAudio::SuspendedState);
        }
    }
}

#else

void QnxAudioOutput::addPcmEventFilter() {}
void QnxAudioOutput::createPcmNotifiers() {}
void QnxAudioOutput::destroyPcmNotifiers() {}
void QnxAudioOutput::setTypeName(snd_pcm_channel_params_t *) {}

#endif

QnxPushIODevice::QnxPushIODevice(QnxAudioOutput *output)
    : QIODevice(output),
      m_output(output)
{
}

QnxPushIODevice::~QnxPushIODevice()
{
}

qint64 QnxPushIODevice::readData(char *data, qint64 len)
{
    Q_UNUSED(data);
    Q_UNUSED(len);
    return 0;
}

qint64 QnxPushIODevice::writeData(const char *data, qint64 len)
{
    int retry = 0;
    qint64 written = 0;

    if (m_output->state() == QAudio::ActiveState
     || m_output->state() == QAudio::IdleState) {
        while (written < len) {
            const int writeSize = m_output->write(data + written, len - written);

            if (writeSize <= 0) {
                retry++;
                if (retry > 10)
                    return written;
                else
                    continue;
            }

            retry = 0;
            written += writeSize;
        }
    }

    return written;
}

QT_END_NAMESPACE
