/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "mozilla/dom/XRSystem.h"

#include "mozilla/StaticPrefs_dom.h"
#include "mozilla/dom/XRPermissionRequest.h"
#include "mozilla/dom/XRSession.h"
#include "mozilla/dom/BindingCallContext.h"
#include "mozilla/dom/Document.h"
#include "mozilla/dom/Promise.h"
#include "mozilla/dom/FeaturePolicyUtils.h"
#include "mozilla/dom/PermissionMessageUtils.h"
#include "nsGlobalWindow.h"
#include "nsThreadUtils.h"
#include "gfxVR.h"
#include "VRDisplayClient.h"
#include "VRManagerChild.h"

namespace mozilla::dom {

using namespace gfx;

////////////////////////////////////////////////////////////////////////////////
// XRSystem cycle collection
NS_IMPL_CYCLE_COLLECTION_CLASS(XRSystem)

NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(XRSystem,
                                                  DOMEventTargetHelper)
  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mActiveImmersiveSession)
  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mInlineSessions)
  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mIsSessionSupportedRequests)
  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(
      mRequestSessionRequestsWaitingForRuntimeDetection)
  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRequestSessionRequestsWithoutHardware)
  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(
      mRequestSessionRequestsWaitingForEnumeration)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END

NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(XRSystem, DOMEventTargetHelper)
  NS_IMPL_CYCLE_COLLECTION_UNLINK(mActiveImmersiveSession)
  NS_IMPL_CYCLE_COLLECTION_UNLINK(mInlineSessions)
  NS_IMPL_CYCLE_COLLECTION_UNLINK(mIsSessionSupportedRequests)
  NS_IMPL_CYCLE_COLLECTION_UNLINK(
      mRequestSessionRequestsWaitingForRuntimeDetection)
  NS_IMPL_CYCLE_COLLECTION_UNLINK(mRequestSessionRequestsWithoutHardware)
  NS_IMPL_CYCLE_COLLECTION_UNLINK(mRequestSessionRequestsWaitingForEnumeration)
  // Don't need NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER because
  // DOMEventTargetHelper does it for us.
NS_IMPL_CYCLE_COLLECTION_UNLINK_END

NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(XRSystem, DOMEventTargetHelper)

JSObject* XRSystem::WrapObject(JSContext* aCx,
                               JS::Handle<JSObject*> aGivenProto) {
  return XRSystem_Binding::Wrap(aCx, this, aGivenProto);
}

// static
already_AddRefed<XRSystem> XRSystem::Create(nsPIDOMWindowInner* aWindow) {
  MOZ_ASSERT(aWindow);

  RefPtr<XRSystem> service = new XRSystem(aWindow);
  return service.forget();
}

XRSystem::XRSystem(nsPIDOMWindowInner* aWindow)
    : DOMEventTargetHelper(aWindow),
      mShuttingDown(false),
      mPendingImmersiveSession(false),
      mEnumerationInFlight(false) {
  // Unregister with VRManagerChild
  VRManagerChild* vmc = VRManagerChild::Get();
  if (vmc) {
    vmc->AddListener(this);
  }
}

void XRSystem::Shutdown() {
  MOZ_ASSERT(!mShuttingDown);
  mShuttingDown = true;

  // Unregister from VRManagerChild
  if (VRManagerChild::IsCreated()) {
    VRManagerChild* vmc = VRManagerChild::Get();
    vmc->RemoveListener(this);
  }
}

void XRSystem::SessionEnded(XRSession* aSession) {
  if (mActiveImmersiveSession == aSession) {
    mActiveImmersiveSession = nullptr;
  }
  mInlineSessions.RemoveElement(aSession);
}

already_AddRefed<Promise> XRSystem::IsSessionSupported(XRSessionMode aMode,
                                                       ErrorResult& aRv) {
  nsCOMPtr<nsIGlobalObject> global = GetParentObject();
  NS_ENSURE_TRUE(global, nullptr);

  RefPtr<Promise> promise = Promise::Create(global, aRv);
  NS_ENSURE_TRUE(!aRv.Failed(), nullptr);

  if (aMode == XRSessionMode::Inline) {
    promise->MaybeResolve(true);
    return promise.forget();
  }

  if (mIsSessionSupportedRequests.IsEmpty()) {
    gfx::VRManagerChild* vm = gfx::VRManagerChild::Get();
    vm->DetectRuntimes();
  }

  RefPtr<IsSessionSupportedRequest> request =
      new IsSessionSupportedRequest(aMode, promise);
  mIsSessionSupportedRequests.AppendElement(request);
  return promise.forget();
}

already_AddRefed<Promise> XRSystem::RequestSession(
    JSContext* aCx, XRSessionMode aMode, const XRSessionInit& aOptions,
    CallerType aCallerType, ErrorResult& aRv) {
  nsCOMPtr<nsIGlobalObject> global = GetParentObject();
  NS_ENSURE_TRUE(global, nullptr);

  RefPtr<Promise> promise = Promise::Create(global, aRv);
  NS_ENSURE_TRUE(!aRv.Failed(), nullptr);

  bool immersive = (aMode == XRSessionMode::Immersive_vr ||
                    aMode == XRSessionMode::Immersive_ar);

  // The document must be a responsible document, active and focused.
  nsCOMPtr<Document> responsibleDocument = GetDocumentIfCurrent();
  if (!responsibleDocument) {
    // The document is not trustworthy
    promise->MaybeRejectWithSecurityError("This document is not responsible.");
    return promise.forget();
  }

  if (immersive || aOptions.mRequiredFeatures.WasPassed() ||
      aOptions.mOptionalFeatures.WasPassed()) {
    if (!responsibleDocument->HasValidTransientUserGestureActivation() &&
        aCallerType != CallerType::System &&
        StaticPrefs::dom_vr_require_gesture()) {
      // A user gesture is required.
      promise->MaybeRejectWithSecurityError("A user gesture is required.");
      return promise.forget();
    }
  }

  nsTArray<XRReferenceSpaceType> requiredReferenceSpaceTypes;
  nsTArray<XRReferenceSpaceType> optionalReferenceSpaceTypes;

  /**
   * By default, all sessions will require XRReferenceSpaceType::Viewer
   * and immersive sessions will require XRReferenceSpaceType::Local.
   *
   * https://www.w3.org/TR/webxr/#default-features
   */
  requiredReferenceSpaceTypes.AppendElement(XRReferenceSpaceType::Viewer);
  if (immersive) {
    requiredReferenceSpaceTypes.AppendElement(XRReferenceSpaceType::Local);
  }

  BindingCallContext callCx(aCx, "XRSystem.requestSession");

  if (aOptions.mRequiredFeatures.WasPassed()) {
    const Sequence<JS::Value>& arr = (aOptions.mRequiredFeatures.Value());
    for (const JS::Value& val : arr) {
      if (!val.isNull() && !val.isUndefined()) {
        bool bFound = false;
        JS::Rooted<JS::Value> v(aCx, val);
        int index = 0;
        if (FindEnumStringIndex<false>(
                callCx, v, XRReferenceSpaceTypeValues::strings,
                "XRReferenceSpaceType", "Argument 2 of XR.requestSession",
                &index)) {
          if (index >= 0) {
            requiredReferenceSpaceTypes.AppendElement(
                static_cast<XRReferenceSpaceType>(index));
            bFound = true;
          }
        }
        if (!bFound) {
          promise->MaybeRejectWithNotSupportedError(
              "A required feature for the XRSession is not available.");
          return promise.forget();
        }
      }
    }
  }

  if (aOptions.mOptionalFeatures.WasPassed()) {
    const Sequence<JS::Value>& arr = (aOptions.mOptionalFeatures.Value());
    for (const JS::Value& val : arr) {
      if (!val.isNull() && !val.isUndefined()) {
        JS::Rooted<JS::Value> v(aCx, val);
        int index = 0;
        if (FindEnumStringIndex<false>(
                callCx, v, XRReferenceSpaceTypeValues::strings,
                "XRReferenceSpaceType", "Argument 2 of XR.requestSession",
                &index)) {
          if (index >= 0) {
            optionalReferenceSpaceTypes.AppendElement(
                static_cast<XRReferenceSpaceType>(index));
          }
        }
      }
    }
  }

  if (immersive) {
    if (mPendingImmersiveSession || mActiveImmersiveSession) {
      promise->MaybeRejectWithInvalidStateError(
          "There can only be one immersive XRSession.");
      return promise.forget();
    }
    mPendingImmersiveSession = true;
  }

  bool isChromeSession = aCallerType == CallerType::System;
  uint32_t presentationGroup =
      isChromeSession ? gfx::kVRGroupChrome : gfx::kVRGroupContent;
  RefPtr<RequestSessionRequest> request = new RequestSessionRequest(
      aMode, presentationGroup, promise, requiredReferenceSpaceTypes,
      optionalReferenceSpaceTypes);
  if (request->WantsHardware()) {
    QueueSessionRequestWithEnumeration(request);
  } else {
    QueueSessionRequestWithoutEnumeration(request);
  }

  return promise.forget();
}

void XRSystem::QueueSessionRequestWithEnumeration(
    RequestSessionRequest* aRequest) {
  MOZ_ASSERT(aRequest->WantsHardware());
  mRequestSessionRequestsWaitingForRuntimeDetection.AppendElement(aRequest);
  gfx::VRManagerChild* vm = gfx::VRManagerChild::Get();
  vm->DetectRuntimes();
}

void XRSystem::QueueSessionRequestWithoutEnumeration(
    RequestSessionRequest* aRequest) {
  MOZ_ASSERT(!aRequest->NeedsHardware());
  mRequestSessionRequestsWithoutHardware.AppendElement(aRequest);

  ResolveSessionRequestsWithoutHardware();
}

bool XRSystem::CancelHardwareRequest(RequestSessionRequest* aRequest) {
  if (!aRequest->NeedsHardware()) {
    // If hardware access was an optional requirement and the user
    // opted not to provide access, queue the request
    // to be resolved without hardware.
    QueueSessionRequestWithoutEnumeration(aRequest);
    return false;
  }

  if (aRequest->IsImmersive()) {
    mPendingImmersiveSession = false;
  }
  return true;
}

bool XRSystem::OnXRPermissionRequestAllow() {
  if (!gfx::VRManagerChild::IsCreated()) {
    // It's possible that this callback returns after
    // we have already started shutting down.
    return false;
  }
  if (!mEnumerationInFlight) {
    mEnumerationInFlight = true;
    gfx::VRManagerChild* vm = gfx::VRManagerChild::Get();
    Unused << vm->EnumerateVRDisplays();
  }
  return mEnumerationInFlight ||
         !mRequestSessionRequestsWaitingForEnumeration.IsEmpty();
}

void XRSystem::OnXRPermissionRequestCancel() {
  nsTArray<RefPtr<RequestSessionRequest>> requestSessionRequests(
      std::move(mRequestSessionRequestsWaitingForEnumeration));
  for (RefPtr<RequestSessionRequest>& request : requestSessionRequests) {
    if (CancelHardwareRequest(request)) {
      request->mPromise->MaybeRejectWithSecurityError(
          "A device supporting the requested session "
          "configuration could not be found.");
    }
  }
}

bool XRSystem::FeaturePolicyBlocked() const {
  nsGlobalWindowInner* win = nsGlobalWindowInner::Cast(GetOwner());
  if (!win) {
    return true;
  }
  RefPtr<XRPermissionRequest> request =
      new XRPermissionRequest(win, win->WindowID());
  return !(request->CheckPermissionDelegate());
}

bool XRSystem::HasActiveImmersiveSession() const {
  return mActiveImmersiveSession;
}

void XRSystem::ResolveSessionRequestsWithoutHardware() {
  // Resolve promises returned by RequestSession
  nsTArray<RefPtr<gfx::VRDisplayClient>> displays;
  // Try resolving support without a device, for inline sessions.
  displays.AppendElement(nullptr);

  nsTArray<RefPtr<RequestSessionRequest>> requestSessionRequests(
      std::move(mRequestSessionRequestsWithoutHardware));

  ResolveSessionRequests(requestSessionRequests, displays);
}

void XRSystem::NotifyEnumerationCompleted() {
  // Enumeration has completed.
  mEnumerationInFlight = false;

  if (!gfx::VRManagerChild::IsCreated()) {
    // It's possible that this callback returns after
    // we have already started shutting down.
    return;
  }

  // Resolve promises returned by RequestSession
  nsTArray<RefPtr<gfx::VRDisplayClient>> displays;
  gfx::VRManagerChild* vm = gfx::VRManagerChild::Get();
  vm->GetVRDisplays(displays);

  nsTArray<RefPtr<RequestSessionRequest>> requestSessionRequests(
      std::move(mRequestSessionRequestsWaitingForEnumeration));

  ResolveSessionRequests(requestSessionRequests, displays);
}

void XRSystem::ResolveSessionRequests(
    nsTArray<RefPtr<RequestSessionRequest>>& aRequests,
    const nsTArray<RefPtr<gfx::VRDisplayClient>>& aDisplays) {
  for (RefPtr<RequestSessionRequest>& request : aRequests) {
    RefPtr<XRSession> session;
    if (request->IsImmersive()) {
      mPendingImmersiveSession = false;
    }
    // Select an XR device
    for (const RefPtr<gfx::VRDisplayClient>& display : aDisplays) {
      nsTArray<XRReferenceSpaceType> enabledReferenceSpaceTypes;
      if (request->ResolveSupport(display, enabledReferenceSpaceTypes)) {
        if (request->IsImmersive()) {
          session = XRSession::CreateImmersiveSession(
              GetOwner(), this, display, request->GetPresentationGroup(),
              enabledReferenceSpaceTypes);
          mActiveImmersiveSession = session;
        } else {
          session = XRSession::CreateInlineSession(GetOwner(), this,
                                                   enabledReferenceSpaceTypes);
          mInlineSessions.AppendElement(session);
        }
        request->mPromise->MaybeResolve(session);
        break;
      }
    }
    if (!session) {
      request->mPromise->MaybeRejectWithNotSupportedError(
          "A device supporting the required XRSession configuration "
          "could not be found.");
    }
  }
}

void XRSystem::NotifyDetectRuntimesCompleted() {
  ResolveIsSessionSupportedRequests();
  if (!mRequestSessionRequestsWaitingForRuntimeDetection.IsEmpty()) {
    ProcessSessionRequestsWaitingForRuntimeDetection();
  }
}

void XRSystem::ResolveIsSessionSupportedRequests() {
  // Resolve promises returned by IsSessionSupported
  gfx::VRManagerChild* vm = gfx::VRManagerChild::Get();
  nsTArray<RefPtr<IsSessionSupportedRequest>> isSessionSupportedRequests(
      std::move(mIsSessionSupportedRequests));
  bool featurePolicyBlocked = FeaturePolicyBlocked();

  for (RefPtr<IsSessionSupportedRequest>& request :
       isSessionSupportedRequests) {
    if (featurePolicyBlocked) {
      request->mPromise->MaybeRejectWithSecurityError(
          "The xr-spatial-tracking feature policy is required.");
      continue;
    }

    bool supported = false;
    switch (request->GetSessionMode()) {
      case XRSessionMode::Immersive_vr:
        supported = vm->RuntimeSupportsVR();
        break;
      case XRSessionMode::Immersive_ar:
        supported = vm->RuntimeSupportsAR();
        break;
      default:
        break;
    }
    request->mPromise->MaybeResolve(supported);
  }
}

void XRSystem::ProcessSessionRequestsWaitingForRuntimeDetection() {
  bool alreadyRequestedPermission =
      !mRequestSessionRequestsWaitingForEnumeration.IsEmpty();
  bool featurePolicyBlocked = FeaturePolicyBlocked();
  gfx::VRManagerChild* vm = gfx::VRManagerChild::Get();

  nsTArray<RefPtr<RequestSessionRequest>> sessionRequests(
      std::move(mRequestSessionRequestsWaitingForRuntimeDetection));

  for (RefPtr<RequestSessionRequest>& request : sessionRequests) {
    bool compatibleRuntime = false;
    switch (request->GetSessionMode()) {
      case XRSessionMode::Immersive_vr:
        compatibleRuntime = vm->RuntimeSupportsVR();
        break;
      case XRSessionMode::Immersive_ar:
        compatibleRuntime = vm->RuntimeSupportsAR();
        break;
      case XRSessionMode::Inline:
        compatibleRuntime = vm->RuntimeSupportsInline();
        break;
      default:
        break;
    }
    if (!compatibleRuntime) {
      // If none of the requested sessions are supported by a
      // runtime, early exit without showing a permission prompt.
      if (CancelHardwareRequest(request)) {
        request->mPromise->MaybeRejectWithNotSupportedError(
            "A device supporting the required XRSession configuration "
            "could not be found.");
      }
      continue;
    }
    if (featurePolicyBlocked) {
      // Don't show a permission prompt if blocked by feature policy.
      if (CancelHardwareRequest(request)) {
        request->mPromise->MaybeRejectWithSecurityError(
            "The xr-spatial-tracking feature policy is required.");
      }
      continue;
    }
    // To continue evaluating this request, it must wait for hardware
    // enumeration and permission request.
    mRequestSessionRequestsWaitingForEnumeration.AppendElement(request);
  }

  if (!mRequestSessionRequestsWaitingForEnumeration.IsEmpty() &&
      !alreadyRequestedPermission) {
    /**
     * Inline sessions will require only a user gesture
     * and should not trigger XR permission UI.
     * This is not a problem currently, as the only platforms
     * allowing xr-spatial-tracking for inline sessions do not
     * present a modal XR permission UI. (eg. Android Firefox Reality)
     */
    nsGlobalWindowInner* win = nsGlobalWindowInner::Cast(GetOwner());
    win->RequestXRPermission();
  }
}

void XRSystem::NotifyVRDisplayMounted(uint32_t aDisplayID) {}
void XRSystem::NotifyVRDisplayUnmounted(uint32_t aDisplayID) {}

void XRSystem::NotifyVRDisplayConnect(uint32_t aDisplayID) {
  DispatchTrustedEvent(u"devicechange"_ns);
}

void XRSystem::NotifyVRDisplayDisconnect(uint32_t aDisplayID) {
  DispatchTrustedEvent(u"devicechange"_ns);
}

void XRSystem::NotifyVRDisplayPresentChange(uint32_t aDisplayID) {}
void XRSystem::NotifyPresentationGenerationChanged(uint32_t aDisplayID) {
  if (mActiveImmersiveSession) {
    mActiveImmersiveSession->ExitPresent();
  }
}
bool XRSystem::GetStopActivityStatus() const { return true; }

RequestSessionRequest::RequestSessionRequest(
    XRSessionMode aSessionMode, uint32_t aPresentationGroup, Promise* aPromise,
    const nsTArray<XRReferenceSpaceType>& aRequiredReferenceSpaceTypes,
    const nsTArray<XRReferenceSpaceType>& aOptionalReferenceSpaceTypes)
    : mPromise(aPromise),
      mSessionMode(aSessionMode),
      mPresentationGroup(aPresentationGroup),
      mRequiredReferenceSpaceTypes(aRequiredReferenceSpaceTypes.Clone()),
      mOptionalReferenceSpaceTypes(aOptionalReferenceSpaceTypes.Clone()) {}

bool RequestSessionRequest::ResolveSupport(
    const gfx::VRDisplayClient* aDisplay,
    nsTArray<XRReferenceSpaceType>& aEnabledReferenceSpaceTypes) const {
  if (aDisplay) {
    if (!aDisplay->GetIsConnected()) {
      return false;
    }
    if ((aDisplay->GetDisplayInfo().GetPresentingGroups() &
         mPresentationGroup) != 0) {
      return false;
    }

    const gfx::VRDisplayInfo& info = aDisplay->GetDisplayInfo();
    switch (mSessionMode) {
      case XRSessionMode::Inline:
        if (!bool(info.mDisplayState.capabilityFlags &
                  gfx::VRDisplayCapabilityFlags::Cap_Inline)) {
          return false;
        }
        break;
      case XRSessionMode::Immersive_vr:
        if (!bool(info.mDisplayState.capabilityFlags &
                  gfx::VRDisplayCapabilityFlags::Cap_ImmersiveVR)) {
          return false;
        }
        break;
      case XRSessionMode::Immersive_ar:
        if (!bool(info.mDisplayState.capabilityFlags &
                  gfx::VRDisplayCapabilityFlags::Cap_ImmersiveAR)) {
          return false;
        }
        break;
      default:
        break;
    }
  } else if (mSessionMode != XRSessionMode::Inline) {
    // If we don't have a device, we can only support inline sessions
    return false;
  }

  // All sessions support XRReferenceSpaceType::Viewer by default
  aEnabledReferenceSpaceTypes.AppendElement(XRReferenceSpaceType::Viewer);

  // Immersive sessions support XRReferenceSpaceType::Local by default
  if (IsImmersive()) {
    aEnabledReferenceSpaceTypes.AppendElement(XRReferenceSpaceType::Local);
  }

  for (XRReferenceSpaceType type : mRequiredReferenceSpaceTypes) {
    if (aDisplay) {
      if (!aDisplay->IsReferenceSpaceTypeSupported(type)) {
        return false;
      }
    } else if (type != XRReferenceSpaceType::Viewer) {
      // If we don't have a device, We only support
      // XRReferenceSpaceType::Viewer
      return false;
    }
    if (!aEnabledReferenceSpaceTypes.Contains(type)) {
      aEnabledReferenceSpaceTypes.AppendElement(type);
    }
  }
  if (aDisplay) {
    for (XRReferenceSpaceType type : mOptionalReferenceSpaceTypes) {
      if (aDisplay->IsReferenceSpaceTypeSupported(type) &&
          !aEnabledReferenceSpaceTypes.Contains(type)) {
        aEnabledReferenceSpaceTypes.AppendElement(type);
      }
    }
  }
  return true;
}

bool RequestSessionRequest::IsImmersive() const {
  return (mSessionMode == XRSessionMode::Immersive_vr ||
          mSessionMode == XRSessionMode::Immersive_ar);
}

bool RequestSessionRequest::WantsHardware() const {
  for (XRReferenceSpaceType type : mOptionalReferenceSpaceTypes) {
    // Any XRReferenceSpaceType other than Viewer requires hardware
    if (type != XRReferenceSpaceType::Viewer) {
      return true;
    }
  }
  return NeedsHardware();
}

bool RequestSessionRequest::NeedsHardware() const {
  for (XRReferenceSpaceType type : mRequiredReferenceSpaceTypes) {
    // Any XRReferenceSpaceType other than Viewer requires hardware
    if (type != XRReferenceSpaceType::Viewer) {
      return true;
    }
  }
  return false;
}

XRSessionMode RequestSessionRequest::GetSessionMode() const {
  return mSessionMode;
}

uint32_t RequestSessionRequest::GetPresentationGroup() const {
  return mPresentationGroup;
}

////////////////////////////////////////////////////////////////////////////////
// IsSessionSupportedRequest cycle collection
NS_IMPL_CYCLE_COLLECTION(IsSessionSupportedRequest, mPromise)

XRSessionMode IsSessionSupportedRequest::GetSessionMode() const {
  return mSessionMode;
}

////////////////////////////////////////////////////////////////////////////////
// XRRequestSessionPermissionRequest cycle collection
NS_IMPL_CYCLE_COLLECTION_INHERITED(XRRequestSessionPermissionRequest,
                                   ContentPermissionRequestBase)

NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(
    XRRequestSessionPermissionRequest, ContentPermissionRequestBase)

XRRequestSessionPermissionRequest::XRRequestSessionPermissionRequest(
    nsPIDOMWindowInner* aWindow, nsIPrincipal* aNodePrincipal,
    AllowCallback&& aAllowCallback,
    AllowAnySiteCallback&& aAllowAnySiteCallback,
    CancelCallback&& aCancelCallback)
    : ContentPermissionRequestBase(aNodePrincipal, aWindow, "dom.xr"_ns,
                                   "xr"_ns),
      mAllowCallback(std::move(aAllowCallback)),
      mAllowAnySiteCallback(std::move(aAllowAnySiteCallback)),
      mCancelCallback(std::move(aCancelCallback)),
      mCallbackCalled(false) {
  mPermissionRequests.AppendElement(
      PermissionRequest(mType, nsTArray<nsString>()));
}

XRRequestSessionPermissionRequest::~XRRequestSessionPermissionRequest() {
  Cancel();
}

NS_IMETHODIMP
XRRequestSessionPermissionRequest::Cancel() {
  if (!mCallbackCalled) {
    mCallbackCalled = true;
    mCancelCallback();
  }
  return NS_OK;
}

NS_IMETHODIMP
XRRequestSessionPermissionRequest::Allow(JS::Handle<JS::Value> aChoices) {
  nsTArray<PermissionChoice> choices;
  nsresult rv = TranslateChoices(aChoices, mPermissionRequests, choices);
  if (NS_FAILED(rv)) {
    return rv;
  }

  // There is no support to allow grants automatically from the prompting code
  // path.

  if (!mCallbackCalled) {
    mCallbackCalled = true;
    if (choices.Length() == 1 &&
        choices[0].choice().EqualsLiteral("allow-on-any-site")) {
      mAllowAnySiteCallback();
    } else if (choices.Length() == 1 &&
               choices[0].choice().EqualsLiteral("allow")) {
      mAllowCallback();
    }
  }
  return NS_OK;
}

already_AddRefed<XRRequestSessionPermissionRequest>
XRRequestSessionPermissionRequest::Create(
    nsPIDOMWindowInner* aWindow, AllowCallback&& aAllowCallback,
    AllowAnySiteCallback&& aAllowAnySiteCallback,
    CancelCallback&& aCancelCallback) {
  if (!aWindow) {
    return nullptr;
  }
  nsGlobalWindowInner* win = nsGlobalWindowInner::Cast(aWindow);
  if (!win->GetPrincipal()) {
    return nullptr;
  }
  RefPtr<XRRequestSessionPermissionRequest> request =
      new XRRequestSessionPermissionRequest(
          aWindow, win->GetPrincipal(), std::move(aAllowCallback),
          std::move(aAllowAnySiteCallback), std::move(aCancelCallback));
  return request.forget();
}

////////////////////////////////////////////////////////////////////////////////
// RequestSessionRequest cycle collection
NS_IMPL_CYCLE_COLLECTION(RequestSessionRequest, mPromise)

}  // namespace mozilla::dom
