/**
 * @file NotificationSoundOutput.cpp
 * @brief Self-contained audio output for notification sounds.
 *
 * Designed for fire-and-forget playback of buffered audio data.
 * Manages its own buffer lifecycle and releases the audio device
 * automatically when playback completes.
 */

#include "NotificationSoundOutput.h"

#include <QAudioSink>
#include <QBuffer>

NotificationSoundOutput::NotificationSoundOutput(QObject *parent)
    : QObject(parent)
{
}

NotificationSoundOutput::~NotificationSoundOutput()
{
    if (m_sink) {
        disconnect(m_sink.get(), nullptr, this, nullptr);
        m_sink->stop();
    }
}

/**
 * @brief Sets the audio device and buffer size.
 * @param device The QAudioDevice to use.
 * @param msBuffer The buffer size in milliseconds.
 */
void NotificationSoundOutput::setDevice(QAudioDevice const &device,
                                        unsigned const msBuffer)
{
    m_device = device;
    m_msBuffer = msBuffer;
}

/**
 * @brief Sets the attenuation in dB.
 * @param a The attenuation value (0 = full volume).
 */
void NotificationSoundOutput::setAttenuation(qreal const a)
{
    Q_ASSERT(0.0 <= a && a <= 999.0);
    m_volume = qPow(10.0, -a / 20.0);

    if (m_sink)
        m_sink->setVolume(m_volume);
}

/**
 * @brief Plays audio from the provided data and format.
 * @param data The raw PCM audio data.
 * @param format The QAudioFormat describing the data.
 *
 * Any currently playing notification is stopped and replaced.
 */
void NotificationSoundOutput::play(QByteArray const &data,
                                   QAudioFormat const &format)
{
    if (m_sink) {
        disconnect(m_sink.get(), nullptr, this, nullptr);
        m_sink->stop();
    }

    if (m_buffer) {
        m_buffer->close();
        m_buffer.reset();
    }

    if (m_device.isNull()) {
        Q_EMIT error(tr("No audio output device configured."));
        return;
    }

    if (!format.isValid()) {
        Q_EMIT error(tr("Requested audio format is not valid."));
        return;
    }

    if (!m_device.isFormatSupported(format)) {
        Q_EMIT error(tr("Requested audio format is not supported on device."));
        return;
    }

    if (data.isEmpty()) {
        Q_EMIT error(tr("Audio data is empty."));
        return;
    }

    m_buffer.reset(new QBuffer);
    m_buffer->setData(data);

    if (!m_buffer->open(QIODevice::ReadOnly)) {
        m_buffer.reset();
        Q_EMIT error(tr("Unable to open audio buffer."));
        return;
    }

    // Only create a new sink if we don't have one, or if the
    // format has changed since the last playback.
    if (!m_sink || m_currentFormat != format) {
        m_sink.reset(new QAudioSink(m_device, format));
        m_sink->setVolume(m_volume);
        m_currentFormat = format;

        if (m_msBuffer > 0) {
            m_sink->setBufferSize(
                m_sink->format().bytesForDuration(m_msBuffer));
        }
    }

    connect(m_sink.get(), &QAudioSink::stateChanged,
            this, &NotificationSoundOutput::handleStateChanged);

    m_sink->start(m_buffer.get());

    if (m_sink->error() != QAudio::NoError) {
        Q_EMIT error(tr("Failed to start audio output."));
        m_sink->stop();
        if (m_buffer) {
            m_buffer->close();
            m_buffer.reset();
        }
    }
}

/**
 * @brief Stops playback and releases all resources.
 */
void NotificationSoundOutput::stop()
{
    release();
}

// Private
void NotificationSoundOutput::release()
{
    if (m_sink) {
        disconnect(m_sink.get(), nullptr, this, nullptr);
        m_sink->stop();
    }

    if (m_buffer) {
        m_buffer->close();
        m_buffer.reset();
    }
}

/**
 * @brief Handles state changes from the QAudioSink.
 * @param newState The new audio state.
 */
void NotificationSoundOutput::handleStateChanged(QAudio::State const newState)
{
    switch (newState) {
    case QAudio::ActiveState:
        Q_EMIT status(tr("Active"));
        break;

    case QAudio::SuspendedState:
        Q_EMIT status(tr("Suspended"));
        break;

    case QAudio::IdleState:
    case QAudio::StoppedState:
        if (m_buffer) {
            m_buffer->close();
            m_buffer.reset();
        }
        Q_EMIT status(tr("Idle"));
        break;
    }
}
