/**
 * @file soundin.cpp
 * @brief Implementation of SoundInput class
 */
#include "SoundInput.h"
#include "JS8_Main/DriftingDateTime.h"

#include <QAudioFormat>
#include <QLoggingCategory>
#include <QSysInfo>

#include "moc_SoundInput.cpp"

Q_DECLARE_LOGGING_CATEGORY(soundin_js8)

/**
 * @brief Checks for audio errors and emits appropriate error messages.
 *
 * @return true
 * @return false
 */
bool SoundInput::audioError() const {
    bool result(true);

    Q_ASSERT_X(m_stream, "SoundInput", "programming error");
    if (m_stream) {
        switch (m_stream->error()) {
        case QAudio::OpenError:
            Q_EMIT error(
                tr("An error opening the audio input device has occurred."));
            break;

        case QAudio::IOError:
            Q_EMIT error(tr(
                "An error occurred during read from the audio input device."));
            break;

        case QAudio::UnderrunError:
            Q_EMIT error(tr("Audio data not being fed to the audio input "
                            "device fast enough."));
            break;

        case QAudio::FatalError:
            Q_EMIT error(tr("Non-recoverable error, audio input device not "
                            "usable at this time."));
            break;

        case QAudio::NoError:
            result = false;
            break;
        }
    }
    return result;
}

/**
 * @brief Starts audio input from the specified device.
 *
 * @param device The QAudioDevice to use for input.
 * @param framesPerBuffer The number of frames per buffer.
 * @param sink The AudioDevice sink to write audio data to.
 * @param channel The audio channel configuration (Mono or Stereo).
 */
void SoundInput::start(QAudioDevice const &device, int framesPerBuffer,
                       AudioDevice *sink, AudioDevice::Channel channel) {
    Q_ASSERT(sink);

    stop();

    m_sink = sink;

    QAudioFormat format(device.preferredFormat());
    //  qCDebug (soundin_js8) << "Preferred audio input format:" << format;
    format.setSampleFormat(QAudioFormat::Int16);
    format.setChannelCount(AudioDevice::Mono == channel ? 1 : 2);
    format.setSampleRate(48000);
    if (!format.isValid()) {
        Q_EMIT error(tr("Requested input audio format is not valid."));
        return;
    }

    if (!device.isFormatSupported(format)) {
        //      qCDebug (soundin_js8) << "Nearest supported audio format:" <<
        //      device.nearestFormat (format);
        Q_EMIT error(
            tr("Requested input audio format is not supported on device."));
        return;
    }
    //  qCDebug (soundin_js8) << "Selected audio input format:" << format;

    m_stream.reset(new QAudioSource{device, format});
    if (audioError()) {
        return;
    }

    connect(m_stream.data(), &QAudioSource::stateChanged, this,
            &SoundInput::handleStateChanged);

    m_stream->setBufferSize(m_stream->format().bytesForFrames(framesPerBuffer));
    if (sink->initialize(QIODevice::WriteOnly, channel)) {
        m_stream->start(sink);
        audioError();
    } else {
        Q_EMIT error(tr("Failed to initialize audio sink device"));
    }
}

/**
 * @brief Suspends audio input.
 */
void SoundInput::suspend() {
    if (m_stream) {
        m_stream->suspend();
        audioError();
    }
}

/**
 * @brief Resumes audio input.
 */
void SoundInput::resume() {
    //  qCDebug(soundin_js8) << "Resume" <<
    //  fmod(0.001*DriftingDateTime::currentMSecsSinceEpoch(),6.0);
    if (m_sink) {
        m_sink->reset();
    }

    if (m_stream) {
        m_stream->resume();
        audioError();
    }
}

/**
 * @brief Handles state changes of the audio input.
 * @param newState The new state of the audio input.
 */
void SoundInput::handleStateChanged(QAudio::State newState) const {
    // qCDebug (soundin_js8) << "SoundInput::handleStateChanged: newState:" <<
    // newState;

    switch (newState) {
    case QAudio::IdleState:
        Q_EMIT status(tr("Idle"));
        break;

    case QAudio::ActiveState:
        Q_EMIT status(tr("Receiving"));
        break;

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

    case QAudio::StoppedState:
        if (audioError()) {
            Q_EMIT status(tr("Error"));
        } else {
            Q_EMIT status(tr("Stopped"));
        }
        break;
    }
}

/**
 * @brief Stops audio input.
 */
void SoundInput::stop() {
    if (m_stream) {
        m_stream->stop();
    }
    m_stream.reset();

    if (m_sink) {
        m_sink->close();
    }
}

/**
 * @brief Destructs the SoundInput object.
 */
SoundInput::~SoundInput() { stop(); }

Q_LOGGING_CATEGORY(soundin_js8, "soundin.js8", QtWarningMsg)
