// Copyright (C) 2021 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 "qwasmmediadevices_p.h"
#include "private/qcameradevice_p.h"
#include "private/qplatformmediaintegration_p.h"
#include "qwasmwebaudiosource_p.h"
#include "qwasmwebaudiosink_p.h"
#include "qwasmaudiodevice_p.h"

#include <QMap>
#include <QDebug>

#include <emscripten.h>

QT_BEGIN_NAMESPACE

Q_LOGGING_CATEGORY(qWasmMediaDevices, "qt.multimedia.wasm.mediadevices")

static QWasmMediaDevices *s_mediaDevicesInstance = nullptr;
static bool s_constructingInstance = false;

bool isFirefox() {
    return !emscripten::val::global("InstallTrigger").isUndefined();
}


// Firefox only as it limits enumerateDevices to inputs only when no permissions are given
extern "C" {
EMSCRIPTEN_KEEPALIVE void qtMediaDevicesOnAudioOutputSelected()
{
    if (QWasmMediaDevices *instance = QWasmMediaDevices::instance())
        instance->getMediaDevices();
}
} // extern "C"

EM_JS(void, setupAudioOutputSelector, (), {
    const overlay = document.createElement('div');
    overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.5);z-index:9999;display:flex;align-items:center;justify-content:center;';

    const dialog = document.createElement('div');
    dialog.style.cssText = 'background:white;padding:24px;border-radius:8px;text-align:center;font-family:sans-serif;min-width:240px;';

    const message = document.createElement('p');
    message.textContent = 'Select an audio output device to continue.';
    message.style.cssText = 'margin:0 0 16px 0;font-size:14px;';

    const button = document.createElement('button');
    button.textContent = 'Select Audio Output';
    button.style.cssText = 'padding:8px 16px;font-size:14px;cursor:pointer;';

    button.addEventListener('click', async () => {
        document.body.removeChild(overlay);
        try {
            const deviceInfo = await navigator.mediaDevices.selectAudioOutput();
            console.log("Selected device: ", deviceInfo.label);
            Module._qtMediaDevicesOnAudioOutputSelected();
        } catch (err) {
            console.error(err);
        }
    }, { once: true });

    dialog.appendChild(message);
    dialog.appendChild(button);
    overlay.appendChild(dialog);
    document.body.appendChild(overlay);
});

QWasmCameraDevices::QWasmCameraDevices(QPlatformMediaIntegration *integration)
    : QPlatformVideoDevices(integration)
{
}

QList<QCameraDevice> QWasmCameraDevices::findVideoInputs() const
{
    return QWasmMediaDevices::instance() ? QWasmMediaDevices::instance()->videoInputs() : QList<QCameraDevice>();
}

void QWasmCameraDevices::connectNotify(const QMetaMethod &signal)
{
    Q_ASSERT(QThread::isMainThread());
    QWasmMediaDevices::instance();
}

QWasmAudioDevices::QWasmAudioDevices() = default;

QPlatformAudioSource *QWasmAudioDevices::createAudioSource(const QAudioDevice &deviceInfo,
                                                           const QAudioFormat &fmt,
                                                           QObject *parent)
{
    return new QWasmAudioSource(deviceInfo, fmt, parent);
}

QPlatformAudioSink *QWasmAudioDevices::createAudioSink(const QAudioDevice &deviceInfo,
                                                       const QAudioFormat &fmt,
                                                       QObject *parent)
{
    return new QWasmAudioSink(deviceInfo, fmt, parent);
}

QList<QAudioDevice> QWasmAudioDevices::findAudioInputs() const
{
    return QWasmMediaDevices::instance() ? QWasmMediaDevices::instance()->audioInputs() : QList<QAudioDevice>();
}

QList<QAudioDevice> QWasmAudioDevices::findAudioOutputs() const
{
    return QWasmMediaDevices::instance() ? QWasmMediaDevices::instance()->audioOutputs() : QList<QAudioDevice>();
}

void QWasmAudioDevices::connectNotify(const QMetaMethod &signal)
{
    Q_ASSERT(QThread::isMainThread());
    QWasmMediaDevices::instance();
}

QWasmMediaDevices::QWasmMediaDevices()
{
}

QWasmMediaDevices *QWasmMediaDevices::instance()
{
    if (s_mediaDevicesInstance)
        return s_mediaDevicesInstance;
    if (s_constructingInstance)
        return nullptr;
    s_constructingInstance = true;
    s_mediaDevicesInstance = new QWasmMediaDevices();
    s_constructingInstance = false;
    s_mediaDevicesInstance->initDevices();
    return s_mediaDevicesInstance;
}

void QWasmMediaDevices::initDevices()
{
    if (m_initDone)
        return;

    m_initDone = true;
    if (isFirefox())
        setupAudioOutputSelector();
    else
        getMediaDevices(); // asynchronous
}

QList<QCameraDevice> QWasmMediaDevices::videoInputs() const
{
    return m_cameraDevices.values();
}

QList<QAudioDevice> QWasmMediaDevices::audioInputs() const
{
    return m_audioInputs.values();
}

QList<QAudioDevice> QWasmMediaDevices::audioOutputs() const
{
    return m_audioOutputs.values();
}

void QWasmMediaDevices::parseDevices(emscripten::val devices)
{
    if (devices.isNull() || devices.isUndefined()) {
        qWarning() << "Something went wrong enumerating devices";
        return;
    }

    QList<std::string> cameraDevicesToRemove = m_cameraDevices.keys();
    QList<std::string> audioOutputsToRemove;
    QList<std::string> audioInputsToRemove;

    audioOutputsToRemove = m_audioOutputs.keys();
    audioInputsToRemove = m_audioInputs.keys();
    m_audioInputsAdded = false;
    m_audioOutputsAdded = false;
    m_videoInputsAdded = false;

    bool m_videoInputsRemoved = false;
    bool m_audioInputsRemoved = false;
    bool m_audioOutputsRemoved = false;

    for (int i = 0; i < devices["length"].as<int>(); i++) {

        emscripten::val mediaDevice = devices[i];

        const std::string deviceKind = mediaDevice["kind"].as<std::string>();
        std::string label = mediaDevice["label"].as<std::string>();
        std::string deviceId = mediaDevice["deviceId"].as<std::string>();

        qCDebug(qWasmMediaDevices) << QString::fromStdString(deviceKind)
                                   << QString::fromStdString(deviceId)
                                   << QString::fromStdString(label);

        if (deviceId.empty()) { // no permissions we'll use System;
            label = "System " + deviceKind;
            deviceId = label;
        }
        if (deviceKind.empty())
            continue;
        bool isDefault = false;

        if (deviceKind == std::string("videoinput")) {
            if (!m_cameraDevices.contains(deviceId)) {
                QCameraDevicePrivate *camera = new QCameraDevicePrivate; // QSharedData
                camera->id = QString::fromStdString(deviceId).toUtf8();
                camera->description = QString::fromUtf8(label.c_str());
                // no camera defaults, first in wins!
                camera->isDefault = !m_videoInputsAdded;
                m_cameraDevices.insert(deviceId, camera->create());
                m_videoInputsAdded = true;
            }
            cameraDevicesToRemove.removeOne(deviceId);
        } else if (deviceKind == std::string("audioinput")) {
            if (!m_audioInputs.contains(deviceId)) {
                isDefault = !m_audioInputsAdded;
                m_audioInputs.insert(
                        deviceId,
                        QAudioDevicePrivate::createQAudioDevice(std::make_unique<QWasmAudioDevice>(
                                deviceId.c_str(), label.c_str(), isDefault, QAudioDevice::Input)));

                m_audioInputsAdded = true;
            }
            audioInputsToRemove.removeOne(deviceId);
        } else if (deviceKind == std::string("audiooutput")) {
            if (!m_audioOutputs.contains(deviceId)) {
                isDefault = !m_audioOutputsAdded;
                m_audioOutputs.insert(
                        deviceId,
                        QAudioDevicePrivate::createQAudioDevice(std::make_unique<QWasmAudioDevice>(
                                deviceId.c_str(), label.c_str(), isDefault, QAudioDevice::Output)));

                m_audioOutputsAdded = true;
            }
            audioOutputsToRemove.removeOne(deviceId);
        }
        // if permissions are given label will hold the actual
        // camera name, such as "Live! Cam Sync 1080p (041e:409d)"
    }
    // any left here were removed
    int j = 0;
    for (; j < cameraDevicesToRemove.count(); j++) {
        m_cameraDevices.remove(cameraDevicesToRemove.at(j));
    }
    m_videoInputsRemoved = !cameraDevicesToRemove.isEmpty();

    for (j = 0; j < audioInputsToRemove.count(); j++) {
        m_audioInputs.remove(audioInputsToRemove.at(j));
    }
    m_audioInputsRemoved = !audioInputsToRemove.isEmpty();

    for (j = 0; j < audioOutputsToRemove.count(); j++) {
        m_audioOutputs.remove(audioOutputsToRemove.at(j));
    }
    m_audioOutputsRemoved = !audioOutputsToRemove.isEmpty();

    if (m_videoInputsAdded || m_videoInputsRemoved) {
        auto videoDevices = static_cast<QWasmCameraDevices*>(QPlatformMediaIntegration::instance()->videoDevices());
        videoDevices->onVideoInputsChanged();
    }
    if (m_audioInputsAdded || m_audioInputsRemoved) {
        auto audioDevices = static_cast<QWasmAudioDevices*>(QPlatformMediaIntegration::instance()->audioDevices());
        audioDevices->onAudioInputsChanged();
    }
    if (!m_audioOutputsAdded) {
        // Firefox and Safari require mic or camera permissions
        // (or selectAudioOutput for Firefox)
        // to enumerate output devices, so we just fake one.
        // The device actually does not require perms to play.
        m_audioOutputs.insert(
                "",
                QAudioDevicePrivate::createQAudioDevice(std::make_unique<QWasmAudioDevice>(
                        "", "System output", true, QAudioDevice::Output)));
        m_audioOutputsAdded = true;
    }
    if (m_audioOutputsAdded || m_audioOutputsRemoved) {
        auto audioDevices = static_cast<QWasmAudioDevices*>(QPlatformMediaIntegration::instance()->audioDevices());
        audioDevices->onAudioOutputsChanged();
    }

}

void QWasmMediaDevices::getMediaDevices()
{
    emscripten::val navigator = emscripten::val::global("navigator");
    m_jsMediaDevicesInterface = navigator["mediaDevices"];

    if (m_jsMediaDevicesInterface.isNull() || m_jsMediaDevicesInterface.isUndefined()) {
        qWarning() << "No media devices found";
        return;
    }

    if (qstdweb::haveAsyncify()) {

#ifdef QT_HAVE_EMSCRIPTEN_ASYNCIFY
        auto asyncEnumerate = [](void *arg){
            QWasmMediaDevices *mediaDevices = static_cast<QWasmMediaDevices *>(arg);
            mediaDevices->devicesList = mediaDevices->m_jsMediaDevicesInterface.call<emscripten::val>("enumerateDevices").await();
            if (mediaDevices->devicesList.isNull() || mediaDevices->devicesList.isUndefined()) {
                qWarning() << "devices list error";
                return;
            }
            mediaDevices->parseDevices(mediaDevices->devicesList);
        };

        asyncEnumerate(this);

        m_deviceChangedCallback = std::make_unique<qstdweb::EventCallback>(
                m_jsMediaDevicesInterface, "devicechange",
                [this, asyncEnumerate](emscripten::val) {
                    asyncEnumerate(this);
                });
#endif

    } else {

        qstdweb::PromiseCallbacks enumerateDevicesCallback{
            .thenFunc =
            [&](emscripten::val devices) {
                parseDevices(devices);
            },
            .catchFunc =
            [this](emscripten::val error) {
                qWarning() << "mediadevices enumerateDevices fail"
                           << QString::fromStdString(error["name"].as<std::string>())
                           << QString::fromStdString(error["message"].as<std::string>());
                m_initDone = false;
            }
        };

        qstdweb::Promise::make(m_jsMediaDevicesInterface,
                               QStringLiteral("enumerateDevices"),
                               std::move(enumerateDevicesCallback));

        // setup devicechange monitor
        m_deviceChangedCallback = std::make_unique<qstdweb::EventCallback>(
                m_jsMediaDevicesInterface, "devicechange",
                [this, enumerateDevicesCallback](emscripten::val) {
                    qstdweb::Promise::make(m_jsMediaDevicesInterface,
                                           QStringLiteral("enumerateDevices"),
                                           std::move(enumerateDevicesCallback));
                });
    }

}

QT_END_NAMESPACE
