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

#if ENABLE(WEBXR)

#include "Document.h"
#include "EventNames.h"
#include "JSWebXRReferenceSpace.h"
#include "WebCoreOpaqueRoot.h"
#include "WebXRBoundedReferenceSpace.h"
#include "WebXRFrame.h"
#include "WebXRSystem.h"
#include "WebXRView.h"
#include "XRFrameRequestCallback.h"
#include "XRRenderStateInit.h"
#include "XRSessionEvent.h"
#include <wtf/IsoMallocInlines.h>
#include <wtf/RefPtr.h>

namespace WebCore {

WTF_MAKE_ISO_ALLOCATED_IMPL(WebXRSession);

Ref<WebXRSession> WebXRSession::create(Document& document, WebXRSystem& system, XRSessionMode mode, PlatformXR::Device& device, FeatureList&& requestedFeatures)
{
    auto session = adoptRef(*new WebXRSession(document, system, mode, device, WTFMove(requestedFeatures)));
    session->suspendIfNeeded();
    return session;
}

WebXRSession::WebXRSession(Document& document, WebXRSystem& system, XRSessionMode mode, PlatformXR::Device& device, FeatureList&& requestedFeatures)
    : ActiveDOMObject(&document)
    , m_inputSources(WebXRInputSourceArray::create(*this))
    , m_xrSystem(system)
    , m_mode(mode)
    , m_device(device)
    , m_requestedFeatures(WTFMove(requestedFeatures))
    , m_activeRenderState(WebXRRenderState::create(mode))
    , m_viewerReferenceSpace(makeUnique<WebXRViewerSpace>(document, *this))
    , m_timeOrigin(MonotonicTime::now())
    , m_views(device.views(mode))
{
    m_device->setTrackingAndRenderingClient(*this);
    m_device->initializeTrackingAndRendering(document.securityOrigin().data(), mode, m_requestedFeatures);

    // https://immersive-web.github.io/webxr/#ref-for-dom-xrreferencespacetype-viewer%E2%91%A2
    // Every session MUST support viewer XRReferenceSpaces.
    m_device->initializeReferenceSpace(XRReferenceSpaceType::Viewer);
}

WebXRSession::~WebXRSession()
{
    if (!m_ended && m_device)
        m_device->shutDownTrackingAndRendering();
}

XREnvironmentBlendMode WebXRSession::environmentBlendMode() const
{
    return m_environmentBlendMode;
}

XRInteractionMode WebXRSession::interactionMode() const
{
    return m_interactionMode;
}

XRVisibilityState WebXRSession::visibilityState() const
{
    return m_visibilityState;
}

const WebXRRenderState& WebXRSession::renderState() const
{
    return *m_activeRenderState;
}

const WebXRInputSourceArray& WebXRSession::inputSources() const
{
    return m_inputSources;
}

static bool isImmersive(XRSessionMode mode)
{
    return mode == XRSessionMode::ImmersiveAr || mode == XRSessionMode::ImmersiveVr;
}

// https://immersive-web.github.io/webxr/#dom-xrsession-updaterenderstate
ExceptionOr<void> WebXRSession::updateRenderState(const XRRenderStateInit& newState)
{
    // 1. Let session be this.
    // 2. If session's ended value is true, throw an InvalidStateError and abort these steps.
    if (m_ended)
        return Exception { InvalidStateError };

    // 3. If newState's baseLayer was created with an XRSession other than session,
    //    throw an InvalidStateError and abort these steps.
    if (newState.baseLayer && &newState.baseLayer->session() != this)
        return Exception { InvalidStateError };

    // 4. If newState's inlineVerticalFieldOfView is set and session is an immersive session,
    //    throw an InvalidStateError and abort these steps.
    if (newState.inlineVerticalFieldOfView && isImmersive(m_mode))
        return Exception { InvalidStateError };

    // 5. If none of newState's depthNear, depthFar, inlineVerticalFieldOfView, baseLayer,
    //    layers are set, abort these steps.
    if (!newState.depthNear && !newState.depthFar && !newState.inlineVerticalFieldOfView && !newState.baseLayer && !newState.layers)
        return { };

    // 6. Run update the pending layers state with session and newState.
    // https://immersive-web.github.io/webxr/#update-the-pending-layers-state
    if (newState.layers)
        return Exception { NotSupportedError };

    // 7. Let activeState be session's active render state.
    // 8. If session's pending render state is null, set it to a copy of activeState.
    if (!m_pendingRenderState)
        m_pendingRenderState = m_activeRenderState->clone();

    // 9. If newState's depthNear value is set, set session's pending render state's depthNear to newState's depthNear.
    if (newState.depthNear)
        m_pendingRenderState->setDepthNear(newState.depthNear.value());

    // 10. If newState's depthFar value is set, set session's pending render state's depthFar to newState's depthFar.
    if (newState.depthFar)
        m_pendingRenderState->setDepthFar(newState.depthFar.value());

    // 11. If newState's inlineVerticalFieldOfView is set, set session's pending render state's inlineVerticalFieldOfView
    //     to newState's inlineVerticalFieldOfView.
    if (newState.inlineVerticalFieldOfView)
        m_pendingRenderState->setInlineVerticalFieldOfView(newState.inlineVerticalFieldOfView.value());

    // 12. If newState's baseLayer is set, set session's pending render state's baseLayer to newState's baseLayer.
    if (newState.baseLayer)
        m_pendingRenderState->setBaseLayer(newState.baseLayer.get());

    return { };
}

// https://immersive-web.github.io/webxr/#reference-space-is-supported
bool WebXRSession::referenceSpaceIsSupported(XRReferenceSpaceType type) const
{
    // 1. If type is not contained in session’s XR device's list of enabled features for mode return false.
    if (!m_requestedFeatures.contains(sessionFeatureFromReferenceSpaceType(type)))
        return false;

    // 2. If type is viewer, return true.
    if (type == XRReferenceSpaceType::Viewer)
        return true;

    bool isImmersiveSession = isImmersive(m_mode);
    if (type == XRReferenceSpaceType::Local || type == XRReferenceSpaceType::LocalFloor) {
        // 3. If type is local or local-floor, and session is an immersive session, return true.
        if (isImmersiveSession)
            return true;

        // 4. If type is local or local-floor, and the XR device supports reporting orientation data, return true.
        if (m_device->supportsOrientationTracking())
            return true;
    }

    // 5. If type is bounded-floor and session is an immersive session, return the result of whether bounded
    //    reference spaces are supported by the XR device.
    // https://immersive-web.github.io/webxr/#bounded-reference-spaces-are-supported
    // TODO: add API to PlatformXR::Device
    if (type == XRReferenceSpaceType::BoundedFloor && isImmersiveSession)
        return true;

    // 6. If type is unbounded, session is an immersive session, and the XR device supports stable tracking
    //    near the user over an unlimited distance, return true.
    // TODO: add API to PlatformXR::Device to check stable tracking over unlimited distance.
    if (type == XRReferenceSpaceType::Unbounded && isImmersiveSession)
        return true;

    // 7. Return false.
    return false;
}

// https://immersive-web.github.io/webxr/#dom-xrsession-requestreferencespace
void WebXRSession::requestReferenceSpace(XRReferenceSpaceType type, RequestReferenceSpacePromise&& promise)
{
    if (!scriptExecutionContext()) {
        promise.reject(Exception { InvalidStateError });
        return;
    }

    // 1. Let promise be a new Promise.
    // 2. Run the following steps in parallel:
    scriptExecutionContext()->postTask([this, weakThis = WeakPtr { *this }, promise = WTFMove(promise), type](auto&) mutable {
        if (!weakThis)
            return;
        // 2.1. If the result of running reference space is supported for type and session is false, queue a task to reject promise
        // with a NotSupportedError and abort these steps.
        if (!referenceSpaceIsSupported(type)) {
            queueTaskKeepingObjectAlive(*this, TaskSource::WebXR, [promise = WTFMove(promise)]() mutable {
                promise.reject(Exception { NotSupportedError });
            });
            return;
        }
        // 2.2. Set up any platform resources required to track reference spaces of type type.
        m_device->initializeReferenceSpace(type);

        // 2.3. Queue a task to run the following steps:
        queueTaskKeepingObjectAlive(*this, TaskSource::WebXR, [this, type, promise = WTFMove(promise)]() mutable {
            if (!scriptExecutionContext()) {
                promise.reject(Exception { InvalidStateError });
                return;
            }
            auto& document = downcast<Document>(*scriptExecutionContext());
            // 2.4. Create a reference space, referenceSpace, with type and session.
            // https://immersive-web.github.io/webxr/#create-a-reference-space
            RefPtr<WebXRReferenceSpace> referenceSpace;
            if (type == XRReferenceSpaceType::BoundedFloor)
                referenceSpace = WebXRBoundedReferenceSpace::create(document, Ref { *this }, type);
            else
                referenceSpace = WebXRReferenceSpace::create(document, Ref { *this }, type);

            // 2.5. Resolve promise with referenceSpace.
            promise.resolve(referenceSpace.releaseNonNull());
        });
    });
}

// https://immersive-web.github.io/webxr/#dom-xrsession-requestanimationframe
unsigned WebXRSession::requestAnimationFrame(Ref<XRFrameRequestCallback>&& callback)
{
    // Ignore any new frame requests once the session is ended.
    if (m_ended)
        return 0;

    // 1. Let session be the target XRSession object.
    // 2. Increment session's animation frame callback identifier by one.
    unsigned newId = m_nextCallbackId++;

    // 3. Append callback to session's list of animation frame callbacks, associated with session's
    // animation frame callback identifier's current value.
    callback->setCallbackId(newId);
    m_callbacks.append(WTFMove(callback));

    // Script can add multiple requestAnimationFrame callbacks but we should only request a device frame once.
    // When requestAnimationFrame is called during processing RAF callbacks the next requestFrame is scheduled
    // at the end of WebXRSession::onFrame() to prevent requesting a new frame before the current one has ended.
    requestFrameIfNeeded();

    // 4. Return session's animation frame callback identifier's current value.
    return newId;
}

// https://immersive-web.github.io/webxr/#dom-xrsession-cancelanimationframe
void WebXRSession::cancelAnimationFrame(unsigned callbackId)
{
    // 1. Let session be the target XRSession object.
    // 2. Find the entry in session's list of animation frame callbacks or session's list of
    //    currently running animation frame callbacks that is associated with the value handle.
    // 3. If there is such an entry, set its cancelled boolean to true and remove it from
    //    session's list of animation frame callbacks.
    size_t position = m_callbacks.findIf([callbackId] (auto& item) {
        return item->callbackId() == callbackId;
    });

    if (position != notFound) {
        m_callbacks[position]->setFiredOrCancelled();
        return;
    }
}

// https://immersive-web.github.io/webxr/#native-webgl-framebuffer-resolution
IntSize WebXRSession::nativeWebGLFramebufferResolution() const
{
    if (m_mode == XRSessionMode::Inline) {
        // FIXME: replace the conditional by ASSERTs once we properly initialize the outputCanvas.
        return m_activeRenderState && m_activeRenderState->outputCanvas() ? m_activeRenderState->outputCanvas()->size() : IntSize(1, 1);
    }

    return recommendedWebGLFramebufferResolution();
}

// https://immersive-web.github.io/webxr/#recommended-webgl-framebuffer-resolution
IntSize WebXRSession::recommendedWebGLFramebufferResolution() const
{
    ASSERT(m_device);
    return m_device->recommendedResolution(m_mode);
}

// https://immersive-web.github.io/webxr/#view-viewport-modifiable
bool WebXRSession::supportsViewportScaling() const
{
    ASSERT(m_device);
    // Only immersive sessions support viewport scaling.
    return m_mode == XRSessionMode::ImmersiveVr && m_device->supportsViewportScaling();
}

bool WebXRSession::isPositionEmulated() const
{
    return m_frameData.isPositionEmulated || !m_frameData.isPositionValid;
}

// https://immersive-web.github.io/webxr/#shut-down-the-session
void WebXRSession::shutdown(InitiatedBySystem initiatedBySystem)
{
    Ref protectedThis { *this };

    if (m_ended) {
        // This method was called earlier with initiatedBySystem=No when the
        // session termination was requested manually via XRSession.end(). When
        // the system has completed the shutdown, this method is now called again
        // with initiatedBySystem=Yes to do the final cleanup.
        if (initiatedBySystem == InitiatedBySystem::Yes)
            didCompleteShutdown();
        return;
    }

    // 1. Let session be the target XRSession object.
    // 2. Set session's ended value to true.
    m_ended = true;

    // 3. If the active immersive session is equal to session, set the active immersive session to null.
    // 4. Remove session from the list of inline sessions.
    m_xrSystem.sessionEnded(*this);

    m_inputSources->clear();

    if (initiatedBySystem == InitiatedBySystem::Yes) {
        // If we get here, the session termination was triggered by the system rather than
        // via XRSession.end(). Since the system has completed the session shutdown, we can
        // immediately do the final cleanup.
        didCompleteShutdown();
        return;
    }

    // TODO: complete the implementation
    // 5. Reject any outstanding promises returned by session with an InvalidStateError, except for any promises returned by end().
    // 6. If no other features of the user agent are actively using them, perform the necessary platform-specific steps to shut down the device's tracking and rendering capabilities. This MUST include:
    //  6.1. Releasing exclusive access to the XR device if session is an immersive session.
    //  6.2. Deallocating any graphics resources acquired by session for presentation to the XR device.
    //  6.3. Putting the XR device in a state such that a different source may be able to initiate a session with the same device if session is an immersive session.
    if (m_device)
        m_device->shutDownTrackingAndRendering();

    // If device will not report shutdown completion via the TrackingAndRenderingClient,
    // complete the shutdown cleanup here.
    if (!m_device || !m_device->supportsSessionShutdownNotification())
        didCompleteShutdown();
}

void WebXRSession::didCompleteShutdown()
{
    if (m_device)
        m_device->setTrackingAndRenderingClient(nullptr);

    // Resolve end promise from XRSession::end()
    if (m_endPromise) {
        m_endPromise->resolve();
        m_endPromise = std::nullopt;
    }

    // From https://immersive-web.github.io/webxr/#shut-down-the-session
    // 7. Queue a task that fires an XRSessionEvent named end on session.
    auto event = XRSessionEvent::create(eventNames().endEvent, { RefPtr { this } });
    queueTaskToDispatchEvent(*this, TaskSource::WebXR, WTFMove(event));
}

// https://immersive-web.github.io/webxr/#dom-xrsession-end
ExceptionOr<void> WebXRSession::end(EndPromise&& promise)
{
    // The shutdown() call below might remove the sole reference to session
    // that could exist (the XRSystem owns the sessions) so let's protect this.
    Ref protectedThis { *this };

    if (m_ended)
        return Exception { InvalidStateError, "Cannot end a session more than once"_s };

    ASSERT(!m_endPromise);
    m_endPromise = WTFMove(promise);

    // 1. Let promise be a new Promise.
    // 2. Shut down the target XRSession object.
    shutdown(InitiatedBySystem::No);

    // 3. Queue a task to perform the following steps:
    // 3.1 Wait until any platform-specific steps related to shutting down the session have completed.
    // 3.2 Resolve promise.
    // 4. Return promise.
    return { };
}

const char* WebXRSession::activeDOMObjectName() const
{
    return "XRSession";
}

void WebXRSession::stop()
{
}

void WebXRSession::sessionDidInitializeInputSources(Vector<PlatformXR::Device::FrameData::InputSource>&& inputSources)
{
    // https://immersive-web.github.io/webxr/#dom-xrsystem-requestsession
    // 5.4.11 Queue a task to perform the following steps: NOTE: These steps ensure that initial inputsourceschange
    // events occur after the initial session is resolved.
    queueTaskKeepingObjectAlive(*this, TaskSource::WebXR, [this, inputSources = WTFMove(inputSources)]() mutable {
        //  1. Set session's promise resolved flag to true.
        m_inputInitialized = true;
        //  2. Let sources be any existing input sources attached to session.
        //  3. If sources is non-empty, perform the following steps:
        if (!inputSources.isEmpty()) {
            auto timestamp = (MonotonicTime::now() - m_timeOrigin).milliseconds();
            //  3.1. Set session's list of active XR input sources to sources.
            //  3.2. Fire an XRInputSourcesChangeEvent named inputsourceschange on session with added set to sources.
            //  Note: 3.1 and 3.2 steps are handled inside the update() call.
            m_inputSources->update(timestamp, inputSources);
        }
    });
}

void WebXRSession::sessionDidEnd()
{
    // This can be called as a result of finishing the shutdown initiated
    // from XRSession::end(), or session termination triggered by the system.
    shutdown(InitiatedBySystem::Yes);
}

void WebXRSession::updateSessionVisibilityState(PlatformXR::VisibilityState visibilityState)
{
    if (m_visibilityState == visibilityState)
        return;

    m_visibilityState = visibilityState;

    requestFrameIfNeeded();

    // From https://immersive-web.github.io/webxr/#event-types
    // A user agent MUST dispatch a visibilitychange event on an XRSession each time the
    // visibility state of the XRSession has changed. The event MUST be of type XRSessionEvent.
    auto event = XRSessionEvent::create(eventNames().visibilitychangeEvent, { RefPtr { this } });
    queueTaskToDispatchEvent(*this, TaskSource::WebXR, WTFMove(event));
}

void WebXRSession::applyPendingRenderState()
{
    // https: //immersive-web.github.io/webxr/#apply-the-pending-render-state
    // 1. Let activeState be session’s active render state.
    // 2. Let newState be session’s pending render state.
    // 3. Set session’s pending render state to null.
    auto newState = m_pendingRenderState;
    ASSERT(newState);

    // 4. Let oldBaseLayer be activeState’s baseLayer.
    // 5. Let oldLayers be activeState’s layers.
    // FIXME: those are only needed for step 6.2.

    // 6.1 Set activeState to newState.
    m_activeRenderState = newState;

    // 6.2 If oldBaseLayer is not equal to activeState’s baseLayer, oldLayers is not equal to activeState’s layers, or the dimensions of any of the layers have changed, update the viewports for session.
    // FIXME: implement this.

    // 6.3 If activeState’s inlineVerticalFieldOfView is less than session’s minimum inline field of view set activeState’s inlineVerticalFieldOfView to session’s minimum inline field of view.
    if (m_activeRenderState->inlineVerticalFieldOfView() < m_minimumInlineFOV)
        m_activeRenderState->setInlineVerticalFieldOfView(m_minimumInlineFOV);

    // 6.4 If activeState’s inlineVerticalFieldOfView is greater than session’s maximum inline field of view set activeState’s inlineVerticalFieldOfView to session’s maximum inline field of view.
    if (m_activeRenderState->inlineVerticalFieldOfView() > m_maximumInlineFOV)
        m_activeRenderState->setInlineVerticalFieldOfView(m_maximumInlineFOV);

    // 6.5 If activeState’s depthNear is less than session’s minimum near clip plane set activeState’s depthNear to session’s minimum near clip plane.
    if (m_activeRenderState->depthNear() < m_minimumNearClipPlane)
        m_activeRenderState->setDepthNear(m_minimumNearClipPlane);

    // 6.6 If activeState’s depthFar is greater than session’s maximum far clip plane set activeState’s depthFar to session’s maximum far clip plane.
    if (m_activeRenderState->depthFar() > m_maximumFarClipPlane)
        m_activeRenderState->setDepthFar(m_maximumFarClipPlane);

    // 6.7 Let baseLayer be activeState’s baseLayer.
    auto baseLayer = m_activeRenderState->baseLayer();

    // 6.8 Set activeState’s composition enabled and output canvas as follows:
    if (m_mode == XRSessionMode::Inline && is<WebXRWebGLLayer>(baseLayer) && !baseLayer->isCompositionEnabled()) {
        m_activeRenderState->setCompositionEnabled(false);
        m_activeRenderState->setOutputCanvas(baseLayer->canvas());
    } else {
        m_activeRenderState->setCompositionEnabled(true);
        m_activeRenderState->setOutputCanvas(nullptr);
    }
}

// https://immersive-web.github.io/webxr/#should-be-rendered
bool WebXRSession::frameShouldBeRendered() const
{
    if (!m_activeRenderState->baseLayer())
        return false;
    if (m_mode == XRSessionMode::Inline && !m_activeRenderState->outputCanvas())
        return false;
    return true;
}

void WebXRSession::requestFrameIfNeeded()
{
    ASSERT(isMainThread());

    if (m_ended)
        return;

    if (m_visibilityState == XRVisibilityState::Hidden)
        return;

    if (m_callbacks.isEmpty() || m_isDeviceFrameRequestPending)
        return;

    m_isDeviceFrameRequestPending = true;
    m_device->requestFrame([this, protectedThis = Ref { *this }](auto&& frameData) {
        m_isDeviceFrameRequestPending = false;
        onFrame(WTFMove(frameData));
    });
}

void WebXRSession::onFrame(PlatformXR::Device::FrameData&& frameData)
{
    ASSERT(isMainThread());

    if (m_ended)
        return;

    // From https://immersive-web.github.io/webxr/#xrsession-visibility-state
    // A state of hidden indicates that imagery rendered by the XRSession cannot be seen by the user.
    // requestAnimationFrame() callbacks will not be processed until the visibility state changes.
    // Input is not processed by the XRSession.
    if (m_visibilityState == XRVisibilityState::Hidden)
        return;

    // Queue a task to perform the following steps.
    queueTaskKeepingObjectAlive(*this, TaskSource::WebXR, [this, frameData = WTFMove(frameData)]() mutable {
        if (m_ended || m_visibilityState == XRVisibilityState::Hidden)
            return;

        m_frameData = WTFMove(frameData);
        //  1.Let now be the current high resolution time.
        auto now = (MonotonicTime::now() - m_timeOrigin).milliseconds();

        auto frame = WebXRFrame::create(*this, WebXRFrame::IsAnimationFrame::Yes);
        //  2.Let frame be session’s animation frame.
        //  3.Set frame’s time to frameTime.
        frame->setTime(static_cast<DOMHighResTimeStamp>(m_frameData.predictedDisplayTime));

        // 4. For each view in list of views, set view’s viewport modifiable flag to true.
        // 5. If the active flag of any view in the list of views has changed since the last XR animation frame, update the viewports.
        // FIXME: implement.

        // FIXME: I moved step 7 before 6 because of https://github.com/immersive-web/webxr/issues/1164
        // 7.If session’s pending render state is not null, apply the pending render state.
        if (m_pendingRenderState)
            applyPendingRenderState();

        // 6. If the frame should be rendered for session:
        if (frameShouldBeRendered() && m_frameData.shouldRender) {
            // Prepare all layers for render
            if (m_mode == XRSessionMode::ImmersiveVr && m_activeRenderState->baseLayer())
                m_activeRenderState->baseLayer()->startFrame(m_frameData);

            // 6.1.Set session’s list of currently running animation frame callbacks to be session’s list of animation frame callbacks.
            // 6.2.Set session’s list of animation frame callbacks to the empty list.
            auto callbacks = m_callbacks;

            // 6.3.Set frame’s active boolean to true.
            frame->setActive(true);

            // 6.4.Apply frame updates for frame.
            if (m_inputInitialized)
                m_inputSources->update(now, m_frameData.inputSources);

            // 6.5.For each entry in session’s list of currently running animation frame callbacks, in order:
            for (auto& callback : callbacks) {
                //  6.6.If the entry’s cancelled boolean is true, continue to the next entry.
                if (callback->isFiredOrCancelled())
                    continue;
                callback->setFiredOrCancelled();
                //  6.7.Invoke the Web IDL callback function for entry, passing now and frame as the arguments
                callback->handleEvent(now, frame.get());

                //  6.8.If an exception is thrown, report the exception.
            }
            // 6.9.Set session’s list of currently running animation frame callbacks to the empty list.
            m_callbacks.removeAllMatching([](auto& callback) {
                return callback->isFiredOrCancelled();
            });

            // 6.10.Set frame’s active boolean to false.
            // If the session is ended, m_animationFrame->setActive false is set in shutdown().
            frame->setActive(false);


            // Submit current frame layers to the device.
            Vector<PlatformXR::Device::Layer> frameLayers;
            if (m_mode == XRSessionMode::ImmersiveVr && m_activeRenderState->baseLayer())
                frameLayers.append(m_activeRenderState->baseLayer()->endFrame());

            m_device->submitFrame(WTFMove(frameLayers));
        }

        requestFrameIfNeeded();
    });
}

// https://immersive-web.github.io/webxr/#poses-may-be-reported
bool WebXRSession::posesCanBeReported(const Document& document) const
{
    // 1. If session’s relevant global object is not the current global object, return false.
    auto* sessionDocument = downcast<Document>(scriptExecutionContext());
    if (!sessionDocument || sessionDocument->domWindow() != document.domWindow())
        return false;

    // 2. If session's visibilityState in not "visible", return false.
    if (m_visibilityState != XRVisibilityState::Visible)
        return false;

    // 5. Determine if the pose data can be returned as follows:
    // The procedure in the specs tries to ensure that we apply measures to
    // prevent fingerprintint in pose data and return false in case we don't.
    // We're going to apply them so let's just return true.
    return true;
}

#if ENABLE(WEBXR_HANDS)
bool WebXRSession::isHandTrackingEnabled() const
{
    return m_requestedFeatures.contains(PlatformXR::SessionFeature::HandTracking);
}
#endif

WebCoreOpaqueRoot root(WebXRSession* session)
{
    return WebCoreOpaqueRoot { session };
}

} // namespace WebCore

#endif // ENABLE(WEBXR)
