/*
 * Copyright (C) 2020 Igalia S.L. All rights reserved.
 * Copyright (C) 2022 Apple Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
 * THE POSSIBILITY OF SUCH DAMAGE.
 */

#include "config.h"
#include "WebXRSystem.h"

#if ENABLE(WEBXR)

#include "Chrome.h"
#include "ChromeClient.h"
#include "DOMWindow.h"
#include "Document.h"
#include "FeaturePolicy.h"
#include "IDLTypes.h"
#include "JSWebXRSession.h"
#include "JSXRReferenceSpaceType.h"
#include "Navigator.h"
#include "Page.h"
#include "PlatformXR.h"
#include "RequestAnimationFrameCallback.h"
#include "RuntimeEnabledFeatures.h"
#include "SecurityOrigin.h"
#include "UserGestureIndicator.h"
#include "WebXRSession.h"
#include "XRReferenceSpaceType.h"
#include "XRSessionInit.h"
#include <JavaScriptCore/JSCJSValue.h>
#include <JavaScriptCore/JSGlobalObject.h>
#include <JavaScriptCore/JSString.h>
#include <wtf/IsoMallocInlines.h>
#include <wtf/Scope.h>

namespace WebCore {

WTF_MAKE_ISO_ALLOCATED_IMPL(WebXRSystem);

Ref<WebXRSystem> WebXRSystem::create(Navigator& navigator)
{
    auto system = adoptRef(*new WebXRSystem(navigator));
    system->suspendIfNeeded();
    return system;
}

WebXRSystem::WebXRSystem(Navigator& navigator)
    : ActiveDOMObject(navigator.scriptExecutionContext())
    , m_navigator(navigator)
    , m_defaultInlineDevice(*navigator.scriptExecutionContext())
{
    m_inlineXRDevice = m_defaultInlineDevice;
}

WebXRSystem::~WebXRSystem() = default;

Navigator* WebXRSystem::navigator()
{
    return m_navigator.get();
}

// https://immersive-web.github.io/webxr/#ensures-an-immersive-xr-device-is-selected
void WebXRSystem::ensureImmersiveXRDeviceIsSelected(CompletionHandler<void()>&& callback)
{
    // Don't ask platform code for XR devices, we're using simulated ones.
    if (UNLIKELY(m_testingDevices)) {
        callback();
        return;
    }

    if (m_activeImmersiveDevice) {
        callback();
        return;
    }

    // https://immersive-web.github.io/webxr/#enumerate-immersive-xr-devices
    auto document = downcast<Document>(scriptExecutionContext());
    if (!document || !document->page()) {
        callback();
        return;
    }

    bool isFirstXRDevicesEnumeration = !m_immersiveXRDevicesHaveBeenEnumerated;
    document->page()->chrome().client().enumerateImmersiveXRDevices([this, protectedThis = Ref { *this }, isFirstXRDevicesEnumeration, callback = WTFMove(callback)](auto& immersiveXRDevices) mutable {
        m_immersiveXRDevicesHaveBeenEnumerated = true;

        auto callbackOnExit = makeScopeExit([&]() {
            callOnMainThread(WTFMove(callback));
        });

        // https://immersive-web.github.io/webxr/#select-an-immersive-xr-device
        auto* oldDevice = m_activeImmersiveDevice.get();
        if (immersiveXRDevices.isEmpty()) {
            m_activeImmersiveDevice = nullptr;
            return;
        }
        if (immersiveXRDevices.size() == 1) {
            m_activeImmersiveDevice = immersiveXRDevices.first().get();
            return;
        }

        if (m_activeImmersiveSession && oldDevice && immersiveXRDevices.findIf([&](auto& entry) { return entry.ptr() == oldDevice; }) != notFound)
            ASSERT(m_activeImmersiveDevice.get() == oldDevice);
        else {
            // FIXME: implement a better UA selection mechanism if required.
            m_activeImmersiveDevice = immersiveXRDevices.first().get();
        }

        if (isFirstXRDevicesEnumeration || m_activeImmersiveDevice.get() == oldDevice) {
            return;
        }

        // FIXME: 7. Shut down any active XRSessions.
        // FIXME: 8. Set the XR compatible boolean of all WebGLRenderingContextBase instances to false.
        // FIXME: 9. Queue a task to fire an event named devicechange on the context object.
    });
}

void WebXRSystem::obtainCurrentDevice(XRSessionMode mode, const JSFeatureList& requiredFeatures, const JSFeatureList& optionalFeatures, CompletionHandler<void(PlatformXR::Device*)>&& callback)
{
    if (mode == XRSessionMode::ImmersiveAr || mode == XRSessionMode::ImmersiveVr) {
        ensureImmersiveXRDeviceIsSelected([this, callback = WTFMove(callback)]() mutable {
            callback(m_activeImmersiveDevice.get());
        });
        return;
    }
    if (!requiredFeatures.isEmpty() || !optionalFeatures.isEmpty()) {
        callback(m_inlineXRDevice.get());
        return;
    }

    callback(&m_defaultInlineDevice);
}


// https://immersive-web.github.io/webxr/#dom-xrsystem-issessionsupported
void WebXRSystem::isSessionSupported(XRSessionMode mode, IsSessionSupportedPromise&& promise)
{
    // 1. Let promise be a new Promise.
    // 2. If mode is "inline", resolve promise with true and return it.
    if (mode == XRSessionMode::Inline) {
        promise.resolve(true);
        return;
    }

    // 3. If the requesting document's origin is not allowed to use the "xr-spatial-tracking" feature policy,
    //    reject promise with a "SecurityError" DOMException and return it.
    auto document = downcast<Document>(scriptExecutionContext());
    if (!isFeaturePolicyAllowedByDocumentAndAllOwners(FeaturePolicy::Type::XRSpatialTracking, *document, LogFeaturePolicyFailure::Yes)) {
        promise.reject(Exception { SecurityError });
        return;
    }

    // 4. Run the following steps in parallel:
    // 4.1 Ensure an immersive XR device is selected.
    ensureImmersiveXRDeviceIsSelected([this, promise = WTFMove(promise), mode]() mutable {
        // 4.2 If the immersive XR device is null, resolve promise with false and abort these steps.
        if (!m_activeImmersiveDevice) {
            promise.resolve(false);
            return;
        }

        // 4.3 If the immersive XR device's list of supported modes does not contain mode, resolve promise with false and abort these steps.
        if (!m_activeImmersiveDevice->supports(mode)) {
            promise.resolve(false);
            return;
        }

        // 4.4 Resolve promise with true.
        promise.resolve(true);
    });
}

// https://immersive-web.github.io/webxr/#immersive-session-request-is-allowed
bool WebXRSystem::immersiveSessionRequestIsAllowedForGlobalObject(DOMWindow& globalObject, Document& document) const
{
    // 1. If the request was not made while the global object has transient
    //    activation or when launching a web application, return false
    // TODO: add a check for "not launching a web application".
    if (!globalObject.hasTransientActivation())
        return false;

    // 2. If the requesting document is not considered trustworthy, return false.
    // https://immersive-web.github.io/webxr/#trustworthy.
    if (&document != globalObject.document())
        return false;

    // https://immersive-web.github.io/webxr/#active-and-focused
    if (!document.hasFocus() || !document.securityOrigin().isSameOriginAs(globalObject.document()->securityOrigin()))
        return false;

    // 3. If user intent to begin an immersive session is not well understood,
    //    either via explicit consent or implicit consent, return false
    if (!UserGestureIndicator::processingUserGesture())
        return false;

    return true;
}

// https://immersive-web.github.io/webxr/#inline-session-request-is-allowed
bool WebXRSystem::inlineSessionRequestIsAllowedForGlobalObject(DOMWindow& globalObject, Document& document, const XRSessionInit& init) const
{
    auto isEmptyOrViewer = [&document](const JSFeatureList& features) {
        if (features.isEmpty())
            return true;
        if (features.size() == 1 && document.globalObject()) {
            auto featureString = features.first().toWTFString(document.globalObject());
            auto sessionFeature = PlatformXR::parseSessionFeatureDescriptor(featureString);
            if (sessionFeature && *sessionFeature == PlatformXR::SessionFeature::ReferenceSpaceTypeViewer)
                return true;
        }
        return false;
    };

    // 1. If the session request contained any required features or optional features (besides 'viewer') and the request was not made
    //    while the global object has transient activation or when launching a web application, return false.
    bool sessionRequestContainedAnyFeature = !isEmptyOrViewer(init.optionalFeatures) || !isEmptyOrViewer(init.requiredFeatures);
    if (sessionRequestContainedAnyFeature && !globalObject.hasTransientActivation() /* TODO: || !launching a web app */)
        return false;

    // 2. If the requesting document is not responsible, return false.
    if (&document != globalObject.document())
        return false;

    return true;
}


struct WebXRSystem::ResolvedRequestedFeatures {
    FeatureList granted;
    FeatureList consentRequired;
    FeatureList consentOptional;
    FeatureList requested;
};

static bool featureRequiresExplicitConsent(PlatformXR::SessionFeature feature)
{
#if ENABLE(WEBXR_HANDS)
    if (feature == PlatformXR::SessionFeature::HandTracking)
        return true;
#else
    UNUSED_PARAM(feature);
#endif
    return false;
}

bool WebXRSystem::isFeatureSupported(PlatformXR::SessionFeature feature, XRSessionMode mode, const PlatformXR::Device& device) const
{
    if (!device.supportedFeatures(mode).contains(feature))
        return false;

#if ENABLE(WEBXR_HANDS)
    if (feature == PlatformXR::SessionFeature::HandTracking) {
        auto scriptExecutionContext = this->scriptExecutionContext();
        if (!scriptExecutionContext || !scriptExecutionContext->settingsValues().webXRHandInputModuleEnabled)
            return false;
    }
#endif

    return true;
}

#define RETURN_FALSE_OR_CONTINUE(mustReturn) { \
    if (mustReturn) {\
        return false; \
    } \
    continue; \
}

// https://immersive-web.github.io/webxr/#resolve-the-requested-features
std::optional<WebXRSystem::ResolvedRequestedFeatures> WebXRSystem::resolveRequestedFeatures(XRSessionMode mode, const XRSessionInit& init, PlatformXR::Device* device, JSC::JSGlobalObject& globalObject) const
{
    // 1. Let consentRequired be an empty list of DOMString.
    // 2. Let consentOptional be an empty list of DOMString.
    ResolvedRequestedFeatures resolvedFeatures;

    // 3. Let device be the result of obtaining the current device for mode, requiredFeatures, and optionalFeatures.
    // 4. Let granted be a list of DOMString initialized to device's list of enabled features for mode.
    // 5. If device is null or device's list of supported modes does not contain mode, run the following steps:
    //  5.1 Return the tuple (consentRequired, consentOptional, granted)
    if (!device || !device->supports(mode))
        return resolvedFeatures;

    resolvedFeatures.granted = device->enabledFeatures(mode);

    // 6. Add every feature descriptor in the default features table associated
    //    with mode to the indicated feature list if it is not already present.
    // https://immersive-web.github.io/webxr/#default-features
    auto requiredFeaturesWithDefaultFeatures = init.requiredFeatures;
    requiredFeaturesWithDefaultFeatures.append(JSC::jsStringWithCache(globalObject.vm(), PlatformXR::sessionFeatureDescriptor(PlatformXR::SessionFeature::ReferenceSpaceTypeViewer)));
    if (mode == XRSessionMode::ImmersiveAr || mode == XRSessionMode::ImmersiveVr)
        requiredFeaturesWithDefaultFeatures.append(JSC::jsStringWithCache(globalObject.vm(), PlatformXR::sessionFeatureDescriptor(PlatformXR::SessionFeature::ReferenceSpaceTypeLocal)));

    // 7. For each feature in requiredFeatures|optionalFeatures perform the following steps:
    // 8. For each feature in optionalFeatures perform the following steps:
    // We're merging both loops in a single lambda. The only difference is that a failure on any required features
    // implies cancelling the whole process while failures in optional features are just skipped.
    enum class ParsingMode { Strict, Loose };
    auto parseFeatures = [this, &device, &globalObject, mode, &resolvedFeatures] (const JSFeatureList& sessionFeatures, ParsingMode parsingMode) -> bool {
        bool returnOnFailure = parsingMode == ParsingMode::Strict;
        for (const auto& sessionFeature : sessionFeatures) {
            // 1. If the feature is null, continue to the next entry.
            if (sessionFeature.isNull())
                continue;

            // FIXME: This is only testing for XRReferenceSpaceType features. It
            // needs to handle optional features like "hand-tracking".

            // 2. If feature is not a valid feature descriptor, perform the following steps
            //   2.1. Let s be the result of calling ? ToString(feature).
            //   2.2. If s is not a valid feature descriptor or is undefined, (return null|continue to next entry).
            //   2.3. Set feature to s.
            auto featureString = sessionFeature.toWTFString(&globalObject);
            auto feature = PlatformXR::parseSessionFeatureDescriptor(featureString);
            if (!feature)
                RETURN_FALSE_OR_CONTINUE(returnOnFailure);

            // 3. If feature is already in granted, continue to the next entry.
            if (resolvedFeatures.granted.contains(feature.value())) {
                resolvedFeatures.requested.append(feature.value());
                continue;
            }

            // 4. If the requesting document's origin is not allowed to use any feature policy required by feature
            //    as indicated by the feature requirements table, (return null|continue to next entry).

            // 5. If session's XR device is not capable of supporting the functionality described by feature or the
            //    user agent has otherwise determined to reject the feature, (return null|continue to next entry).
            if (!isFeatureSupported(feature.value(), mode, *device))
                RETURN_FALSE_OR_CONTINUE(returnOnFailure);

            // 6. If the functionality described by feature requires explicit consent, append it to (consentRequired|consentOptional).
            if (featureRequiresExplicitConsent(feature.value())) {
                if (parsingMode == ParsingMode::Strict)
                    resolvedFeatures.consentRequired.append(feature.value());
                else
                    resolvedFeatures.consentOptional.append(feature.value());
            } else {
                // 7. Else append feature to granted.
                resolvedFeatures.granted.append(feature.value());
            }

            // https://immersive-web.github.io/webxr/#requested-features
            // The combined list of feature descriptors given by the requiredFeatures and optionalFeatures are collectively considered the requested features for an XRSession.
            resolvedFeatures.requested.append(feature.value());
        }
        return true;
    };

    if (!parseFeatures(requiredFeaturesWithDefaultFeatures, ParsingMode::Strict))
        return std::nullopt;

    parseFeatures(init.optionalFeatures, ParsingMode::Loose);
    return resolvedFeatures;
}

// https://immersive-web.github.io/webxr/#request-the-xr-permission
void WebXRSystem::resolveFeaturePermissions(XRSessionMode mode, const XRSessionInit& init, PlatformXR::Device* device, JSC::JSGlobalObject& globalObject, CompletionHandler<void(std::optional<FeatureList>&&)>&& completionHandler) const
{
    // 1. Set status's granted to an empty FrozenArray.
    // 2. Let requiredFeatures be descriptor's requiredFeatures.
    // 3. Let optionalFeatures be descriptor's optionalFeatures.
    // 4. Let device be the result of obtaining the current device for mode, requiredFeatures, and optionalFeatures.

    // 5. Let result be the result of resolving the requested features given requiredFeatures,optionalFeatures, and mode.
    auto resolvedFeatures = resolveRequestedFeatures(mode, init, device, globalObject);

    // 6. If result is null, run the following steps:
    //  6.1. Set status's state to "denied".
    //  6.2. Abort these steps.
    if (!resolvedFeatures) {
        completionHandler(std::nullopt);
        return;
    }

    // 7. Let (consentRequired, consentOptional, granted) be the fields of result.
    // 8. The user agent MAY at this point ask the user's permission for the calling algorithm to use any of the features
    //    in consentRequired and consentOptional. The results of these prompts should be included when determining if there
    //    is a clear signal of user intent for enabling these features.
    // 9. For each feature in consentRequired perform the following steps:
    //  9.1. The user agent MAY at this point ask the user's permission for the calling algorithm to use feature. The results
    //       of these prompts should be included when determining if there is a clear signal of user intent to enable feature.
    //  9.2. If a clear signal of user intent to enable feature has not been determined, set status's state to "denied" and
    //       abort these steps.
    //  9.3. If feature is not in granted, append feature to granted.
    // 10. For each feature in consentOptional perform the following steps:
    //  10.1. The user agent MAY at this point ask the user's permission for the calling algorithm to use feature. The results
    //        of these prompts should be included when determining if there is a clear signal of user intent to enable feature.
    //  10.2. If a clear signal of user intent to enable feature has not been determined, continue to the next entry.
    //  10.3. If feature is not in granted, append feature to granted.
    auto document = downcast<Document>(scriptExecutionContext());
    if (!document || !document->page()) {
        completionHandler(std::nullopt);
        return;
    }

    // FIXME: Replace with Permissions API implementation.
    document->page()->chrome().client().requestPermissionOnXRSessionFeatures(document->securityOrigin().data(), mode, resolvedFeatures->granted, resolvedFeatures->consentRequired, resolvedFeatures->consentOptional, [device, mode, consentRequired = WTFMove(resolvedFeatures->consentRequired), completionHandler = WTFMove(completionHandler)](std::optional<PlatformXR::Device::FeatureList>&& userGranted) mutable {
        if (!userGranted) {
            completionHandler(std::nullopt);
            return;
        }

        for (auto featureRequiringConsent : consentRequired) {
            if (!userGranted->contains(featureRequiringConsent)) {
                completionHandler(std::nullopt);
                return;
            }
        }

        // 11. Set status's granted to granted.
        // 12. Set device's list of enabled features for mode to granted.
        // 13. Set status's state to "granted".
        device->setEnabledFeatures(mode, *userGranted);

        completionHandler(*userGranted);
    });
}


// https://immersive-web.github.io/webxr/#dom-xrsystem-requestsession
void WebXRSystem::requestSession(Document& document, XRSessionMode mode, const XRSessionInit& init, RequestSessionPromise&& promise)
{
    // 1. Let promise be a new Promise.
    // 2. Let immersive be true if mode is an immersive session mode, and false otherwise.
    // 3. Let global object be the relevant Global object for the XRSystem on which this method was invoked.
    bool immersive = mode == XRSessionMode::ImmersiveAr || mode == XRSessionMode::ImmersiveVr;
    Ref protectedDocument { document };
    auto* globalObject = protectedDocument->domWindow();
    if (!globalObject) {
        promise.reject(Exception { InvalidAccessError});
        return;
    }

    // 4. Check whether the session request is allowed as follows:
    if (immersive) {
        if (!immersiveSessionRequestIsAllowedForGlobalObject(*globalObject, document)) {
            promise.reject(Exception { SecurityError });
            return;
        }
        if (m_pendingImmersiveSession || m_activeImmersiveSession) {
            promise.reject(Exception { InvalidStateError });
            return;
        }
        m_pendingImmersiveSession = true;
    } else if (!inlineSessionRequestIsAllowedForGlobalObject(*globalObject, document, init)) {
        promise.reject(Exception { SecurityError });
        return;
    }

    // 5. Run the following steps in parallel:
    // 5.1 Let requiredFeatures be options' requiredFeatures.
    // 5.2 Let optionalFeatures be options' optionalFeatures.
    // 5.3 Set device to the result of obtaining the current device for mode, requiredFeatures, and optionalFeatures.
    // 5.4 Queue a task to perform the following steps:
    obtainCurrentDevice(mode, init.requiredFeatures, init.optionalFeatures, [this, protectedDocument, immersive, init, mode, promise = WTFMove(promise)](auto* device) mutable {
        auto rejectPromiseWithNotSupportedError = makeScopeExit([&]() {
            promise.reject(Exception { NotSupportedError });
            m_pendingImmersiveSession = false;
        });

        // 5.4.1 If device is null or device's list of supported modes does not contain mode, run the following steps:
        //  - Reject promise with a "NotSupportedError" DOMException.
        //  - If immersive is true, set pending immersive session to false.
        //  - Abort these steps.
        if (!device || !device->supports(mode))
            return;

        auto* globalObject = protectedDocument->globalObject();
        if (!globalObject)
            return;

        rejectPromiseWithNotSupportedError.release();

        // WebKit does not currently support the Permissions API. https://w3c.github.io/permissions/
        // However we do implement here the permission request algorithm without the
        // Permissions API bits as it handles, among others, the session features parsing. We also
        // do it here before creating the session as there is no need to do it on advance.

        // 5.4.4 Let descriptor be an XRPermissionDescriptor initialized with session, requiredFeatures, and optionalFeatures
        // 5.4.5 Let status be an XRPermissionStatus, initially null
        // 5.4.6 Request the xr permission with descriptor and status.
        // 5.4.7 If status' state is "denied" run the following steps: (same as above in 5.4.1)
        resolveFeaturePermissions(mode, init, device, *globalObject, [this, weakThis = WeakPtr { *this }, protectedDocument, device, immersive, mode, promise](std::optional<FeatureList>&& requestedFeatures) mutable {
            if (!weakThis || !requestedFeatures) {
                promise.reject(Exception { NotSupportedError });
                m_pendingImmersiveSession = false;
                return;
            }

            // 5.4.2 Let session be a new XRSession object.
            // 5.4.3 Initialize the session with session, mode, and device.
            auto session = WebXRSession::create(protectedDocument.get(), *this, mode, *device, WTFMove(*requestedFeatures));

            // 5.4.8 Potentially set the active immersive session as follows:
            if (immersive) {
                m_activeImmersiveSession = session.copyRef();
                m_pendingImmersiveSession = false;
            } else
                m_inlineSessions.add(session.copyRef());

            // 5.4.9 Resolve promise with session.
            promise.resolve(WTFMove(session));

            // 5.4.10 is handled in WebXRSession::sessionDidInitializeInputSources.
        });
    });
}

const char* WebXRSystem::activeDOMObjectName() const
{
    return "XRSystem";
}

void WebXRSystem::stop()
{
}

void WebXRSystem::registerSimulatedXRDeviceForTesting(PlatformXR::Device& device)
{
    auto scriptExecutionContext = this->scriptExecutionContext();
    if (!scriptExecutionContext || !scriptExecutionContext->settingsValues().webXREnabled)
        return;

    m_testingDevices++;
    if (device.supports(XRSessionMode::ImmersiveVr) || device.supports(XRSessionMode::ImmersiveAr)) {
        m_immersiveDevices.add(device);
        m_activeImmersiveDevice = device;
    }
    if (device.supports(XRSessionMode::Inline))
        m_inlineXRDevice = device;
}

void WebXRSystem::unregisterSimulatedXRDeviceForTesting(PlatformXR::Device& device)
{
    auto scriptExecutionContext = this->scriptExecutionContext();
    if (!scriptExecutionContext || !scriptExecutionContext->settingsValues().webXREnabled)
        return;

    ASSERT(m_testingDevices);
    bool removed = m_immersiveDevices.remove(device);
    ASSERT_UNUSED(removed, removed || m_inlineXRDevice == &device);
    if (m_activeImmersiveDevice == &device)
        m_activeImmersiveDevice = nullptr;
    if (m_inlineXRDevice == &device)
        m_inlineXRDevice = m_defaultInlineDevice;
    m_testingDevices--;
}

void WebXRSystem::sessionEnded(WebXRSession& session)
{
    if (m_activeImmersiveSession == &session)
        m_activeImmersiveSession = nullptr;

    m_inlineSessions.remove(session);
}

class InlineRequestAnimationFrameCallback final: public RequestAnimationFrameCallback {
public:
    static Ref<InlineRequestAnimationFrameCallback> create(ScriptExecutionContext& scriptExecutionContext, Function<void()>&& callback)
    {
        return adoptRef(*new InlineRequestAnimationFrameCallback(scriptExecutionContext, WTFMove(callback)));
    }
private:
    InlineRequestAnimationFrameCallback(ScriptExecutionContext& scriptExecutionContext, Function<void()>&& callback)
        : RequestAnimationFrameCallback(&scriptExecutionContext), m_callback(WTFMove(callback)) 
    {
    }

    CallbackResult<void> handleEvent(double) final
    {
        m_callback();
        return { };
    }

    Function<void()> m_callback;
};


WebXRSystem::DummyInlineDevice::DummyInlineDevice(ScriptExecutionContext& scriptExecutionContext)
    : ContextDestructionObserver(&scriptExecutionContext)
{
    setSupportedFeatures(XRSessionMode::Inline, { PlatformXR::SessionFeature::ReferenceSpaceTypeViewer });
}

void WebXRSystem::DummyInlineDevice::requestFrame(PlatformXR::Device::RequestFrameCallback&& callback)
{
    if (!scriptExecutionContext())
        return;
    // Inline XR sessions rely on document.requestAnimationFrame to perform the render loop.
    auto document = downcast<Document>(scriptExecutionContext());
    if (!document)
        return;

    auto raf = InlineRequestAnimationFrameCallback::create(*scriptExecutionContext(), [callback = WTFMove(callback)]() mutable {
        PlatformXR::Device::FrameData data;
        data.isTrackingValid = true;
        data.isPositionValid = true;
        data.shouldRender = true;
        data.views.append({ });
        callback(WTFMove(data));
    });

    document->requestAnimationFrame(raf);
}

Vector<PlatformXR::Device::ViewData> WebXRSystem::DummyInlineDevice::views(XRSessionMode) const
{
    return { { .active = true, .eye = PlatformXR::Eye::None } };
}


} // namespace WebCore

#endif // ENABLE(WEBXR)
