// 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/qfunctions_win_p.h>
#include <QtCore/private/quniquehandle_types_p.h>
#include <QtMultimedia/private/qaudioformat_p.h>
#include <QtMultimedia/private/qaudiosystem_platform_stream_support_p.h>
#include <QtMultimedia/private/qmemory_resource_tlsf_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;

namespace {
QAudioFormat makeHostFormatForSource(const QAudioDevice &device, const QAudioFormat &format)
{
    const QWindowsAudioDevice *winDevice = QAudioDevicePrivate::handle<QWindowsAudioDevice>(device);

    auto status = winDevice->m_probeDataFuture.wait_for(QAudioDevicePrivate::formatProbeTimeout);
    switch (status) {
    case std::future_status::ready:
    case std::future_status::deferred:
        // proceed
        break;
    case std::future_status::timeout:
        return QAudioFormat{};
    default:
        Q_UNREACHABLE();
    }

    auto [minProbedChannels, maxProbedChannels] = winDevice->m_probeDataFuture.get().channelCountRange;
    auto [minProbedSampleRate, maxProbedSampleRate] = winDevice->m_probeDataFuture.get().sampleRateRange;

    QAudioFormat hostFormat = format;
    const int requestedChannelCount = format.channelCount();
    if (requestedChannelCount < device.minimumChannelCount()) {
        hostFormat.setChannelCount(minProbedChannels);
        hostFormat.setChannelConfig(
                QAudioFormat::defaultChannelConfigForChannelCount(minProbedChannels));
    } else if (requestedChannelCount > device.maximumChannelCount()) {
        hostFormat.setChannelCount(maxProbedChannels);
        hostFormat.setChannelConfig(
                QAudioFormat::defaultChannelConfigForChannelCount(maxProbedChannels));
    }

    const int requestedSampleRate = format.sampleRate();
    if (requestedSampleRate < device.minimumSampleRate())
        hostFormat.setSampleRate(minProbedSampleRate);
    else if (requestedSampleRate > device.maximumSampleRate())
        hostFormat.setSampleRate(maxProbedSampleRate);

    return hostFormat;
}
} // namespace

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

QWASAPIAudioSourceStream::~QWASAPIAudioSourceStream() = default;

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

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

    setQIODevice(ioDevice);
    createQIODeviceConnections(ioDevice);

    return startAudioClient();
}

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

    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();
    return started ? ioDevice : nullptr;
}

bool QWASAPIAudioSourceStream::start(AudioCallback &&cb)
{
    auto immDevice = QAudioDevicePrivate::handle<QWindowsAudioDevice>(m_audioDevice)->open();
    if (!immDevice)
        return false;

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

    m_audioCallback = std::move(cb);

    return startAudioClient();
}

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);

    joinWorkerThread();
    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_hostFormat, m_nativePeriodFrames, 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;
    }

    return true;
}

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

        std::optional<QComHelper> m_comHelper;

        if (m_hostFormat != m_format) {
            m_comHelper.emplace();
            m_resampler = std::make_unique<QWindowsResampler>();
            m_resampler->setup(m_hostFormat, m_format);

            m_memoryResource = std::make_unique<QTlsfMemoryResource>(512 * 1024);
        }

        runProcessLoop();
    }));

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

    bool clientStarted = audioClientStart(m_audioClient);
    if (!clientStarted) {
        joinWorkerThread();
        return false;
    }

    return true;
}

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 = m_audioCallback ? processCallback() : processRingbuffer();
        if (!success) {
            handleAudioClientError();
            return;
        }
    }
}

template <typename Functor>
bool QWASAPIAudioSourceStream::visitAudioClientBuffer(Functor &&f)
{
    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;
        }

        // Note: when starting the capture client, we an initial wakeup, despite no buffer being
        // available
        if (hostBufferFrames == 0)
            return true;

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

        if (m_resampler) {
            Q_UNLIKELY_BRANCH;
            auto resampledBuffer =
                    m_resampler->resample(as_bytes(hostBufferSpan), m_memoryResource.get());
            QPlatformAudioSourceStream::process(resampledBuffer,
                                                m_format.framesForBytes(resampledBuffer.size()));
        } else {
            f(as_bytes(hostBufferSpan), hostBufferFrames);
        }

        hr = m_captureClient->ReleaseBuffer(hostBufferFrames);

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

bool QWASAPIAudioSourceStream::processRingbuffer() noexcept QT_MM_NONBLOCKING
{
    return visitAudioClientBuffer(
            [&](QSpan<const std::byte> hostBuffer, uint32_t hostBufferFrames) {
        uint64_t framesWritten = QPlatformAudioSourceStream::process(hostBuffer, hostBufferFrames);
        if (framesWritten != hostBufferFrames)
            updateStreamIdle(true);
    });
}

bool QWASAPIAudioSourceStream::processCallback() noexcept QT_MM_NONBLOCKING
{
    return visitAudioClientBuffer([&](QSpan<const std::byte> hostBuffer, uint32_t) {
        runAudioCallback(*m_audioCallback, as_bytes(hostBuffer), m_format, volume());
    });
}

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

    invokeOnAppThread([this] {
        handleIOError(m_parent);
    });
}

void QWASAPIAudioSourceStream::joinWorkerThread()
{
    requestStop();
    ::SetEvent(m_wasapiHandle.get()); // force wakeup
    m_workerThread->wait();
    m_workerThread = {};
}

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

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

QWindowsAudioSource::~QWindowsAudioSource()
    = default;

} // namespace QtWASAPI

QT_END_NAMESPACE
