// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only

#include "qwindowsaudiosink_p.h"

#include <QtCore/private/qsystemerror_p.h>
#include <QtMultimedia/private/qwindowsaudiodevice_p.h>

#include <audioclient.h>
#include <mmdeviceapi.h>

QT_BEGIN_NAMESPACE

namespace QtWASAPI {

using QWindowsAudioUtils::audioClientErrorString;
using namespace std::chrono_literals;

QWASAPIAudioSinkStream::QWASAPIAudioSinkStream(QAudioDevice device, const QAudioFormat &format, std::optional<qsizetype> ringbufferSize,
                                               QWindowsAudioSink *parent, float volume, std::optional<int32_t> hardwareBufferFrames, AudioEndpointRole role):
    QPlatformAudioSinkStream{
        std::move(device),
        format,
        ringbufferSize,
        hardwareBufferFrames,
        volume,
    },
    m_role{
          role,
    },
    m_wasapiHandle {
        CreateEvent(0, false, false, nullptr),
    },
    m_parent{
        parent
    }
{
}

bool QWASAPIAudioSinkStream::open()
{
    return true;
}

bool QWASAPIAudioSinkStream::start(QIODevice *ioDevice)
{
    auto immDevice = QAudioDevicePrivate::handle<QWindowsAudioDevice>(m_audioDevice)->open();
    bool clientOpen = openAudioClient(std::move(immDevice), m_role);
    if (!clientOpen)
        return false;

    setQIODevice(ioDevice);
    createQIODeviceConnections(ioDevice);
    pullFromQIODevice();

    bool started = startAudioClient(StreamType::Ringbuffer);
    if (!started)
        return false;

    return true;
}

QIODevice *QWASAPIAudioSinkStream::start()
{
    auto immDevice = QAudioDevicePrivate::handle<QWindowsAudioDevice>(m_audioDevice)->open();
    bool clientOpen = openAudioClient(std::move(immDevice), m_role);
    if (!clientOpen)
        return nullptr;

    QIODevice *ioDevice = createRingbufferWriterDevice();

    m_parent->updateStreamIdle(true, QWindowsAudioSink::EmitStateSignal::False);

    setQIODevice(ioDevice);
    createQIODeviceConnections(ioDevice);

    bool started = startAudioClient(StreamType::Ringbuffer);
    if (!started)
        return nullptr;

    return ioDevice;
}

bool QWASAPIAudioSinkStream::start(AudioCallback audioCallback)
{
    auto immDevice = QAudioDevicePrivate::handle<QWindowsAudioDevice>(m_audioDevice)->open();
    bool clientOpen = openAudioClient(std::move(immDevice), m_role);
    if (!clientOpen)
        return false;

    m_audioCallback = std::move(audioCallback);

    return startAudioClient(StreamType::Callback);
}

void QWASAPIAudioSinkStream::suspend()
{
    m_suspended = true;
    QWindowsAudioUtils::audioClientStop(m_audioClient);
}

void QWASAPIAudioSinkStream::resume()
{
    m_suspended = false;
    QWindowsAudioUtils::audioClientStart(m_audioClient);
}

void QWASAPIAudioSinkStream::stop(ShutdownPolicy shutdownPolicy)
{
    using namespace QWindowsAudioUtils;

    m_parent = nullptr;
    m_shutdownPolicy = shutdownPolicy;

    switch (shutdownPolicy) {
    case ShutdownPolicy::DiscardRingbuffer: {
        requestStop();
        audioClientStop(m_audioClient);
        m_workerThread->wait();
        m_workerThread = {};
        audioClientReset(m_audioClient);

        return;
    }
    case ShutdownPolicy::DrainRingbuffer: {
        m_ringbufferDrained.callOnActivated([self = shared_from_this()]() mutable {
            self->m_workerThread->wait();
            self = {};
        });
        return;
    }
    default:
        Q_UNREACHABLE_RETURN();
    }
}

void QWASAPIAudioSinkStream::updateStreamIdle(bool streamIsIdle)
{
    if (m_parent)
        m_parent->updateStreamIdle(streamIsIdle);
}

bool QWASAPIAudioSinkStream::openAudioClient(ComPtr<IMMDevice> device, AudioEndpointRole role)
{
    using namespace QWindowsAudioUtils;

    std::optional<AudioClientCreationResult> clientData =
            createAudioClient(device, m_format, m_hardwareBufferFrames, m_wasapiHandle, role);

    if (!clientData)
        return false;

    m_audioClient = std::move(clientData->client);
    m_periodSize = clientData->periodSize;
    m_audioClientFrames = clientData->audioClientFrames;

    HRESULT hr = m_audioClient->GetService(IID_PPV_ARGS(m_renderClient.GetAddressOf()));
    if (FAILED(hr)) {
        qWarning() << "IAudioClient3::GetService failed to obtain IAudioRenderClient"
                   << audioClientErrorString(hr);
        return false;
    }

    if (m_audioDevice.preferredFormat().sampleRate() != m_format.sampleRate())
        audioClientSetRate(m_audioClient, m_format.sampleRate());

    return true;
}

bool QWASAPIAudioSinkStream::startAudioClient(StreamType streamType)
{
    using namespace QWindowsAudioUtils;
    m_workerThread.reset(QThread::create([this, streamType] {
        setMCSSForPeriodSize(m_periodSize);
        fillInitialHostBuffer();

        switch (streamType) {
        case StreamType::Ringbuffer:
            return runProcessRingbufferLoop();
        case StreamType::Callback:
            return runProcessCallbackLoop();
        }
    }));
    m_workerThread->setObjectName(u"QWASAPIAudioSinkStream");
    m_workerThread->start();

    return QWindowsAudioUtils::audioClientStart(m_audioClient);
}

void QWASAPIAudioSinkStream::fillInitialHostBuffer()
{
    processRingbuffer();
}

void QWASAPIAudioSinkStream::runProcessRingbufferLoop()
{
    using namespace QWindowsAudioUtils;

    for (;;) {
        constexpr std::chrono::milliseconds timeout = 2s;
        DWORD retval = WaitForSingleObject(m_wasapiHandle.get(), timeout.count());
        if (retval != WAIT_OBJECT_0) {
            if (m_suspended)
                continue;

            handleAudioClientError();
            return;
        }

        if (isStopRequested()) {
            switch (m_shutdownPolicy.load(std::memory_order_relaxed)) {
            case ShutdownPolicy::DiscardRingbuffer:
                return;
            case ShutdownPolicy::DrainRingbuffer: {
                bool bufferDrained = visitRingbuffer([](const auto &ringbuffer) {
                    return ringbuffer.used() == 0;
                });
                if (bufferDrained) {
                    audioClientStop(m_audioClient);
                    audioClientReset(m_audioClient);

                    m_ringbufferDrained.set();
                    return;
                }
                break;
            }
            default:
                Q_UNREACHABLE_RETURN();
            }
        }

        bool success = processRingbuffer();
        if (!success) {
            handleAudioClientError();
            return;
        }
    }
}

void QWASAPIAudioSinkStream::runProcessCallbackLoop()
{
    using namespace QWindowsAudioUtils;

    for (;;) {
        constexpr std::chrono::milliseconds timeout = 2s;
        DWORD retval = WaitForSingleObject(m_wasapiHandle.get(), timeout.count());
        if (retval != WAIT_OBJECT_0) {
            if (m_suspended)
                continue;

            handleAudioClientError();
            return;
        }

        if (isStopRequested())
            return;

        bool success = processCallback();
        if (!success) {
            handleAudioClientError();
            return;
        }
    }
}

bool QWASAPIAudioSinkStream::processRingbuffer() noexcept QT_MM_NONBLOCKING
{
    uint32_t numFramesPadding;
    HRESULT hr = m_audioClient->GetCurrentPadding(&numFramesPadding);
    if (FAILED(hr)) {
        qWarning() << "IAudioClient3::GetCurrentPadding failed" << audioClientErrorString(hr);
        return false;
    }

    uint32_t requiredFrames = m_audioClientFrames - numFramesPadding;
    if (requiredFrames == 0)
        return true;

    // Grab the next empty buffer from the audio device.
    unsigned char *hostBuffer{};
    hr = m_renderClient->GetBuffer(requiredFrames, &hostBuffer);
    if (FAILED(hr)) {
        qWarning() << "IAudioRenderClient::getBuffer failed" << audioClientErrorString(hr);
        return false;
    }

    uint32_t requiredDataSize = m_format.bytesForFrames(requiredFrames);
    auto hostBufferSpan = as_writable_bytes(QSpan{ hostBuffer, qsizetype(requiredDataSize) });
    uint64_t consumedFrames = QPlatformAudioSinkStream::process(hostBufferSpan, requiredFrames);

    DWORD flags = consumedFrames != 0 ? 0 : AUDCLNT_BUFFERFLAGS_SILENT;

    hr = m_renderClient->ReleaseBuffer(requiredFrames, flags);
    if (FAILED(hr)) {
        qWarning() << "IAudioRenderClient::ReleaseBuffer failed" << audioClientErrorString(hr);
        return false;
    }

    return true;
}

bool QWASAPIAudioSinkStream::processCallback() noexcept QT_MM_NONBLOCKING
{
    using namespace QtMultimediaPrivate;

    uint32_t numFramesPadding;
    HRESULT hr = m_audioClient->GetCurrentPadding(&numFramesPadding);
    if (FAILED(hr)) {
        qWarning() << "IAudioClient3::GetCurrentPadding failed" << audioClientErrorString(hr);
        return false;
    }

    uint32_t requiredFrames = m_audioClientFrames - numFramesPadding;
    if (requiredFrames == 0)
        return true;

    uint32_t requiredSamples = requiredFrames * m_format.channelCount();

    // Grab the next empty buffer from the audio device.
    unsigned char *hostBuffer{};
    hr = m_renderClient->GetBuffer(requiredFrames, &hostBuffer);
    if (FAILED(hr)) {
        qWarning() << "IAudioRenderClient::getBuffer failed" << audioClientErrorString(hr);
        return false;
    }

    runAudioSinkCallback(m_audioCallback, reinterpret_cast<std::byte *>(hostBuffer),
                         requiredSamples, m_format);

    constexpr DWORD flags = 0;
    hr = m_renderClient->ReleaseBuffer(requiredFrames, flags);
    if (FAILED(hr)) {
        qWarning() << "IAudioRenderClient::ReleaseBuffer failed" << audioClientErrorString(hr);
        return false;
    }

    return true;
}

void QWASAPIAudioSinkStream::handleAudioClientError()
{
    using namespace QWindowsAudioUtils;
    audioClientStop(m_audioClient);
    audioClientReset(m_audioClient);

    QMetaObject::invokeMethod(&m_ringbufferDrained, [this] {
        handleIOError(m_parent);
    });
}

QWindowsAudioSink::QWindowsAudioSink(QAudioDevice audioDevice, const QAudioFormat &fmt,
                                     QObject *parent)
    : BaseClass(std::move(audioDevice), fmt, parent)
{
}

} // namespace QtWASAPI

QT_END_NAMESPACE
