// Copyright 2025 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_spatial_anchor_manager.h"

#include <algorithm>
#include <array>
#include <optional>
#include <utility>
#include <vector>

#include "base/functional/callback_helpers.h"
#include "base/types/expected.h"
#include "device/vr/openxr/openxr_api_wrapper.h"
#include "device/vr/openxr/openxr_extension_helper.h"
#include "device/vr/openxr/openxr_platform.h"
#include "device/vr/openxr/openxr_spatial_framework_manager.h"
#include "device/vr/openxr/openxr_spatial_plane_manager.h"
#include "device/vr/openxr/openxr_spatial_utils.h"
#include "device/vr/openxr/openxr_util.h"
#include "device/vr/openxr/scoped_openxr_object.h"
#include "third_party/abseil-cpp/absl/container/flat_hash_map.h"
#include "third_party/abseil-cpp/absl/container/flat_hash_set.h"
#include "third_party/openxr/src/include/openxr/openxr.h"

namespace device {

// static
bool OpenXrSpatialAnchorManager::IsSupported(
    const std::vector<XrSpatialCapabilityEXT>& capabilities) {
  // The only component that we need to support anchors is
  // XR_SPATIAL_COMPONENT_TYPE_ANCHOR_EXT, which is guaranteed to be supported
  // if the XR_SPATIAL_CAPABILITY_ANCHOR_EXT is supported, so that's all we need
  // to check.
  return std::ranges::contains(capabilities, XR_SPATIAL_CAPABILITY_ANCHOR_EXT);
}

OpenXrSpatialAnchorManager::OpenXrSpatialAnchorManager(
    const OpenXrExtensionHelper& extension_helper,
    const OpenXrSpatialFrameworkManager& spatial_framework_manager,
    OpenXrSpatialPlaneManager* plane_manager,
    XrSpace mojo_space)
    : extension_helper_(extension_helper),
      spatial_framework_manager_(spatial_framework_manager),
      plane_manager_(plane_manager),
      mojo_space_(mojo_space) {}

OpenXrSpatialAnchorManager::~OpenXrSpatialAnchorManager() {
  for (auto const& [id, anchor] : anchors_) {
    extension_helper_->ExtensionMethods().xrDestroySpatialEntityEXT(
        anchor.entity);
  }
}

void OpenXrSpatialAnchorManager::PopulateCapabilityConfiguration(
    absl::flat_hash_map<XrSpatialCapabilityEXT,
                        absl::flat_hash_set<XrSpatialComponentTypeEXT>>&
        capability_configuration) const {
  // Operator[] creates an empty entry if it does not exist.
  capability_configuration[XR_SPATIAL_CAPABILITY_ANCHOR_EXT].insert(
      XR_SPATIAL_COMPONENT_TYPE_ANCHOR_EXT);

  // In order to set a Plane as our Parent we need to also enable the parent
  // component type. Note that this component is guaranteed by the same
  // extension that allows the plane manager to say that they can be our parent.
  if (plane_manager_ && plane_manager_->can_parent_anchors()) {
    capability_configuration[XR_SPATIAL_CAPABILITY_ANCHOR_EXT].insert(
        XR_SPATIAL_COMPONENT_TYPE_PARENT_EXT);
  }
}

AnchorId OpenXrSpatialAnchorManager::CreateAnchor(
    XrPosef pose,
    XrSpace space,
    XrTime predicted_display_time,
    std::optional<PlaneId> plane_id) {
  XrSpatialAnchorCreateInfoEXT create_info{
      XR_TYPE_SPATIAL_ANCHOR_CREATE_INFO_EXT};
  create_info.baseSpace = space;
  create_info.time = predicted_display_time;
  create_info.pose = pose;

  XrSpatialEntityIdEXT parent_entity = XR_NULL_SPATIAL_ENTITY_ID_EXT;
  if (plane_manager_ && plane_manager_->can_parent_anchors() &&
      plane_id.has_value()) {
    parent_entity = plane_manager_->GetEntityId(*plane_id);
  }

  // This type is small enough to create even if we won't use it, since it needs
  // to be created outside the scope of the if block to attach it to the `next`
  // chain.
  XrSpatialAnchorParentANDROID parent_info{
      .type = XR_TYPE_SPATIAL_ANCHOR_PARENT_ANDROID, .parentId = parent_entity};

  if (parent_entity != XR_NULL_SPATIAL_ENTITY_ID_EXT) {
    create_info.next = &parent_info;
  }

  SpatialAnchorData anchor_data;
  if (XR_FAILED(extension_helper_->ExtensionMethods().xrCreateSpatialAnchorEXT(
          spatial_framework_manager_->GetSpatialContext(), &create_info,
          &anchor_data.entity_id, &anchor_data.entity))) {
    return kInvalidAnchorId;
  }

  AnchorId anchor_id = anchor_id_generator_.GenerateNextId();
  CHECK(anchor_id);
  anchors_.emplace(anchor_id, anchor_data);
  entity_id_to_anchor_id_.emplace(anchor_data.entity_id, anchor_id);
  return anchor_id;
}

void OpenXrSpatialAnchorManager::DetachAnchor(AnchorId anchor_id) {
  auto it = anchors_.find(anchor_id);
  if (it == anchors_.end()) {
    return;
  }

  extension_helper_->ExtensionMethods().xrDestroySpatialEntityEXT(
      it->second.entity);
  entity_id_to_anchor_id_.erase(it->second.entity_id);
  anchors_.erase(it);
  cached_anchor_poses_.erase(anchor_id);
}

std::optional<XrLocation> OpenXrSpatialAnchorManager::GetXrLocationFromAnchor(
    AnchorId anchor_id,
    const gfx::Transform& anchor_id_from_new_anchor) const {
  auto it = cached_anchor_poses_.find(anchor_id);
  if (it == cached_anchor_poses_.end() || !it->second.has_value()) {
    return std::nullopt;
  }

  // The cached anchor poses are in |mojo_space_| hence mojo_from_anchor_id.
  gfx::Transform mojo_from_anchor_id = it->second->ToTransform();
  gfx::Transform mojo_from_new_anchor =
      mojo_from_anchor_id * anchor_id_from_new_anchor;

  return XrLocation{GfxTransformToXrPose(mojo_from_new_anchor), mojo_space_};
}

mojom::XRAnchorsDataPtr OpenXrSpatialAnchorManager::GetCurrentAnchorsData(
    XrTime predicted_display_time) {
  cached_anchor_poses_.clear();
  if (anchors_.empty()) {
    return nullptr;
  }

  std::vector<XrSpatialEntityEXT> entities;
  entities.reserve(anchors_.size());
  for (auto const& [_, anchor] : anchors_) {
    entities.push_back(anchor.entity);
  }

  XrSpatialUpdateSnapshotCreateInfoEXT snapshot_create_info{
      XR_TYPE_SPATIAL_UPDATE_SNAPSHOT_CREATE_INFO_EXT};
  snapshot_create_info.entityCount = entities.size();
  snapshot_create_info.entities = entities.data();
  snapshot_create_info.baseSpace = mojo_space_;
  snapshot_create_info.time = predicted_display_time;

  ScopedOpenXrObject<XrSpatialSnapshotEXT> snapshot(extension_helper_.get());
  if (XR_FAILED(extension_helper_->ExtensionMethods()
                    .xrCreateSpatialUpdateSnapshotEXT(
                        spatial_framework_manager_->GetSpatialContext(),
                        &snapshot_create_info, snapshot.receive()))) {
    return nullptr;
  }

  constexpr std::array<XrSpatialComponentTypeEXT, 1> kComponentTypes = {
      XR_SPATIAL_COMPONENT_TYPE_ANCHOR_EXT};
  XrSpatialComponentDataQueryConditionEXT query_cond{
      XR_TYPE_SPATIAL_COMPONENT_DATA_QUERY_CONDITION_EXT};
  query_cond.componentTypeCount = kComponentTypes.size();
  query_cond.componentTypes = kComponentTypes.data();

  // Locations is an out parameter, so pre-fill it with empty poses.
  std::vector<XrPosef> locations(anchors_.size());
  XrSpatialComponentAnchorListEXT location_list{
      XR_TYPE_SPATIAL_COMPONENT_ANCHOR_LIST_EXT};
  location_list.locationCount = locations.size();
  location_list.locations = locations.data();

  XrSpatialComponentDataQueryResultEXT query_result{
      XR_TYPE_SPATIAL_COMPONENT_DATA_QUERY_RESULT_EXT};
  query_result.next = &location_list;

  // tracking_states is an out parameter, so pre-fill it with empty poses.
  std::vector<XrSpatialEntityTrackingStateEXT> tracking_states(anchors_.size());
  query_result.entityStateCapacityInput = tracking_states.size();
  query_result.entityStates = tracking_states.data();

  // entity_ids is an out parameter, so pre-fill it with empty poses.
  std::vector<XrSpatialEntityIdEXT> entity_ids(anchors_.size());
  query_result.entityIdCapacityInput = entity_ids.size();
  query_result.entityIds = entity_ids.data();

  if (XR_FAILED(
          extension_helper_->ExtensionMethods().xrQuerySpatialComponentDataEXT(
              snapshot.get(), &query_cond, &query_result))) {
    return nullptr;
  }

  // No matter what, we will send all current anchors up to the page, and then
  // we will remove any deleted ones for the next frame.
  std::vector<AnchorId> all_anchors_ids;
  std::vector<mojom::XRAnchorDataPtr> updated_anchors;
  all_anchors_ids.reserve(anchors_.size());
  updated_anchors.reserve(anchors_.size());
  absl::flat_hash_set<AnchorId> permanently_stopped_anchors;

  // Per the spec, the order of entities in the `entity_ids` list matches the
  // order of the entities in both the tracking states and locations lists.
  for (uint32_t i = 0; i < query_result.entityIdCountOutput; i++) {
    auto it = entity_id_to_anchor_id_.find(entity_ids[i]);
    if (it == entity_id_to_anchor_id_.end()) {
      continue;
    }

    AnchorId anchor_id = it->second;
    all_anchors_ids.push_back(anchor_id);

    switch (tracking_states[i]) {
      case XR_SPATIAL_ENTITY_TRACKING_STATE_TRACKING_EXT: {
        // If the anchor is tracked, it has a valid pose, and we should cache it
        // and then send that pose as the updated location.
        auto pose = XrPoseToDevicePose(locations[i]);
        cached_anchor_poses_.emplace(anchor_id, pose);
        updated_anchors.push_back(mojom::XRAnchorData::New(anchor_id, pose));
        break;
      }
      case XR_SPATIAL_ENTITY_TRACKING_STATE_PAUSED_EXT: {
        // If it's paused we'll still put it in the cache as it may come back,
        // but we don't know a pose for it at present.
        cached_anchor_poses_.emplace(anchor_id, std::nullopt);
        updated_anchors.push_back(
            mojom::XRAnchorData::New(anchor_id, std::nullopt));
        break;
      }
      case XR_SPATIAL_ENTITY_TRACKING_STATE_STOPPED_EXT: {
        // Permanently stopped anchors will never become locatable again. Still
        // send up a pose for this frame, but don't cache it for future lookups
        // and remove it from the list after we're done processing.
        permanently_stopped_anchors.insert(anchor_id);
        updated_anchors.push_back(
            mojom::XRAnchorData::New(anchor_id, std::nullopt));
        break;
      }
      case XR_SPATIAL_ENTITY_TRACKING_STATE_MAX_ENUM_EXT:
        NOTREACHED();
    }
  }

  // Check that every anchor we are tracking is accounted for in the query
  // results. If we are tracking an anchor that was not returned in the query,
  // it's in an unknown state, so we should treat it as paused.
  if (cached_anchor_poses_.size() + permanently_stopped_anchors.size() !=
      anchors_.size()) {
    DVLOG(1) << __func__ << " Not all tracked anchors were updated!";
    for (const auto& [id, anchor_data] : anchors_) {
      if (!cached_anchor_poses_.contains(id) &&
          !permanently_stopped_anchors.contains(id)) {
        DVLOG(3) << __func__
                 << " Did not receive an update for: " << id.GetUnsafeValue();
        cached_anchor_poses_.emplace(id, std::nullopt);
      }
    }
  }

  for (auto const& id : permanently_stopped_anchors) {
    DetachAnchor(id);
  }

  return mojom::XRAnchorsData::New(std::move(all_anchors_ids),
                                   std::move(updated_anchors));
}

}  // namespace device
