// 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 "qwindowsaudiosource_p.h"

#include <QtCore/qthread.h>
#include <QtCore/qloggingcategory.h>
#include <QtCore/private/quniquehandle_types_p.h>
#include <QtMultimedia/private/qaudiosystem_platform_stream_support_p.h>
#include <QtMultimedia/private/qwindowsaudiodevice_p.h>
#include <QtMultimedia/private/qwindowsaudioutils_p.h>

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

QT_BEGIN_NAMESPACE

namespace QtWASAPI {

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

QWASAPIAudioSourceStream::QWASAPIAudioSourceStream(QAudioDevice device, const QAudioFormat &format,
                                                   std::optional<qsizetype> ringbufferSize,
                                                   QWindowsAudioSource *parent,
                                                   float volume,
                                                   std::optional<int32_t> hardwareBufferFrames):
    QPlatformAudioSourceStream{
        std::move(device),
        format,
        ringbufferSize,
        hardwareBufferFrames,
        volume,
    },
    m_wasapiHandle {
        CreateEvent(0, false, false, nullptr),
    },
    m_parent{
        parent
    }
{
}

QWASAPIAudioSourceStream::~QWASAPIAudioSourceStream() = default;

bool QWASAPIAudioSourceStream::start(QIODevice *ioDevice)
{
    auto immDevice = QAudioDevicePrivate::handle<QWindowsAudioDevice>(m_audioDevice)->open();

    bool clientOpen = openAudioClient(std::move(immDevice));
    if (!clientOpen)
        return false;

    setQIODevice(ioDevice);
    createQIODeviceConnections(ioDevice);

    bool started = startAudioClient();
    if (!started)
        return false;

    return true;
}

QIODevice *QWASAPIAudioSourceStream::start()
{
    auto immDevice = QAudioDevicePrivate::handle<QWindowsAudioDevice>(m_audioDevice)->open();

    bool clientOpen = openAudioClient(std::move(immDevice));
    if (!clientOpen)
        return nullptr;

    QIODevice *ioDevice = createRingbufferReaderDevice();

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

    setQIODevice(ioDevice);
    createQIODeviceConnections(ioDevice);

    bool started = startAudioClient();
    if (!started)
        return nullptr;

    return ioDevice;
}

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

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

void QWASAPIAudioSourceStream::stop(ShutdownPolicy shutdownPolicy)
{
    m_parent = nullptr;
    m_shutdownPolicy = shutdownPolicy;

    requestStop();
    disconnectQIODeviceConnections();

    QWindowsAudioUtils::audioClientStop(m_audioClient);
    m_workerThread->wait();
    QWindowsAudioUtils::audioClientReset(m_audioClient);

    finalizeQIODevice(shutdownPolicy);
    if (shutdownPolicy == ShutdownPolicy::DiscardRingbuffer)
        emptyRingbuffer();
}

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

bool QWASAPIAudioSourceStream::openAudioClient(ComPtr<IMMDevice> device)
{
    using namespace QWindowsAudioUtils;

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

    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_captureClient.GetAddressOf()));
    if (FAILED(hr)) {
        qWarning() << "IAudioClient3::GetService failed to obtain IAudioCaptureClient"
                   << audioClientErrorString(hr);
        return false;
    }

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

    return true;
}

bool QWASAPIAudioSourceStream::startAudioClient()
{
    using namespace QWindowsAudioUtils;
    m_workerThread.reset(QThread::create([this] {
        setMCSSForPeriodSize(m_periodSize);
        runProcessLoop();
    }));

    m_workerThread->setObjectName(u"QWASAPIAudioSourceStream");
    m_workerThread->start();

    return audioClientStart(m_audioClient);
}

void QWASAPIAudioSourceStream::runProcessLoop()
{
    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; // TODO: distinguish between stop/reset?

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

bool QWASAPIAudioSourceStream::process() noexcept QT_MM_NONBLOCKING
{
    for (;;) {
        unsigned char *hostBuffer;
        uint32_t hostBufferFrames;
        DWORD flags;
        uint64_t devicePosition;
        uint64_t QPCPosition;
        HRESULT hr = m_captureClient->GetBuffer(&hostBuffer, &hostBufferFrames, &flags,
                                                &devicePosition, &QPCPosition);
        if (FAILED(hr)) {
            qWarning() << "IAudioCaptureClient::GetBuffer failed" << audioClientErrorString(hr);
            return false;
        }

        QSpan hostBufferSpan{
            hostBuffer,
            m_format.bytesForFrames(hostBufferFrames),
        };

        uint64_t framesWritten =
                QPlatformAudioSourceStream::process(as_bytes(hostBufferSpan), hostBufferFrames);
        if (framesWritten != hostBufferFrames)
            updateStreamIdle(true);

        hr = m_captureClient->ReleaseBuffer(hostBufferFrames);
        if (FAILED(hr)) {
            qWarning() << "IAudioCaptureClient::ReleaseBuffer failed" << audioClientErrorString(hr);
            return false;
        }

        uint32_t framesInNextPacket;
        hr = m_captureClient->GetNextPacketSize(&framesInNextPacket);

        if (FAILED(hr)) {
            qWarning() << "IAudioCaptureClient::GetNextPacketSize failed"
                       << audioClientErrorString(hr);
            return false;
        }

        if (framesInNextPacket == 0)
            return true;
    }
}

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

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

///////////////////////////////////////////////////////////////////////////////////////////////////

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

} // namespace QtWASAPI

QT_END_NAMESPACE
