/*
    Copyright © 2014-2019 by The qTox Project Contributors

    This file is part of qTox, a Qt-based graphical interface for Tox.

    qTox is libre 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 3 of the License, or
    (at your option) any later version.

    qTox is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with qTox.  If not, see <http://www.gnu.org/licenses/>.
*/

#include "openal.h"
#include "src/persistence/settings.h"

#include <QDebug>
#include <QFile>
#include <QMutexLocker>
#include <QPointer>
#include <QThread>
#include <QWaitCondition>
#include <QtMath>

#include <cassert>

namespace {
void applyGain(int16_t* buffer, uint32_t bufferSize, qreal gainFactor)
{
    for (quint32 i = 0; i < bufferSize; ++i) {
        // gain amplification with clipping to 16-bit boundaries
        buffer[i] =
            qBound<int16_t>(std::numeric_limits<int16_t>::min(), qRound(buffer[i] * gainFactor),
                            std::numeric_limits<int16_t>::max());
    }
}
} // namespace

/**
 * @class OpenAL
 * @brief Provides the OpenAL audio backend
 *
 * @var BUFFER_COUNT
 * @brief Number of buffers to use per audio source
 *
 * @var AUDIO_CHANNELS
 * @brief Ideally, we'd auto-detect, but that's a sane default
 */

static const unsigned int BUFFER_COUNT = 16;
static const uint32_t AUDIO_CHANNELS = 2;

OpenAL::OpenAL()
    : audioThread{new QThread}
{
    // initialize OpenAL error stack
    alGetError();
    alcGetError(nullptr);

    audioThread->setObjectName("qTox Audio");
    QObject::connect(audioThread, &QThread::finished, &voiceTimer, &QTimer::stop);
    QObject::connect(audioThread, &QThread::finished, &captureTimer, &QTimer::stop);
    QObject::connect(audioThread, &QThread::finished, audioThread, &QThread::deleteLater);

    moveToThread(audioThread);

    voiceTimer.setSingleShot(true);
    voiceTimer.moveToThread(audioThread);
    connect(this, &OpenAL::startActive, &voiceTimer,
            static_cast<void (QTimer::*)(int)>(&QTimer::start));
    connect(&voiceTimer, &QTimer::timeout, this, &OpenAL::stopActive);

    connect(&captureTimer, &QTimer::timeout, this, &OpenAL::doAudio);
    captureTimer.setInterval(AUDIO_FRAME_DURATION / 2);
    captureTimer.setSingleShot(false);
    captureTimer.moveToThread(audioThread);
    // TODO for Qt 5.6+: use qOverload
    connect(audioThread, &QThread::started, &captureTimer,
            static_cast<void (QTimer::*)(void)>(&QTimer::start));

    cleanupTimer.setInterval(1000);
    cleanupTimer.setSingleShot(false);
    connect(&cleanupTimer, &QTimer::timeout, this, &OpenAL::cleanupSound);
    // TODO for Qt 5.6+: use qOverload
    connect(audioThread, &QThread::started, &cleanupTimer,
            static_cast<void (QTimer::*)(void)>(&QTimer::start));

    audioThread->start();
}

OpenAL::~OpenAL()
{
    audioThread->exit();
    audioThread->wait();
    cleanupInput();
    cleanupOutput();
}

void OpenAL::checkAlError() noexcept
{
    const ALenum al_err = alGetError();
    if (al_err != AL_NO_ERROR)
        qWarning("OpenAL error: %d", al_err);
}

void OpenAL::checkAlcError(ALCdevice* device) noexcept
{
    const ALCenum alc_err = alcGetError(device);
    if (alc_err)
        qWarning("OpenAL error: %d", alc_err);
}

/**
 * @brief Returns the current output volume (between 0 and 1)
 */
qreal OpenAL::outputVolume() const
{
    QMutexLocker locker(&audioLock);

    ALfloat volume = 0.0;

    if (alOutDev) {
        alGetListenerf(AL_GAIN, &volume);
        checkAlError();
    }

    return volume;
}

/**
 * @brief Set the master output volume.
 *
 * @param[in] volume   the master volume (between 0 and 1)
 */
void OpenAL::setOutputVolume(qreal volume)
{
    QMutexLocker locker(&audioLock);

    volume = std::max(0.0, std::min(volume, 1.0));

    alListenerf(AL_GAIN, static_cast<ALfloat>(volume));
    checkAlError();
}

/**
 * @brief The minimum gain value for an input device.
 *
 * @return minimum gain value in dB
 */
qreal OpenAL::minInputGain() const
{
    QMutexLocker locker(&audioLock);
    return minInGain;
}

/**
 * @brief Set the minimum allowed gain value in dB.
 *
 * @note Default is -30dB; usually you don't need to alter this value;
 */
void OpenAL::setMinInputGain(qreal dB)
{
    QMutexLocker locker(&audioLock);
    minInGain = dB;
}

/**
 * @brief The maximum gain value for an input device.
 *
 * @return maximum gain value in dB
 */
qreal OpenAL::maxInputGain() const
{
    QMutexLocker locker(&audioLock);
    return maxInGain;
}

/**
 * @brief Set the maximum allowed gain value in dB.
 *
 * @note Default is 30dB; usually you don't need to alter this value.
 */
void OpenAL::setMaxInputGain(qreal dB)
{
    QMutexLocker locker(&audioLock);
    maxInGain = dB;
}

/**
 * @brief The minimum threshold value for an input device.
 *
 * @return minimum normalized threshold
 */
qreal OpenAL::minInputThreshold() const
{
    QMutexLocker locker(&audioLock);
    return minInThreshold;
}

/**
 * @brief The maximum normalized threshold value for an input device.
 *
 * @return maximum normalized threshold
 */
qreal OpenAL::maxInputThreshold() const
{
    QMutexLocker locker(&audioLock);
    return maxInThreshold;
}

void OpenAL::reinitInput(const QString& inDevDesc)
{
    QMutexLocker locker(&audioLock);

    const auto bakSources = sources;
    sources.clear();

    cleanupInput();
    initInput(inDevDesc);

    locker.unlock();
    // this must happen outside `audioLock`, to avoid a deadlock when
    // a slot on AlSource::invalidate tries to create a new source immedeately.
    for (auto& source : bakSources) {
        source->kill();
    }
}

bool OpenAL::reinitOutput(const QString& outDevDesc)
{
    QMutexLocker locker(&audioLock);

    const auto bakSinks = sinks;

    sinks.clear();

    cleanupOutput();
    const bool result = initOutput(outDevDesc);

    locker.unlock();
    // this must happen outside `audioLock`, to avoid a deadlock when
    // a slot on AlSink::invalidate tries to create a new source immedeately.
    for (auto& sink : bakSinks) {
        sink->kill();
    }

    return result;
}

/**
 * @brief Allocates ressources for a new audio output
 * @return AudioSink on success, nullptr on failure
 */
std::unique_ptr<IAudioSink> OpenAL::makeSink()
{
    QMutexLocker locker(&audioLock);

    if (!autoInitOutput()) {
        qWarning("Failed to subscribe to audio output device.");
        return {};
    }

    ALuint sid;
    alGenSources(1, &sid);

    auto const sink = new AlSink(*this, sid);
    if (sink == nullptr) {
        return {};
    }

    sinks.insert(sink);
    qDebug() << "Audio source" << sid << "created. Sources active:" << sinks.size();

    return std::unique_ptr<IAudioSink>{sink};
}

/**
 * @brief Must be called by the destructor of AlSink to remove the internal ressources.
 *        If no sinks are opened, the output is closed afterwards.
 * @param sink Audio sink to remove.
 */
void OpenAL::destroySink(AlSink& sink)
{
    QMutexLocker locker(&audioLock);

    const auto sinksErased = sinks.erase(&sink);
    const auto soundSinksErased = soundSinks.erase(&sink);

    if (sinksErased == 0 && soundSinksErased == 0) {
        qWarning() << "Destroying non-existant source";
        return;
    }

    const uint sid = sink.getSourceId();

    if (alIsSource(sid)) {
        // stop playing, marks all buffers as processed
        alSourceStop(sid);
        cleanupBuffers(sid);
        qDebug() << "Audio source" << sid << "deleted. Sources active:" << sinks.size();
    } else {
        qWarning() << "Trying to delete invalid audio source" << sid;
    }

    if (sinks.empty() && soundSinks.empty()) {
        cleanupOutput();
    }
}

/**
 * @brief Subscribe to capture sound from the opened input device.
 *
 * If the input device is not open, it will be opened before capturing.
 */
std::unique_ptr<IAudioSource> OpenAL::makeSource()
{
    QMutexLocker locker(&audioLock);

    if (!autoInitInput()) {
        qWarning("Failed to subscribe to audio input device.");
        return {};
    }

    auto const source = new AlSource(*this);
    if (source == nullptr) {
        return {};
    }

    sources.insert(source);

    qDebug() << "Subscribed to audio input device [" << sources.size() << "subscriptions ]";

    return std::unique_ptr<IAudioSource>{source};
}

/**
 * @brief Unsubscribe from capturing from an opened input device.
 *
 * If the input device has no more subscriptions, it will be closed.
 */
void OpenAL::destroySource(AlSource& source)
{
    QMutexLocker locker(&audioLock);

    const auto s = sources.find(&source);
    if (s == sources.end()) {
        qWarning() << "Destroyed non-existant source";
        return;
    }

    sources.erase(s);

    qDebug() << "Unsubscribed from audio input device [" << sources.size() << "subscriptions left ]";

    if (sources.empty()) {
        cleanupInput();
    }
}

/**
 * @brief Initialize audio input device, if not initialized.
 *
 * @return true, if device was initialized; false otherwise
 */
bool OpenAL::autoInitInput()
{
    return alInDev ? true : initInput(Settings::getInstance().getInDev());
}

/**
 * @brief Initialize audio output device, if not initialized.
 *
 * @return true, if device was initialized; false otherwise
 */
bool OpenAL::autoInitOutput()
{
    return alOutDev ? true : initOutput(Settings::getInstance().getOutDev());
}

bool OpenAL::initInput(const QString& deviceName)
{
    return initInput(deviceName, AUDIO_CHANNELS);
}

bool OpenAL::initInput(const QString& deviceName, uint32_t channels)
{
    if (!Settings::getInstance().getAudioInDevEnabled()) {
        return false;
    }

    qDebug() << "Opening audio input" << deviceName;
    assert(!alInDev);

    // TODO: Try to actually detect if our audio source is stereo
    this->channels = channels;
    int stereoFlag = channels == 1 ? AL_FORMAT_MONO16 : AL_FORMAT_STEREO16;
    const int bytesPerSample = 2;
    const int safetyFactor = 2; // internal OpenAL ring buffer. must be larger than our inputBuffer
                                // to avoid the ring from overwriting itself between captures.
    AUDIO_FRAME_SAMPLE_COUNT_TOTAL = AUDIO_FRAME_SAMPLE_COUNT_PER_CHANNEL * channels;
    const ALCsizei ringBufSize = AUDIO_FRAME_SAMPLE_COUNT_TOTAL * bytesPerSample * safetyFactor;

    const QByteArray qDevName = deviceName.toUtf8();
    const ALchar* tmpDevName = qDevName.isEmpty() ? nullptr : qDevName.constData();
    alInDev = alcCaptureOpenDevice(tmpDevName, AUDIO_SAMPLE_RATE, stereoFlag, ringBufSize);

    // Restart the capture if necessary
    if (!alInDev) {
        qWarning() << "Failed to initialize audio input device:" << deviceName;
        return false;
    }

    inputBuffer = new int16_t[AUDIO_FRAME_SAMPLE_COUNT_TOTAL];
    setInputGain(Settings::getInstance().getAudioInGainDecibel());
    setInputThreshold(Settings::getInstance().getAudioThreshold());

    qDebug() << "Opened audio input" << deviceName;
    alcCaptureStart(alInDev);

    return true;
}

/**
 * @brief Open an audio output device
 */
bool OpenAL::initOutput(const QString& deviceName)
{
    // there should be no sinks when initializing the output
    assert(sinks.size() == 0);

    outputInitialized = false;
    if (!Settings::getInstance().getAudioOutDevEnabled())
        return false;

    qDebug() << "Opening audio output" << deviceName;
    assert(!alOutDev);

    const QByteArray qDevName = deviceName.toUtf8();
    const ALchar* tmpDevName = qDevName.isEmpty() ? nullptr : qDevName.constData();
    alOutDev = alcOpenDevice(tmpDevName);

    if (!alOutDev) {
        qWarning() << "Cannot open output audio device" << deviceName;
        return false;
    }

    qDebug() << "Opened audio output" << deviceName;
    alOutContext = alcCreateContext(alOutDev, nullptr);
    checkAlcError(alOutDev);

    if (!alcMakeContextCurrent(alOutContext)) {
        qWarning() << "Cannot create output audio context";
        return false;
    }

    // init master volume
    alListenerf(AL_GAIN, Settings::getInstance().getOutVolume() * 0.01f);
    checkAlError();

    outputInitialized = true;
    return true;
}

/**
 * @brief Play a 48kHz mono 16bit PCM sound
 */
void OpenAL::playMono16Sound(AlSink& sink, const IAudioSink::Sound& sound)
{
    const uint sourceId = sink.getSourceId();
    QFile sndFile(IAudioSink::getSound(sound));
    if (!sndFile.exists()) {
        qDebug() << "Trying to open non existent sound file";
        return;
    }

    sndFile.open(QIODevice::ReadOnly);
    const QByteArray data{sndFile.readAll()};
    if (data.isEmpty()) {
        qDebug() << "Sound file contained no data";
        return;
    }

    QMutexLocker locker(&audioLock);

    // interrupt possibly playing sound, we don't buffer here
    alSourceStop(sourceId);

    ALint processed = 0;
    alGetSourcei(sourceId, AL_BUFFERS_PROCESSED, &processed);
    alSourcei(sourceId, AL_LOOPING, AL_FALSE);

    ALuint bufid;
    if (processed == 0) {
        // create new buffer
        alGenBuffers(1, &bufid);
    } else {
        // we only reserve space for one buffer
        assert(processed == 1);
        // unqueue all processed buffers
        alSourceUnqueueBuffers(sourceId, processed, &bufid);
    }

    alBufferData(bufid, AL_FORMAT_MONO16, data.constData(), data.size(), AUDIO_SAMPLE_RATE);
    alSourcei(sourceId, AL_BUFFER, bufid);
    alSourcePlay(sourceId);
    soundSinks.insert(&sink);
}

void OpenAL::cleanupSound()
{
    QMutexLocker locker(&audioLock);

    auto sinkIt = soundSinks.begin();
    while (sinkIt != soundSinks.end()) {
        auto sink = *sinkIt;
        ALuint sourceId = sink->getSourceId();
        ALint state = 0;

        alGetSourcei(sourceId, AL_SOURCE_STATE, &state);
        if (state != AL_PLAYING) {
            sinkIt = soundSinks.erase(sinkIt);
            emit sink->finishedPlaying();
        } else {
            ++sinkIt;
        }
    }
}

void OpenAL::playAudioBuffer(uint sourceId, const int16_t* data, int samples, unsigned channels,
                             int sampleRate)
{
    assert(channels == 1 || channels == 2);
    QMutexLocker locker(&audioLock);

    if (!(alOutDev && outputInitialized))
        return;

    ALuint bufids[BUFFER_COUNT];
    ALint processed = 0, queued = 0;
    alGetSourcei(sourceId, AL_BUFFERS_PROCESSED, &processed);
    alGetSourcei(sourceId, AL_BUFFERS_QUEUED, &queued);
    alSourcei(sourceId, AL_LOOPING, AL_FALSE);

    if (processed == 0) {
        if (static_cast<ALuint>(queued) >= BUFFER_COUNT) {
            // reached limit, drop audio
            return;
        }
        // create new buffer if none got free and we're below the limit
        alGenBuffers(1, bufids);
    } else {
        // unqueue all processed buffers
        alSourceUnqueueBuffers(sourceId, processed, bufids);
        // delete all but the first buffer, reuse first for new data
        alDeleteBuffers(processed - 1, bufids + 1);
    }

    alBufferData(bufids[0], (channels == 1) ? AL_FORMAT_MONO16 : AL_FORMAT_STEREO16, data,
                 samples * 2 * channels, sampleRate);
    alSourceQueueBuffers(sourceId, 1, bufids);

    ALint state;
    alGetSourcei(sourceId, AL_SOURCE_STATE, &state);
    if (state != AL_PLAYING) {
        alSourcePlay(sourceId);
    }
}

/**
 * @brief Close active audio input device.
 */
void OpenAL::cleanupInput()
{
    if (!alInDev)
        return;

    qDebug() << "Closing audio input";
    alcCaptureStop(alInDev);
    if (alcCaptureCloseDevice(alInDev) == ALC_TRUE) {
        alInDev = nullptr;
    } else {
        qWarning() << "Failed to close input";
    }

    delete[] inputBuffer;
}

/**
 * @brief Close active audio output device
 */
void OpenAL::cleanupOutput()
{
    outputInitialized = false;

    if (alOutDev) {
        if (!alcMakeContextCurrent(nullptr)) {
            qWarning("Failed to clear audio context.");
        }

        alcDestroyContext(alOutContext);
        alOutContext = nullptr;

        qDebug() << "Closing audio output";
        if (alcCloseDevice(alOutDev)) {
            alOutDev = nullptr;
        } else {
            qWarning("Failed to close output.");
        }
    }
}

/**
 * @brief Called by doCapture to calculate volume of the audio buffer
 *
 * @param[in] buf   the current audio buffer
 *
 * @return normalized volume between 0-1
 */
float OpenAL::getVolume()
{
    const quint32 samples = AUDIO_FRAME_SAMPLE_COUNT_TOTAL;
    const float rootTwo = 1.414213562; // sqrt(2), but sqrt is not constexpr
    // calculate volume as the root mean squared of amplitudes in the sample
    float sumOfSquares = 0;
    for (quint32 i = 0; i < samples; i++) {
        float sample = static_cast<float>(inputBuffer[i]) / std::numeric_limits<int16_t>::max();
        sumOfSquares += std::pow(sample, 2);
    }
    const float rms = std::sqrt(sumOfSquares / samples);
    // our calculated normalized volume could possibly be above 1 because our RMS assumes a sinusoidal wave
    const float normalizedVolume = std::min(rms * rootTwo, 1.0f);
    return normalizedVolume;
}

/**
 * @brief Called by voiceTimer's timeout to disable audio broadcasting
 */
void OpenAL::stopActive()
{
    isActive = false;
}

/**
 * @brief handles recording of audio frames
 */
void OpenAL::doInput()
{
    ALint curSamples = 0;
    alcGetIntegerv(alInDev, ALC_CAPTURE_SAMPLES, sizeof(curSamples), &curSamples);
    if (curSamples < static_cast<ALint>(AUDIO_FRAME_SAMPLE_COUNT_PER_CHANNEL)) {
        return;
    }

    captureSamples(alInDev, inputBuffer, AUDIO_FRAME_SAMPLE_COUNT_PER_CHANNEL);

    applyGain(inputBuffer, AUDIO_FRAME_SAMPLE_COUNT_TOTAL, gainFactor);

    float volume = getVolume();
    if (volume >= inputThreshold) {
        isActive = true;
        emit startActive(voiceHold);
    } else if (!isActive) {
        volume = 0;
    }

    // NOTE(sudden6): this loop probably doesn't scale too well with many sources
    for (auto source : sources) {
        emit source->volumeAvailable(volume);
    }
    if (!isActive) {
        return;
    }

    // NOTE(sudden6): this loop probably doesn't scale too well with many sources
    for (auto source : sources) {
        emit source->frameAvailable(inputBuffer, AUDIO_FRAME_SAMPLE_COUNT_PER_CHANNEL, channels,
                                    AUDIO_SAMPLE_RATE);
    }
}

void OpenAL::doOutput()
{
    // Nothing
}

/**
 * @brief Called on the captureTimer events to capture audio
 */
void OpenAL::doAudio()
{
    QMutexLocker lock(&audioLock);

    // Output section does nothing

    // Input section
    if (alInDev && !sources.empty()) {
        doInput();
    }
}

void OpenAL::captureSamples(ALCdevice* device, int16_t* buffer, ALCsizei samples)
{
    alcCaptureSamples(device, buffer, samples);
}

/**
 * @brief Returns true if the output device is open
 */
bool OpenAL::isOutputReady() const
{
    QMutexLocker locker(&audioLock);
    return alOutDev && outputInitialized;
}

QStringList OpenAL::outDeviceNames()
{
    QStringList list;
    const ALchar* pDeviceList = (alcIsExtensionPresent(nullptr, "ALC_ENUMERATE_ALL_EXT") != AL_FALSE)
                                    ? alcGetString(nullptr, ALC_ALL_DEVICES_SPECIFIER)
                                    : alcGetString(nullptr, ALC_DEVICE_SPECIFIER);

    if (pDeviceList) {
        while (*pDeviceList) {
            int len = static_cast<int>(strlen(pDeviceList));
            list << QString::fromUtf8(pDeviceList, len);
            pDeviceList += len + 1;
        }
    }

    return list;
}

QStringList OpenAL::inDeviceNames()
{
    QStringList list;
    const ALchar* pDeviceList = alcGetString(nullptr, ALC_CAPTURE_DEVICE_SPECIFIER);

    if (pDeviceList) {
        while (*pDeviceList) {
            int len = static_cast<int>(strlen(pDeviceList));
            list << QString::fromUtf8(pDeviceList, len);
            pDeviceList += len + 1;
        }
    }

    return list;
}

/**
 * @brief Free all buffers that finished playing on a source
 * @param sourceId where to remove the buffers from
 */
void OpenAL::cleanupBuffers(uint sourceId)
{
    // unqueue all buffers from the source
    ALint processed = 0;
    alGetSourcei(sourceId, AL_BUFFERS_PROCESSED, &processed);
    std::vector<ALuint> bufids;
    // should never be out of range, just to be sure
    assert(processed >= 0);
    assert(processed <= SIZE_MAX);
    bufids.resize(processed);
    alSourceUnqueueBuffers(sourceId, processed, bufids.data());
    // delete all buffers
    alDeleteBuffers(processed, bufids.data());
}

void OpenAL::startLoop(uint sourceId)
{
    QMutexLocker locker(&audioLock);
    alSourcei(sourceId, AL_LOOPING, AL_TRUE);
}

void OpenAL::stopLoop(uint sourceId)
{
    QMutexLocker locker(&audioLock);
    alSourcei(sourceId, AL_LOOPING, AL_FALSE);
    alSourceStop(sourceId);
    cleanupBuffers(sourceId);
}

qreal OpenAL::inputGain() const
{
    return gain;
}

qreal OpenAL::getInputThreshold() const
{
    return inputThreshold;
}

qreal OpenAL::inputGainFactor() const
{
    return gainFactor;
}

void OpenAL::setInputGain(qreal dB)
{
    gain = qBound(minInGain, dB, maxInGain);
    gainFactor = qPow(10.0, (gain / 20.0));
}

void OpenAL::setInputThreshold(qreal normalizedThreshold)
{
    inputThreshold = normalizedThreshold;
}
