// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "device/vr/openxr/openxr_device.h"

#include <algorithm>
#include <string>
#include <vector>

#include "base/functional/callback_helpers.h"
#include "base/no_destructor.h"
#include "build/build_config.h"
#include "device/vr/openxr/openxr_api_wrapper.h"
#include "device/vr/openxr/openxr_render_loop.h"
#include "device/vr/openxr/openxr_util.h"
#include "device/vr/public/cpp/features.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "third_party/blink/public/common/features_generated.h"
#include "third_party/openxr/src/include/openxr/openxr.h"

namespace device {

namespace {

const std::vector<mojom::XRSessionFeature>& GetSupportedFeatures() {
  static base::NoDestructor<std::vector<mojom::XRSessionFeature>>
      kSupportedFeatures{{mojom::XRSessionFeature::REF_SPACE_VIEWER,
                          mojom::XRSessionFeature::REF_SPACE_LOCAL,
                          mojom::XRSessionFeature::REF_SPACE_LOCAL_FLOOR,
                          mojom::XRSessionFeature::REF_SPACE_BOUNDED_FLOOR,
                          mojom::XRSessionFeature::REF_SPACE_UNBOUNDED,
                          mojom::XRSessionFeature::ANCHORS,
                          mojom::XRSessionFeature::HAND_INPUT,
                          mojom::XRSessionFeature::SECONDARY_VIEWS,
                          mojom::XRSessionFeature::HIT_TEST,
                          mojom::XRSessionFeature::LIGHT_ESTIMATION,
                          mojom::XRSessionFeature::DEPTH}};

  return *kSupportedFeatures;
}

bool AreAllRequiredFeaturesSupported(
    const mojom::XRSessionMode mode,
    const std::vector<mojom::XRSessionFeature>& required_features,
    const OpenXrExtensionHelper& extension_helper) {
  return std::ranges::all_of(
      required_features,
      [&extension_helper, mode](const mojom::XRSessionFeature& feature) {
        // First we check if we will allow the feature to be supported in the
        // mode that has been requested; before querying if the feature can
        // actually be supported by the current runtime.
        // The extension helper returns true for features that are supported
        // entirely by the core spec. We rely on the Browser process
        // pre-filtering and not passing us any features that we haven't already
        // indicated that we could support, which is the union of core spec
        // features and things that could theoretically be supported depending
        // on enabled extensions (which we're now checking if they're actually
        // supported,since we need to create an instance to confirm that).
        const bool supported = IsFeatureSupportedForMode(feature, mode) &&
                               extension_helper.IsFeatureSupported(feature);
        if (!supported) {
          DVLOG(1) << __func__ << " " << feature << " not supported";
        }

        return supported;
      });
}
}  // namespace

OpenXrDevice::OpenXrDevice(
    VizContextProviderFactoryAsync context_provider_factory_async,
    OpenXrPlatformHelper* platform_helper)
    : VRDeviceBase(device::mojom::XRDeviceId::OPENXR_DEVICE_ID),
      context_provider_factory_async_(
          std::move(context_provider_factory_async)),
      platform_helper_(platform_helper) {
  CHECK(platform_helper_);
  CHECK(platform_helper_->EnsureInitialized());

  device::mojom::XRDeviceData device_data = platform_helper_->GetXRDeviceData();

  device_data.supported_features = GetSupportedFeatures();

  // Only support layers if the feature flag is enabled.
  if (base::FeatureList::IsEnabled(features::kWebXRLayers)) {
    device_data.supported_features.emplace_back(
        mojom::XRSessionFeature::LAYERS);
  }

  // Only support WebGPU sessions if the feature flag is enabled; the Linux
  // Vulkan binding does not support WebGPU sessions yet.
#if !BUILDFLAG(IS_LINUX)
  if (base::FeatureList::IsEnabled(features::kWebXRWebGPUBinding)) {
    device_data.supported_features.emplace_back(
        mojom::XRSessionFeature::WEBGPU);
  }
#endif

  // Only support Plane Detection if the feature flag is enabled.
  if (base::FeatureList::IsEnabled(features::kWebXRPlaneDetection)) {
    device_data.supported_features.emplace_back(
        mojom::XRSessionFeature::PLANE_DETECTION);
  }
  // Only support Mesh Detection if the feature flag is enabled.
  if (base::FeatureList::IsEnabled(blink::features::kWebXRMeshDetection)) {
    device_data.supported_features.emplace_back(
        mojom::XRSessionFeature::MESH_DETECTION);
  }

  SetDeviceData(std::move(device_data));
}

OpenXrDevice::~OpenXrDevice() {
  // Wait for the render loop to stop before completing destruction. This will
  // ensure that the render loop doesn't get shutdown while it is processing
  // any requests.
  render_loop_.reset();

  if (instance_ != XR_NULL_HANDLE) {
    platform_helper_->DestroyInstance(instance_);
  }

  // request_session_callback_ may still be active if we're tearing down the
  // OpenXrDevice while we're still making asynchronous calls to setup the GPU
  // process connection. Ensure the callback is run regardless.
  if (request_session_callback_) {
    std::move(request_session_callback_).Run(nullptr);
  }

  if (shutdown_request_callback_) {
    std::move(shutdown_request_callback_).Run();
  }
}

void OpenXrDevice::RequestSession(
    mojom::XRRuntimeSessionOptionsPtr options,
    mojom::XRRuntime::RequestSessionCallback callback) {
  CHECK(!request_session_callback_);
  CHECK(!HasExclusiveSession());

  request_session_callback_ = std::move(callback);

  OpenXrCreateInfo create_info;
#if BUILDFLAG(IS_ANDROID)
  if (options->renderer_information) {
    create_info.render_process_id =
        options->renderer_information->render_process_id;
    create_info.render_frame_id =
        options->renderer_information->render_frame_id;
  }
#endif
  platform_helper_->CreateInstanceWithCreateInfo(
      create_info,
      base::BindOnce(&OpenXrDevice::OnCreateInstanceResult,
                     weak_ptr_factory_.GetWeakPtr(), std::move(options)),
      base::BindOnce(&OpenXrDevice::ForceEndSession,
                     weak_ptr_factory_.GetWeakPtr(),
                     ExitXrPresentReason::kXrPlatformHelperShutdown));
}

void OpenXrDevice::OnCreateInstanceResult(
    mojom::XRRuntimeSessionOptionsPtr options,
    XrResult result,
    XrInstance instance) {
  if (XR_FAILED(result)) {
    DVLOG(1) << __func__ << " Failed to create an XrInstance";
    instance_ = XR_NULL_HANDLE;
    std::move(request_session_callback_).Run(nullptr);
    return;
  }

  instance_ = instance;

  extension_helper_ = std::make_unique<OpenXrExtensionHelper>(
      instance_, platform_helper_->GetExtensionEnumeration());

  // Now that we have an instance, check if it's even theoretically possible
  // to support all of our required features. While this check isn't final, as
  // the OpenXrRenderLoop will make that ultimate determination, it can help
  // us early-exit and avoid spinning it up if we know we don't even have the
  // extensions necessary to support a required feature.
  if (!AreAllRequiredFeaturesSupported(
          options->mode, options->required_features, *extension_helper_)) {
    DVLOG(1) << __func__ << " Missing a required feature";
    // Reject session request, and call ForceEndSession to ensure that we clean
    // up any objects that were already created.
    ForceEndSession(ExitXrPresentReason::kOpenXrStartFailed);
    std::move(request_session_callback_).Run(nullptr);
    return;
  }

  if (!render_loop_) {
    render_loop_ = std::make_unique<OpenXrRenderLoop>(
        context_provider_factory_async_, instance_, *extension_helper_,
        platform_helper_);
    render_loop_->Start();
  }

  auto my_callback = base::BindOnce(&OpenXrDevice::OnRequestSessionResult,
                                    weak_ptr_factory_.GetWeakPtr());

  auto on_visibility_state_changed = base::BindRepeating(
      &OpenXrDevice::OnVisibilityStateChanged, weak_ptr_factory_.GetWeakPtr());

  auto session_ended_callback = base::BindOnce(&OpenXrDevice::OnSessionEnded,
                                               weak_ptr_factory_.GetWeakPtr());

  render_loop_->task_runner()->PostTask(
      FROM_HERE, base::BindOnce(&OpenXrRenderLoop::RequestSession,
                                base::Unretained(render_loop_.get()),
                                std::move(on_visibility_state_changed),
                                std::move(options), std::move(my_callback),
                                std::move(session_ended_callback)));
}

void OpenXrDevice::OnRequestSessionResult(
    bool result,
    mojom::XRSessionPtr session,
    mojo::PendingRemote<mojom::ImmersiveOverlay> overlay) {
  DCHECK(request_session_callback_);

  if (!result) {
    // Reject session request, and call ForceEndSession to ensure that we clean
    // up any objects that were already created.
    ForceEndSession(ExitXrPresentReason::kOpenXrStartFailed);
    std::move(request_session_callback_).Run(nullptr);
    return;
  }

  OnStartPresenting();

  auto session_result = mojom::XRRuntimeSessionResult::New();
  session_result->session = std::move(session);
  session_result->controller =
      exclusive_controller_receiver_.BindNewPipeAndPassRemote();
  session_result->overlay = std::move(overlay);

  std::move(request_session_callback_).Run(std::move(session_result));

  // Use of Unretained is safe because the callback will only occur if the
  // binding is not destroyed.
  exclusive_controller_receiver_.set_disconnect_handler(
      base::BindOnce(&OpenXrDevice::OnPresentingControllerMojoConnectionError,
                     base::Unretained(this)));
}

void OpenXrDevice::ForceEndSession(ExitXrPresentReason reason) {
  // This method is called when the rendering process exit presents.
  if (render_loop_) {
    render_loop_->task_runner()->PostTask(
        FROM_HERE,
        base::BindOnce(&OpenXrRenderLoop::ExitPresent,
                       base::Unretained(render_loop_.get()), reason));
    render_loop_.reset();
  } else {
    // If we have a render loop, the act of it finishing shutting down will call
    // OnSessionEnded for us.
    OnSessionEnded();
  }
}

void OpenXrDevice::OnSessionEnded() {
  OnExitPresent();
  exclusive_controller_receiver_.reset();

  render_loop_.reset();

  extension_helper_.reset();
  if (instance_ != XR_NULL_HANDLE) {
    platform_helper_->DestroyInstance(instance_);
  }

  if (shutdown_request_callback_) {
    std::move(shutdown_request_callback_).Run();
  }
}

void OpenXrDevice::OnPresentingControllerMojoConnectionError() {
  ForceEndSession(ExitXrPresentReason::kMojoConnectionError);
}

void OpenXrDevice::ShutdownSession(
    mojom::XRRuntime::ShutdownSessionCallback callback) {
  DVLOG(1) << __func__;
  if (!HasExclusiveSession()) {
    std::move(callback).Run();
    return;
  }

  shutdown_request_callback_ = std::move(callback);
  platform_helper_->PrepareForSessionShutdown(base::BindOnce(
      &OpenXrDevice::ForceEndSession, weak_ptr_factory_.GetWeakPtr(),
      ExitXrPresentReason::kBrowserShutdown));
}

void OpenXrDevice::SetFrameDataRestricted(bool restricted) {
  // Presentation sessions can not currently be restricted.
  NOTREACHED();
}

}  // namespace device
