// Copyright (C) 2022 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
// Qt-Security score:significant reason:default

#include <private/qpermissions_p.h>
#include <private/qstdweb_p.h>

#include <qglobalstatic.h>
#include <qpermissions.h>
#include <qmetaobject.h>
#include <qnamespace.h>
#include <qmetatype.h>
#include <qstring.h>
#include <qtimer.h>
#include <qhash.h>

#include <emscripten.h>
#include <emscripten/bind.h>
#include <emscripten/val.h>

#include <utility>
#include <string>
#include <queue>

QT_BEGIN_NAMESPACE

using namespace QPermissions::Private;
using namespace emscripten;

namespace
{
    constexpr const char *wapiGranted = "granted";
    constexpr const char *wapiDenied = "denied";
    constexpr const char *wapiPrompt = "prompt";
    constexpr const char *wapiCamera = "camera";
    constexpr const char *wapiVideoCapture = "video_capture"; // Alternative to "camera".
    constexpr const char *wapiMicrophone = "microphone";
    constexpr const char *wapiAudioCapture = "audio_capture"; // Alternative to "microphone".
    constexpr const char *wapiGeolocation = "geolocation";

    void updatePermission(const std::string &name, const std::string &state,
                          PermissionCallback callback);

    void checkPermission(const std::string &permissionName)
    {
        val permissions = val::global("navigator")["permissions"];
        if (permissions.isUndefined() || permissions.isNull())
            return;

        qstdweb::PromiseCallbacks callbacks;
        callbacks.thenFunc = [permissionName](val permissionState)
        {
            updatePermission(permissionName, permissionState["state"].as<std::string>(), {});
        };
        callbacks.catchFunc = [permissionName](val err)
        {
            if (err["name"].as<std::string>() == "NotAllowedError"
                || err["name"].as<std::string>() == "NotFoundError")
                return updatePermission(permissionName, wapiDenied, {});

            qCInfo(lcPermissions, "'%s' '%s'", err["name"].as<std::string>().c_str(),
                err["message"].as<std::string>().c_str());
        };

        val query = val::object();
        query.set("name", val(permissionName));

        qstdweb::Promise::make(permissions, QStringLiteral("query"), callbacks, query);
    }

    void checkPermissions()
    {
        checkPermission(wapiCamera);
        checkPermission(wapiMicrophone);
        checkPermission(wapiGeolocation);
    }

    void bootstrapCheckPermissions()
    {
        QTimer::singleShot(0, []{checkPermissions();});
    }

    Q_CONSTRUCTOR_FUNCTION(bootstrapCheckPermissions);

    int permissionTypeIdFromString(const std::string &permission)
    {
        if (permission == wapiCamera || permission == wapiVideoCapture)
            return qMetaTypeId<QCameraPermission>();
        if (permission == wapiMicrophone || permission == wapiAudioCapture)
            return qMetaTypeId<QMicrophonePermission>();
        if (permission == wapiGeolocation)
            return qMetaTypeId<QLocationPermission>();

        qCWarning(lcPermissions, "Unknown permission type '%s'", permission.c_str());

        return -1;
    }

    Qt::PermissionStatus permissionStatusFromString(const std::string &state)
    {
        if (state == wapiGranted)
            return Qt::PermissionStatus::Granted;
        if (state == wapiDenied)
            return Qt::PermissionStatus::Denied;
        if (state == wapiPrompt)
            return Qt::PermissionStatus::Undetermined;

        qCWarning(lcPermissions, "Unknown permission state '%s'", state.c_str());

        return Qt::PermissionStatus::Denied;
    }

    using PermissionHash = QHash<int, Qt::PermissionStatus>;
    Q_GLOBAL_STATIC(PermissionHash, permissionStatuses);

    void updatePermission(const std::string &name, const std::string &state, PermissionCallback callback)
    {
        qCDebug(lcPermissions) << "Updating" << name << "permission to" << state;

        const int type = permissionTypeIdFromString(name);
        if (type == -1)
            return; // Unknown permission type

        const auto status = permissionStatusFromString(state);
        (*permissionStatuses)[type] = status;

        if (callback)
            callback(status);
    }

    void requestMediaDevicePermission(const std::string &device, const PermissionCallback &cb)
    {
        Q_ASSERT(cb);

        val mediaDevices = val::global("navigator")["mediaDevices"];
        if (mediaDevices.isUndefined())
            return cb(Qt::PermissionStatus::Denied);

        qstdweb::PromiseCallbacks queryCallbacks;
        queryCallbacks.thenFunc = [device, cb](val stream)
        {
            // turn stream/light off
            if (!stream.isNull() && !stream.isUndefined() && !stream["getTracks"].isUndefined()) {
                emscripten::val tracks = stream.call<emscripten::val>("getTracks");
                if (!tracks.isUndefined() && tracks["length"].as<int>() > 0) {
                    for (int i = 0; i < tracks["length"].as<int>(); i++) {
                        tracks[i].call<void>("stop");
                    }
                }
            }
            stream = emscripten::val::null(); // this causes the garbage collection to turn off media light

            updatePermission(device, wapiGranted, cb);
        };
        queryCallbacks.catchFunc = [device, cb](val error)
        {
            qCInfo(lcPermissions) << "error"
                       << error["name"].as<std::string>();

            if (error["name"].as<std::string>() == "NotAllowedError")
                return updatePermission(device, wapiDenied, cb);
            updatePermission(device, wapiPrompt, cb);
        };

        val constraint = val::object();
        if (device == wapiCamera)
            constraint.set("video", true);

        constraint.set("audio", true);

        qstdweb::Promise::make(mediaDevices, QStringLiteral("getUserMedia"), queryCallbacks, constraint);
    }

    using GeoRequest = std::pair<QPermission, PermissionCallback>;
    Q_GLOBAL_STATIC(std::deque<GeoRequest>, geolocationRequestQueue);

    bool processingLocationRequest = false;

    void processNextGeolocationRequest();

    void geolocationSuccess(val position)
    {
        Q_UNUSED(position);
        Q_ASSERT(geolocationRequestQueue->size());

        processingLocationRequest = false;

        auto cb = geolocationRequestQueue->front().second;
        geolocationRequestQueue->pop_front();
        updatePermission(wapiGeolocation, wapiGranted, cb);
        processNextGeolocationRequest();
    }

    void geolocationError(val error)
    {
        Q_ASSERT(geolocationRequestQueue->size());

        static int deniedError = []
        {
            val posErr = val::global("GeolocationPositionError");
            if (posErr.isUndefined() || posErr.isNull())
                return 1;
            return posErr["PERMISSION_DENIED"].as<int>();
        }();

        processingLocationRequest = false;

        auto cb = geolocationRequestQueue->front().second;
        geolocationRequestQueue->pop_front();

        const int errorCode = error["code"].as<int>();
        updatePermission(wapiGeolocation, errorCode == deniedError ? wapiDenied : wapiPrompt, cb);
        processNextGeolocationRequest();
    }

    EMSCRIPTEN_BINDINGS(qt_permissions) {
        function("qtLocationSuccess", &geolocationSuccess);
        function("qtLocationError", &geolocationError);
    }

    void processNextGeolocationRequest()
    {
        if (processingLocationRequest)
            return;

        if (geolocationRequestQueue->empty())
            return;

        processingLocationRequest = true;

        val geolocation = val::global("navigator")["geolocation"];
        Q_ASSERT(!geolocation.isUndefined());
        Q_ASSERT(!geolocation.isNull());

        const auto &permission = geolocationRequestQueue->front().first;
        const auto locationPermission = *permission.value<QLocationPermission>();
        const bool highAccuracy = locationPermission.accuracy() == QLocationPermission::Precise;

        val options = val::object();
        options.set("enableHighAccuracy", highAccuracy ? true : false);
        geolocation.call<void>("getCurrentPosition", val::module_property("qtLocationSuccess"),
                               val::module_property("qtLocationError"), options);
    }

    void requestGeolocationPermission(const QPermission &permission, const PermissionCallback &cb)
    {
        Q_ASSERT(cb);
        Q_UNUSED(permission);
        Q_UNUSED(cb);

        val geolocation = val::global("navigator")["geolocation"];
        if (geolocation.isUndefined() || geolocation.isNull())
            return cb(Qt::PermissionStatus::Denied);

        if (processingLocationRequest)
            qCWarning(lcPermissions, "Permission to access location requested, while another request is in progress");

        geolocationRequestQueue->push_back(std::make_pair(permission, cb));
        processNextGeolocationRequest();
    }
} // Unnamed namespace

namespace QPermissions::Private
{
    Qt::PermissionStatus checkPermission(const QPermission &permission)
    {
        const auto it = permissionStatuses->find(permission.type().id());
        return it != permissionStatuses->end() ? it.value() : Qt::PermissionStatus::Undetermined;
    }

    void requestPermission(const QPermission &permission, const PermissionCallback &callback)
    {
        Q_ASSERT(permission.type().isValid());
        Q_ASSERT(callback);

        const auto status = checkPermission(permission);
        if (status != Qt::PermissionStatus::Undetermined)
            return callback(status);

        const int requestedTypeId = permission.type().id();
        if (requestedTypeId == qMetaTypeId<QCameraPermission>())
            return requestMediaDevicePermission(wapiCamera, callback);

        if (requestedTypeId == qMetaTypeId<QMicrophonePermission>())
            return requestMediaDevicePermission(wapiMicrophone, callback);

        if (requestedTypeId == qMetaTypeId<QLocationPermission>())
            return requestGeolocationPermission(permission, callback);

        (*permissionStatuses)[requestedTypeId] = Qt::PermissionStatus::Denied;
        callback(Qt::PermissionStatus::Denied);
    }
}

QT_END_NAMESPACE
