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

#include "content/browser/xr/service/browser_xr_runtime_impl.h"

#include <algorithm>
#include <memory>
#include <utility>

#include "base/functional/callback_helpers.h"
#include "base/logging.h"
#include "base/observer_list.h"
#include "build/build_config.h"
#include "content/browser/xr/service/vr_service_impl.h"
#include "content/browser/xr/xr_utils.h"
#include "content/public/browser/browser_xr_runtime.h"
#include "content/public/browser/xr_install_helper.h"
#include "content/public/browser/xr_integration_client.h"
#include "content/public/common/content_features.h"
#include "device/vr/buildflags/buildflags.h"
#include "device/vr/public/cpp/session_mode.h"
#include "device/vr/public/mojom/vr_service.mojom-shared.h"
#include "device/vr/public/mojom/xr_device.mojom-shared.h"
#include "device/vr/public/mojom/xr_session.mojom-shared.h"
#include "ui/gfx/geometry/decomposed_transform.h"
#include "ui/gfx/geometry/transform.h"

#if BUILDFLAG(IS_WIN)
#include "base/win/windows_types.h"
#endif

namespace content {
namespace {
bool IsValidTransform(const gfx::Transform& transform) {
  if (!transform.IsInvertible() || transform.HasPerspective())
    return false;

  std::optional<gfx::DecomposedTransform> decomp = transform.Decompose();
  if (!decomp)
    return false;

  float kEpsilon = 0.1f;
  if (abs(decomp->perspective[3] - 1) > kEpsilon) {
    // If testing with unexpectedly high values, catch on debug builds rather
    // than silently change data.  On release builds its better to be safe and
    // validate.
    DCHECK(false);
    return false;
  }
  for (int i = 0; i < 3; ++i) {
    if (abs(decomp->scale[i] - 1) > kEpsilon)
      return false;
    if (abs(decomp->skew[i]) > kEpsilon)
      return false;
    if (abs(decomp->perspective[i]) > kEpsilon)
      return false;
  }

  // Only rotate and translate.
  return true;
}

device::mojom::XRViewPtr ValidateXRView(const device::mojom::XRView* view) {
  if (!view) {
    return nullptr;
  }
  device::mojom::XRViewPtr ret = device::mojom::XRView::New();
  ret->eye = view->eye;
  // FOV
  float kDefaultFOV = 45;
  ret->geometry = device::mojom::XRViewGeometry::New();
  ret->geometry->field_of_view = device::mojom::VRFieldOfView::New();
  auto& view_fov = view->geometry->field_of_view;
  auto& ret_fov = ret->geometry->field_of_view;
  if (view_fov->up_degrees < 90 && view_fov->up_degrees > -90 &&
      view_fov->up_degrees > -view_fov->down_degrees &&
      view_fov->down_degrees < 90 && view_fov->down_degrees > -90 &&
      view_fov->down_degrees > -view_fov->up_degrees &&
      view_fov->left_degrees < 90 && view_fov->left_degrees > -90 &&
      view_fov->left_degrees > -view_fov->right_degrees &&
      view_fov->right_degrees < 90 && view_fov->right_degrees > -90 &&
      view_fov->right_degrees > -view_fov->left_degrees) {
    ret_fov->up_degrees = view_fov->up_degrees;
    ret_fov->down_degrees = view_fov->down_degrees;
    ret_fov->left_degrees = view_fov->left_degrees;
    ret_fov->right_degrees = view_fov->right_degrees;
  } else {
    ret_fov->up_degrees = kDefaultFOV;
    ret_fov->down_degrees = kDefaultFOV;
    ret_fov->left_degrees = kDefaultFOV;
    ret_fov->right_degrees = kDefaultFOV;
  }

  if (IsValidTransform(view->geometry->mojo_from_view)) {
    ret->geometry->mojo_from_view = view->geometry->mojo_from_view;
  }
  // else, ret->geometry->mojo_from_view remains the identity transform

  // Renderwidth/height
  int kMaxSize = 16384;
  int kMinSize = 2;
  // DCHECK on debug builds to catch legitimate large sizes, but clamp on
  // release builds to ensure valid state.
  DCHECK_LT(view->viewport.width() + view->viewport.x(), kMaxSize);
  DCHECK_LT(view->viewport.height() + view->viewport.y(), kMaxSize);
  DCHECK_GT(view->viewport.width() + view->viewport.x(), kMinSize);
  DCHECK_GT(view->viewport.height() + view->viewport.y(), kMinSize);
  ret->viewport =
      gfx::Rect(std::clamp(view->viewport.x(), 0, kMaxSize),
                std::clamp(view->viewport.y(), 0, kMaxSize),
                std::clamp(view->viewport.width(), kMinSize, kMaxSize),
                std::clamp(view->viewport.height(), kMinSize, kMaxSize));
  return ret;
}

}  // anonymous namespace

BrowserXRRuntimeImpl::BrowserXRRuntimeImpl(
    device::mojom::XRDeviceId id,
    device::mojom::XRDeviceDataPtr device_data,
    mojo::PendingRemote<device::mojom::XRRuntime> runtime)
    : id_(id),
      device_data_(std::move(device_data)),
      runtime_(std::move(runtime)) {
  DVLOG(2) << __func__ << ": id=" << id;

  runtime_->ListenToDeviceChanges(receiver_.BindNewEndpointAndPassRemote());

  auto* integration_client = GetXrIntegrationClient();

  if (integration_client) {
    install_helper_ = integration_client->GetInstallHelper(id_);
    runtime_observer_ = integration_client->CreateRuntimeObserver();

    if (runtime_observer_) {
      AddObserver(runtime_observer_.get());
    }
  }
}

BrowserXRRuntimeImpl::~BrowserXRRuntimeImpl() {
  DVLOG(2) << __func__ << ": id=" << id_;

  if (runtime_observer_) {
    RemoveObserver(runtime_observer_.get());
  }

  if (install_finished_callback_) {
    std::move(install_finished_callback_).Run(XrInstallResult::kFailed);
  }
}

void BrowserXRRuntimeImpl::ExitActiveImmersiveSession() {
  DVLOG(2) << __func__;
  auto* service = GetServiceWithActiveImmersiveSession();
  if (service) {
    service->ExitPresent(base::DoNothing());
  }
}

bool BrowserXRRuntimeImpl::SupportsFeature(
    device::mojom::XRSessionFeature feature) const {
  if(id_ == device::mojom::XRDeviceId::WEB_TEST_DEVICE_ID ||
     id_ == device::mojom::XRDeviceId::FAKE_DEVICE_ID)
      return true;

  return std::ranges::contains(device_data_->supported_features, feature);
}

bool BrowserXRRuntimeImpl::SupportsAllFeatures(
    const std::vector<device::mojom::XRSessionFeature>& features) const {
  for (const auto& feature : features) {
    if (!SupportsFeature(feature))
      return false;
  }

  return true;
}

bool BrowserXRRuntimeImpl::SupportsCustomIPD() const {
  switch (id_) {
    case device::mojom::XRDeviceId::WEB_TEST_DEVICE_ID:
    case device::mojom::XRDeviceId::FAKE_DEVICE_ID:
    case device::mojom::XRDeviceId::ORIENTATION_DEVICE_ID:
#if BUILDFLAG(ENABLE_ARCORE)
    case device::mojom::XRDeviceId::ARCORE_DEVICE_ID:
#endif  // ENABLE_ARCORE
#if BUILDFLAG(ENABLE_CARDBOARD)
    case device::mojom::XRDeviceId::CARDBOARD_DEVICE_ID:
#endif  // ENABLE_CARDBOARD
      return false;
#if BUILDFLAG(ENABLE_OPENXR)
    case device::mojom::XRDeviceId::OPENXR_DEVICE_ID:
      return true;
#endif
  }

  NOTREACHED();
}

bool BrowserXRRuntimeImpl::SupportsNonEmulatedHeight() const {
  switch (id_) {
    case device::mojom::XRDeviceId::WEB_TEST_DEVICE_ID:
    case device::mojom::XRDeviceId::FAKE_DEVICE_ID:
    case device::mojom::XRDeviceId::ORIENTATION_DEVICE_ID:
#if BUILDFLAG(ENABLE_ARCORE)
    case device::mojom::XRDeviceId::ARCORE_DEVICE_ID:
#endif  // ENABLE_ARCORE
      return false;
#if BUILDFLAG(ENABLE_CARDBOARD)
    case device::mojom::XRDeviceId::CARDBOARD_DEVICE_ID:
      return true;
#endif  // ENABLE_CARDBOARD
#if BUILDFLAG(ENABLE_OPENXR)
    case device::mojom::XRDeviceId::OPENXR_DEVICE_ID:
      return true;
#endif  // ENABLE_OPENXR
  }

  NOTREACHED();
}

bool BrowserXRRuntimeImpl::SupportsArBlendMode() {
  return device_data_->is_ar_blend_mode_supported;
}

void BrowserXRRuntimeImpl::StopImmersiveSession() {
  DVLOG(2) << __func__;

  if (immersive_session_has_camera_access_) {
    for (Observer& observer : observers_) {
      observer.WebXRCameraInUseChanged(nullptr, false);
    }
    immersive_session_has_camera_access_ = false;
  }

  if (immersive_session_controller_) {
    immersive_session_controller_.reset();
    if (presenting_service_) {
      presenting_service_->OnExitPresent();
      presenting_service_ = nullptr;
    }
  }

  vr_ui_host_.reset();
}

void BrowserXRRuntimeImpl::OnExitPresent() {
  DVLOG(2) << __func__;
  if (presenting_service_) {
    presenting_service_->OnExitPresent();
    presenting_service_ = nullptr;
  }
}

void BrowserXRRuntimeImpl::OnVisibilityStateChanged(
    device::mojom::XRVisibilityState visibility_state) {
  for (VRServiceImpl* service : services_) {
    service->OnVisibilityStateChanged(visibility_state);
  }
}

void BrowserXRRuntimeImpl::OnServiceAdded(VRServiceImpl* service) {
  DVLOG(2) << __func__ << ": id=" << id_;
  services_.insert(service);
}

void BrowserXRRuntimeImpl::OnServiceRemoved(VRServiceImpl* service) {
  DVLOG(2) << __func__ << ": id=" << id_;
  DCHECK(service);
  services_.erase(service);
  if (service == presenting_service_) {
    // Our presenting service is no longer valid, so we need to clear it before
    // shutting down the session on the runtime side. Note that while
    // `ExitPresent` looks similar, it may not be called by the presenting
    // service, in which case the service needs to be notified after the
    // shutdown is completed, so we can't simply move the check/clear down into
    // `ShutdownRuntime`.
    presenting_service_ = nullptr;
    ShutdownRuntime();
  }
}

void BrowserXRRuntimeImpl::ExitPresent(VRServiceImpl* service) {
  DVLOG(2) << __func__ << ": id=" << id_ << " service=" << service
           << " presenting_service_=" << presenting_service_;
  if (service == presenting_service_) {
    ShutdownRuntime();
  }
}

void BrowserXRRuntimeImpl::ShutdownRuntime() {
  // As part of it's shutdown, the runtime will disconnect this pipe. If we do
  // not clear the current disconnect handler we'll essentially signal to blink
  // too early that the session has shutdown. This has led to race conditions in
  // tests that end the session from blink and then immediately start a new
  // session where the pending `StopImmersiveSession` callback happens after a
  // new session was granted and then kills the new session.
  immersive_session_controller_.set_disconnect_handler(base::DoNothing());
  runtime_->ShutdownSession(
      base::BindOnce(&BrowserXRRuntimeImpl::StopImmersiveSession,
                     weak_ptr_factory_.GetWeakPtr()));
}

void BrowserXRRuntimeImpl::SetFramesThrottled(const VRServiceImpl* service,
                                              bool throttled) {
  if (service == presenting_service_ && vr_ui_host_) {
    vr_ui_host_->WebXRFramesThrottledChanged(throttled);
  }
}

void BrowserXRRuntimeImpl::RequestInlineSession(
    device::mojom::XRRuntimeSessionOptionsPtr options,
    device::mojom::XRRuntime::RequestSessionCallback callback) {
  runtime_->RequestSession(std::move(options), std::move(callback));
}

void BrowserXRRuntimeImpl::RequestImmersiveSession(
    VRServiceImpl* service,
    device::mojom::XRRuntimeSessionOptionsPtr options,
    RequestSessionCallback callback) {
  DVLOG(2) << __func__ << ": id=" << id_;
  // base::Unretained is safe because we won't be called back after runtime_ is
  // destroyed.
  has_pending_immersive_session_request_ = true;
  runtime_->RequestSession(
      options->Clone(),
      base::BindOnce(&BrowserXRRuntimeImpl::OnRequestSessionResult,
                     base::Unretained(this), service->GetWeakPtr(),
                     options->Clone(), std::move(callback)));
}

void BrowserXRRuntimeImpl::OnRequestSessionResult(
    base::WeakPtr<VRServiceImpl> service,
    device::mojom::XRRuntimeSessionOptionsPtr options,
    RequestSessionCallback callback,
    device::mojom::XRRuntimeSessionResultPtr session_result) {
  has_pending_immersive_session_request_ = false;
  if (session_result && service) {
    DVLOG(2) << __func__ << ": id=" << id_;
    if (device::XRSessionModeUtils::IsImmersive(options->mode)) {
      presenting_service_ = service.get();
      immersive_session_controller_.Bind(std::move(session_result->controller));
      immersive_session_controller_.set_disconnect_handler(
          base::BindOnce(&BrowserXRRuntimeImpl::OnImmersiveSessionError,
                         base::Unretained(this)));

      content::WebContents* web_contents = service->GetWebContents();
      auto* integration_client = GetXrIntegrationClient();
      if (session_result->overlay && integration_client && web_contents) {
        // We have enough information to create a VrUiHost, so validate that
        // information then attempt to create it.
        std::vector<device::mojom::XRViewPtr>& views =
            session_result->session->device_config->views;

        for (device::mojom::XRViewPtr& view : views) {
          view = ValidateXRView(view.get());
        }

        // The overlay code requires the left and right views to render.
        if (!std::ranges::contains(views, device::mojom::XREye::kLeft,
                                   &device::mojom::XRView::eye) ||
            !std::ranges::contains(views, device::mojom::XREye::kRight,
                                   &device::mojom::XRView::eye)) {
          // Notify the service to cleanup any session that it's started to
          // setup, and when that and our corresponding runtime shutdown have
          // finished, notify the page that the session request failed.
          service->ExitPresent(base::BindOnce(
              [](RequestSessionCallback callback) {
                std::move(callback).Run(nullptr);
              },
              std::move(callback)));
          return;
        }

        vr_ui_host_ = integration_client->CreateVrUiHost(
            *web_contents, views, std::move(session_result->overlay));
      }

      immersive_session_has_camera_access_ =
          std::ranges::contains(session_result->session->enabled_features,
                                device::mojom::XRSessionFeature::CAMERA_ACCESS);
      if (immersive_session_has_camera_access_) {
        for (Observer& observer : observers_) {
          observer.WebXRCameraInUseChanged(web_contents, true);
        }
      }
    }

    std::move(callback).Run(std::move(session_result));
  } else {
    std::move(callback).Run(nullptr);
    if (session_result) {
      // The service has been removed, but we still got a session, so make
      // sure to clean up this weird state.
      immersive_session_controller_.Bind(std::move(session_result->controller));
      StopImmersiveSession();
    }
  }
}

void BrowserXRRuntimeImpl::EnsureInstalled(
    const content::GlobalRenderFrameHostId& frame_id,
    base::OnceCallback<void(XrInstallResult)> install_callback) {
  DVLOG(2) << __func__;

  // If there's no install helper, then we can assume no install is needed.
  if (!install_helper_) {
    std::move(install_callback).Run(XrInstallResult::kSuccessAlreadyInstalled);
    return;
  }

  // Only the most recent caller will be notified of a successful install.
  bool had_outstanding_callback = false;
  if (install_finished_callback_) {
    had_outstanding_callback = true;
    std::move(install_finished_callback_).Run(XrInstallResult::kFailed);
  }

  install_finished_callback_ = std::move(install_callback);

  // If we already had a cached install callback, then we don't need to query
  // for installation again.
  if (had_outstanding_callback)
    return;

  install_helper_->EnsureInstalled(
      frame_id, base::BindOnce(&BrowserXRRuntimeImpl::OnInstallFinished,
                               weak_ptr_factory_.GetWeakPtr()));
}

void BrowserXRRuntimeImpl::OnInstallFinished(XrInstallResult result) {
  DCHECK(install_finished_callback_);

  std::move(install_finished_callback_).Run(result);
}

void BrowserXRRuntimeImpl::OnImmersiveSessionError() {
  DVLOG(2) << __func__ << ": id=" << id_;
  StopImmersiveSession();
}

void BrowserXRRuntimeImpl::AddObserver(Observer* observer) {
  observers_.AddObserver(observer);
}

void BrowserXRRuntimeImpl::RemoveObserver(Observer* observer) {
  observers_.RemoveObserver(observer);
}

void BrowserXRRuntimeImpl::BeforeRuntimeRemoved() {
  DVLOG(1) << __func__ << ": id=" << id_;

  // If the device process crashes or otherwise gets removed, it's a race as to
  // whether or not our mojo interface to the device gets reset before we're
  // deleted as the result of the device provider being destroyed.
  // Since this no-ops if we don't have an active immersive session, try to end
  // any immersive session we may be currently responsible for.
  StopImmersiveSession();
}

std::vector<device::mojom::XRSessionFeature>
BrowserXRRuntimeImpl::GetSupportedFeatures() {
  return device_data_->supported_features;
}

#if BUILDFLAG(IS_WIN)
std::optional<CHROME_LUID> BrowserXRRuntimeImpl::GetLuid() const {
  return device_data_->luid;
}
#endif

}  // namespace content
