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

#include <algorithm>

#include "device/vr/openxr/openxr_api_wrapper.h"
#include "device/vr/openxr/openxr_extension_helper.h"
#include "device/vr/openxr/openxr_spatial_framework_manager.h"
#include "device/vr/openxr/openxr_spatial_utils.h"
#include "device/vr/openxr/openxr_util.h"
#include "ui/gfx/geometry/transform.h"

namespace device {

namespace {
mojom::XRPlaneOrientation ToMojomPlaneOrientation(
    const XrSpatialPlaneAlignmentEXT& alignment) {
  switch (alignment) {
    case XR_SPATIAL_PLANE_ALIGNMENT_HORIZONTAL_UPWARD_EXT:
    case XR_SPATIAL_PLANE_ALIGNMENT_HORIZONTAL_DOWNWARD_EXT:
      return mojom::XRPlaneOrientation::HORIZONTAL;
    case XR_SPATIAL_PLANE_ALIGNMENT_VERTICAL_EXT:
      return mojom::XRPlaneOrientation::VERTICAL;
    default:
      return mojom::XRPlaneOrientation::UNKNOWN;
  }
}

std::vector<XrSpatialComponentTypeEXT> GetAttachableComponentTypes(
    const OpenXrExtensionMethods& extension_methods,
    XrInstance instance,
    XrSystemId system) {
  // This is an optional extension.
  if (extension_methods.xrEnumerateSpatialAnchorAttachableComponentsANDROID ==
      nullptr) {
    return {};
  }

  uint32_t attachable_component_count;
  if (XR_FAILED(
          extension_methods.xrEnumerateSpatialAnchorAttachableComponentsANDROID(
              instance, system, 0, &attachable_component_count, nullptr))) {
    return {};
  }

  std::vector<XrSpatialComponentTypeEXT> attachable_components(
      attachable_component_count);
  if (XR_FAILED(
          extension_methods.xrEnumerateSpatialAnchorAttachableComponentsANDROID(
              instance, system,
              static_cast<uint32_t>(attachable_components.size()),
              &attachable_component_count, attachable_components.data()))) {
    attachable_components.clear();
  }

  return attachable_components;
}
}  // namespace

// static
bool OpenXrSpatialPlaneManager::IsSupported(
    const std::vector<XrSpatialCapabilityEXT>& capabilities) {
  // The only components that we need to support planes are
  // XR_SPATIAL_COMPONENT_TYPE_BOUNDED_2D_EXT and
  // XR_SPATIAL_COMPONENT_TYPE_PLANE_ALIGNMENT_EXT which are guaranteed to be
  // supported if the XR_SPATIAL_CAPABILITY_PLANE_TRACKING_EXT is supported, so
  //  that's all we need to check.
  return std::ranges::contains(capabilities,
                               XR_SPATIAL_CAPABILITY_PLANE_TRACKING_EXT);
}

OpenXrSpatialPlaneManager::OpenXrSpatialPlaneManager(
    XrSpace mojo_space,
    const OpenXrExtensionHelper& extension_helper,
    const OpenXrSpatialFrameworkManager& framework_manager,
    XrInstance instance,
    XrSystemId system,
    bool mesh_detection_enabled)
    : mojo_space_(mojo_space),
      extension_helper_(extension_helper),
      framework_manager_(framework_manager),
      mesh_detection_enabled_(mesh_detection_enabled),
      enabled_components_({XR_SPATIAL_COMPONENT_TYPE_BOUNDED_2D_EXT,
                           XR_SPATIAL_COMPONENT_TYPE_PLANE_ALIGNMENT_EXT}) {
  std::vector<XrSpatialComponentTypeEXT> plane_tracking_components =
      GetSupportedComponentTypes(
          extension_helper_->ExtensionMethods()
              .xrEnumerateSpatialCapabilityComponentTypesEXT,
          instance, system, XR_SPATIAL_CAPABILITY_PLANE_TRACKING_EXT);

  polygon_enabled_ = std::ranges::contains(
      plane_tracking_components, XR_SPATIAL_COMPONENT_TYPE_POLYGON_2D_EXT);
  if (polygon_enabled_) {
    enabled_components_.insert(XR_SPATIAL_COMPONENT_TYPE_POLYGON_2D_EXT);
  }

  semantic_label_enabled_ =
      std::ranges::contains(plane_tracking_components,
                            XR_SPATIAL_COMPONENT_TYPE_PLANE_SEMANTIC_LABEL_EXT);
  if (semantic_label_enabled_) {
    enabled_components_.insert(
        XR_SPATIAL_COMPONENT_TYPE_PLANE_SEMANTIC_LABEL_EXT);
  }

  if (mesh_detection_enabled_) {
    bool mesh2d_supported = std::ranges::contains(
        plane_tracking_components, XR_SPATIAL_COMPONENT_TYPE_MESH_2D_EXT);
    if (mesh2d_supported) {
      enabled_components_.insert(XR_SPATIAL_COMPONENT_TYPE_MESH_2D_EXT);
      DVLOG(1) << __func__ << ": Mesh2D component is SUPPORTED and will "
               << "be enabled under PLANE_TRACKING.";
    } else {
      DVLOG(1) << __func__ << ": Mesh2D component is NOT supported for "
               << "PLANE_TRACKING on this runtime.";
    }
  }

  std::vector<XrSpatialComponentTypeEXT> attachable_components =
      GetAttachableComponentTypes(extension_helper_->ExtensionMethods(),
                                  instance, system);

  // If any of our attachable components are in the supportable plane components
  // list, we can attach anchors to planes.
  auto first_attachable_component = std::find_if(
      attachable_components.begin(), attachable_components.end(),
      [&plane_tracking_components](XrSpatialComponentTypeEXT component) {
        return std::ranges::contains(plane_tracking_components, component);
      });

  if (first_attachable_component != attachable_components.end()) {
    // In order to properly support parenting, we have to ensure the component
    // of ours that is attachable is enabled. First check if any of our planned
    // to be enabled components are attachable.
    bool attachable_component_enabled = std::any_of(
        enabled_components_.begin(), enabled_components_.end(),
        [&attachable_components](XrSpatialComponentTypeEXT component) {
          return std::ranges::contains(attachable_components, component);
        });

    // If not, let's enable the first attachable component that we found, since
    // any of them will do. It's only use will be to enable parenting, we don't
    // actually need to query for it.
    if (!attachable_component_enabled) {
      enabled_components_.insert(*first_attachable_component);
    }

    can_parent_anchors_ = true;
  }
}

OpenXrSpatialPlaneManager::~OpenXrSpatialPlaneManager() = default;

void OpenXrSpatialPlaneManager::PopulateCapabilityConfiguration(
    absl::flat_hash_map<XrSpatialCapabilityEXT,
                        absl::flat_hash_set<XrSpatialComponentTypeEXT>>&
        capability_configuration) const {
  capability_configuration[XR_SPATIAL_CAPABILITY_PLANE_TRACKING_EXT].insert(
      enabled_components_.begin(), enabled_components_.end());
}

void OpenXrSpatialPlaneManager::OnSnapshotChanged() {
  const XrSpatialSnapshotEXT snapshot =
      framework_manager_->GetDiscoverySnapshot();
  if (snapshot == XR_NULL_HANDLE) {
    return;
  }

  // Query the snapshot for all entities that have the necessary plane
  // components. Note that we don't use enabled_components_ here, because these
  // are the only values we actually care about.
  XrSpatialComponentDataQueryConditionEXT query_condition{
      XR_TYPE_SPATIAL_COMPONENT_DATA_QUERY_CONDITION_EXT};
  std::vector<XrSpatialComponentTypeEXT> component_types = {
      XR_SPATIAL_COMPONENT_TYPE_BOUNDED_2D_EXT,
      XR_SPATIAL_COMPONENT_TYPE_PLANE_ALIGNMENT_EXT};

  if (polygon_enabled_) {
    component_types.push_back(XR_SPATIAL_COMPONENT_TYPE_POLYGON_2D_EXT);
  }

  if (semantic_label_enabled_) {
    component_types.push_back(
        XR_SPATIAL_COMPONENT_TYPE_PLANE_SEMANTIC_LABEL_EXT);
  }

  query_condition.componentTypeCount = component_types.size();
  query_condition.componentTypes = component_types.data();

  // First need to query for how many results there are, then we can build the
  // arrays to populate.
  XrSpatialComponentDataQueryResultEXT query_result{
      XR_TYPE_SPATIAL_COMPONENT_DATA_QUERY_RESULT_EXT};
  if (XR_FAILED(
          extension_helper_->ExtensionMethods().xrQuerySpatialComponentDataEXT(
              snapshot, &query_condition, &query_result))) {
    return;
  }

  std::vector<XrSpatialEntityIdEXT> entity_ids(
      query_result.entityIdCountOutput);
  query_result.entityIdCapacityInput = entity_ids.size();
  query_result.entityIds = entity_ids.data();

  std::vector<XrSpatialEntityTrackingStateEXT> entity_states(
      query_result.entityIdCountOutput);
  query_result.entityStateCapacityInput = entity_states.size();
  query_result.entityStates = entity_states.data();

  XrNextChainBuilder next_chain(&query_result);

  std::vector<XrSpatialPlaneAlignmentEXT> plane_alignments(
      query_result.entityIdCountOutput);
  XrSpatialComponentPlaneAlignmentListEXT plane_alignment_list{
      .type = XR_TYPE_SPATIAL_COMPONENT_PLANE_ALIGNMENT_LIST_EXT,
      .planeAlignmentCount = static_cast<uint32_t>(plane_alignments.size()),
      .planeAlignments = plane_alignments.data()};
  next_chain.Add(&plane_alignment_list);

  std::vector<XrSpatialBounded2DDataEXT> bounded_2d_data(
      query_result.entityIdCountOutput);
  XrSpatialComponentBounded2DListEXT bounded_2d_list{
      .type = XR_TYPE_SPATIAL_COMPONENT_BOUNDED_2D_LIST_EXT,
      .boundCount = static_cast<uint32_t>(bounded_2d_data.size()),
      .bounds = bounded_2d_data.data()};
  next_chain.Add(&bounded_2d_list);

  std::vector<XrSpatialPolygon2DDataEXT> polygons;
  XrSpatialComponentPolygon2DListEXT polygon_list{
      XR_TYPE_SPATIAL_COMPONENT_POLYGON_2D_LIST_EXT};
  if (polygon_enabled_) {
    polygons.resize(query_result.entityIdCountOutput);
    polygon_list.polygonCount = static_cast<uint32_t>(polygons.size());
    polygon_list.polygons = polygons.data();
    next_chain.Add(&polygon_list);
  }

  std::vector<XrSpatialPlaneSemanticLabelEXT> semantic_labels;
  XrSpatialComponentPlaneSemanticLabelListEXT semantic_label_list{
      XR_TYPE_SPATIAL_COMPONENT_PLANE_SEMANTIC_LABEL_LIST_EXT};
  if (semantic_label_enabled_) {
    semantic_labels.resize(query_result.entityIdCountOutput);
    semantic_label_list.semanticLabelCount =
        static_cast<uint32_t>(semantic_labels.size());
    semantic_label_list.semanticLabels = semantic_labels.data();
    next_chain.Add(&semantic_label_list);
  }

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

  // Reset our list of updated planes. We could potentially be clearing a plane
  // that we said had a pending update but now we don't know about. Since we no
  // longer know about it, then we shouldn't be reporting it.
  updated_entity_ids_.clear();
  mesh_updated_entity_ids_.clear();
  absl::flat_hash_set<XrSpatialEntityIdEXT> paused_entity_ids;
  for (uint32_t i = 0; i < query_result.entityIdCountOutput; i++) {
    XrSpatialEntityIdEXT entity_id = entity_ids[i];
    // We don't need to send up any information about stopped planes, and since
    // planes could be subsumed, we'll just process and clear outdated entries
    // every time as well.
    // We'll note paused planes differently. They won't count as updated this
    // frame, but we'll keep them around/existing.
    if (entity_states[i] == XR_SPATIAL_ENTITY_TRACKING_STATE_PAUSED_EXT) {
      paused_entity_ids.insert(entity_id);
      continue;
    }

    if (entity_states[i] != XR_SPATIAL_ENTITY_TRACKING_STATE_TRACKING_EXT) {
      continue;
    }

    // If we don't have an entry for this entity ID, populate the data for it.
    if (!entity_id_to_data_.contains(entity_id)) {
      entity_id_to_data_[entity_id] = mojom::XRPlaneData::New();
    }

    updated_entity_ids_.insert(entity_id);
    mojom::XRPlaneDataPtr& plane_data = entity_id_to_data_[entity_id];

    // Can't use `GetPlaneId` until our entity_id is in the map.
    plane_data->id = GetPlaneId(entity_id);
    plane_data->orientation = ToMojomPlaneOrientation(plane_alignments[i]);

    if (semantic_label_enabled_) {
      plane_data->semantic_label = ToMojomSemanticLabel(semantic_labels[i]);
    }

    plane_data->polygon.clear();
    bool has_polygon = false;
    if (polygon_enabled_ && i < polygons.size()) {
      has_polygon = GetPolygonFromBuffer(snapshot, polygons[i], plane_data);
    }

    if (!has_polygon) {
      GetPolygonFromExtent(bounded_2d_data[i], plane_data);
    }

    // Synthesize mesh data from the same plane entity.
    if (mesh_detection_enabled_) {
      if (!mesh_entity_id_to_data_.contains(entity_id)) {
        mesh_entity_id_to_data_[entity_id] = mojom::XRMeshData::New();
      }

      mesh_updated_entity_ids_.insert(entity_id);
      mojom::XRMeshDataPtr& mesh_data = mesh_entity_id_to_data_[entity_id];
      mesh_data->id = GetMeshId(entity_id);
      mesh_data->semantic_label = plane_data->semantic_label;
      mesh_data->mojo_from_mesh = plane_data->mojo_from_plane;

      mesh_data->vertices.clear();
      mesh_data->indices.clear();
      const auto& polygon = plane_data->polygon;
      for (const auto& point : polygon) {
        mesh_data->vertices.push_back(point->x);
        mesh_data->vertices.push_back(point->z);
        mesh_data->vertices.push_back(0.0f);
      }
      // TODO(crbug.com/499239200): Ensure the plane polygons returned by OpenXR Runtime are guaranteed to be convex
      if (IsConvexPolygon(polygon)) {
        for (uint32_t j = 1; j + 1 < polygon.size(); ++j) {
          mesh_data->indices.push_back(0);
          mesh_data->indices.push_back(j);
          mesh_data->indices.push_back(j + 1);
        }
      } else {
        mesh_data->indices = EarClipTriangulate(polygon);
      }
    }
  }

  // Remove any planes that are no longer being tracked.
  auto it = entity_id_to_data_.begin();
  while (it != entity_id_to_data_.end()) {
    // If a plane was updated or marked as paused, keep it around. Otherwise,
    // it was either not reported or reported as stopped, so delete it.
    if (updated_entity_ids_.contains(it->first) ||
        paused_entity_ids.contains(it->first)) {
      it++;
    } else {
      entity_id_to_data_.erase(it++);
    }
  }

  if (mesh_detection_enabled_) {
    auto mesh_it = mesh_entity_id_to_data_.begin();
    while (mesh_it != mesh_entity_id_to_data_.end()) {
      if (mesh_updated_entity_ids_.contains(mesh_it->first) ||
          paused_entity_ids.contains(mesh_it->first)) {
        ++mesh_it;
      } else {
        mesh_entity_id_to_data_.erase(mesh_it++);
      }
    }
  }
}

bool OpenXrSpatialPlaneManager::GetPolygonFromBuffer(
    XrSpatialSnapshotEXT snapshot,
    const XrSpatialPolygon2DDataEXT& polygon_data,
    mojom::XRPlaneDataPtr& plane_data) const {
  // There is currently a runtime error where some runtimes may report the
  // presence of a polygon, but not actually have populated the buffer yet.
  // Before querying for the spatial buffer, ensure we have a valid buffer id.
  if (polygon_data.vertexBuffer.bufferId == XR_NULL_SPATIAL_BUFFER_ID_EXT) {
    DLOG(ERROR) << __func__
                << " Runtime reported polygon, but returned invalid buffer id";
    return false;
  }

  XrSpatialBufferGetInfoEXT buffer_info{XR_TYPE_SPATIAL_BUFFER_GET_INFO_EXT};
  buffer_info.bufferId = polygon_data.vertexBuffer.bufferId;

  uint32_t buffer_count_output = 0;
  if (XR_FAILED(
          extension_helper_->ExtensionMethods().xrGetSpatialBufferVector2fEXT(
              snapshot, &buffer_info, 0, &buffer_count_output, nullptr))) {
    return false;
  }

  std::vector<XrVector2f> vertices(buffer_count_output);
  if (XR_FAILED(
          extension_helper_->ExtensionMethods().xrGetSpatialBufferVector2fEXT(
              snapshot, &buffer_info, buffer_count_output, &buffer_count_output,
              vertices.data()))) {
    return false;
  }

  // The incoming pose has the Z axis as the normal, but WebXR expects the Y
  // axis to be the normal.
  plane_data->mojo_from_plane =
      ZNormalXrPoseToYNormalDevicePose(polygon_data.origin);

  // OpenXR provides a counterclockwise polygon that is guaranteed to not self
  // intersect; however, we do need to transform from XY space to XZ space as
  // expected by the spec.
  for (const auto& vertex : vertices) {
    // Vertices are 2D (X, Y) in the polygon's space (Z=0).
    // We need to transform them to WebXR's plane space (Y-up).
    // Construct a point for the vertex.
    gfx::Point3F vertex_point = {vertex.x, vertex.y, 0};
    // Transform from Z-normal to Y-normal.
    auto webxr_vertex_pose = ZNormalPositionToYNormalPosition(vertex_point);
    // Now that it's Y-Normal, Y should be 0, and we send up the expected XZ
    // coordinates.
    plane_data->polygon.push_back(mojom::XRPlanePointData::New(
        webxr_vertex_pose.x(), webxr_vertex_pose.z()));
  }

  return true;
}

void OpenXrSpatialPlaneManager::GetPolygonFromExtent(
    const XrSpatialBounded2DDataEXT& bounded_2d_data,
    mojom::XRPlaneDataPtr& plane_data) const {
  // The incoming pose has the Z axis as the normal, but WebXR expects the Y
  // axis to be the normal.
  plane_data->mojo_from_plane =
      ZNormalXrPoseToYNormalDevicePose(bounded_2d_data.center);
  plane_data->polygon.clear();

  // Create a rectangle from the extents with a counter-clockwise winding.
  const auto& extents = bounded_2d_data.extents;
  plane_data->polygon.push_back(
      mojom::XRPlanePointData::New(-extents.width / 2, -extents.height / 2));
  plane_data->polygon.push_back(
      mojom::XRPlanePointData::New(extents.width / 2, -extents.height / 2));
  plane_data->polygon.push_back(
      mojom::XRPlanePointData::New(extents.width / 2, extents.height / 2));
  plane_data->polygon.push_back(
      mojom::XRPlanePointData::New(-extents.width / 2, extents.height / 2));
}

mojom::XRPlaneDetectionDataPtr
OpenXrSpatialPlaneManager::GetDetectedPlanesData() {
  auto planes_data = mojom::XRPlaneDetectionData::New();

  for (const auto& [entity_id, data] : entity_id_to_data_) {
    planes_data->all_planes_ids.push_back(GetPlaneId(entity_id));
    if (updated_entity_ids_.contains(entity_id)) {
      planes_data->updated_planes_data.push_back(data.Clone());
    }
  }

  updated_entity_ids_.clear();
  return planes_data;
}

std::optional<device::Pose> OpenXrSpatialPlaneManager::TryGetMojoFromPlane(
    PlaneId plane_id) const {
  auto it = entity_id_to_data_.find(GetEntityId(plane_id));
  if (it == entity_id_to_data_.end() || !it->second->mojo_from_plane) {
    return std::nullopt;
  }

  return it->second->mojo_from_plane;
}

PlaneId OpenXrSpatialPlaneManager::GetPlaneId(
    XrSpatialEntityIdEXT entity_id) const {
  if (entity_id == XR_NULL_SPATIAL_ENTITY_ID_EXT ||
      !entity_id_to_data_.contains(entity_id)) {
    return kInvalidPlaneId;
  }

  return PlaneId(static_cast<uint64_t>(entity_id));
}

XrSpatialEntityIdEXT OpenXrSpatialPlaneManager::GetEntityId(
    PlaneId plane_id) const {
  if (plane_id == kInvalidPlaneId) {
    return XR_NULL_SPATIAL_ENTITY_ID_EXT;
  }

  auto entity_id = static_cast<XrSpatialEntityIdEXT>(plane_id.GetUnsafeValue());
  if (!entity_id_to_data_.contains(entity_id)) {
    return XR_NULL_SPATIAL_ENTITY_ID_EXT;
  }

  return entity_id;
}

std::optional<XrLocation> OpenXrSpatialPlaneManager::GetXrLocationFromPlane(
    PlaneId plane_id,
    const gfx::Transform& plane_id_from_object) const {
  // We don't have an xr_space_ for the plane, so we'll just locate the pose
  // in mojo_space_ and send that up as the base of the XrLocation.
  std::optional<device::Pose> mojo_from_plane = TryGetMojoFromPlane(plane_id);
  if (!mojo_from_plane) {
    return std::nullopt;
  }

  gfx::Transform mojo_from_new_anchor =
      mojo_from_plane->ToTransform() * plane_id_from_object;
  return XrLocation{GfxTransformToXrPose(mojo_from_new_anchor), mojo_space_};
}

mojom::XRMeshDetectionDataPtr
OpenXrSpatialPlaneManager::GetDetectedMeshesData(XrTime frame_time,
                                                  XrSpace view_space) {
  auto meshes_data = mojom::XRMeshDetectionData::New();
  for (const auto& [entity_id, data] : mesh_entity_id_to_data_) {
    meshes_data->all_meshes_ids.emplace_back(GetMeshId(entity_id));
    if (mesh_updated_entity_ids_.contains(entity_id)) {
      meshes_data->updated_meshes_data.push_back(data.Clone());
    }
  }
  mesh_updated_entity_ids_.clear();
  return meshes_data;
}

std::optional<XrLocation> OpenXrSpatialPlaneManager::GetXrLocationFromMesh(
    MeshId mesh_id,
    const gfx::Transform& mesh_id_from_object) const {
  // TODO(crbug.com/498979573): Implement mesh-based native origin lookup.
  return std::nullopt;
}

void OpenXrSpatialPlaneManager::OnReferenceSpaceChanged() {
  mesh_entity_id_to_data_.clear();
  mesh_updated_entity_ids_.clear();
}

std::optional<device::Pose> OpenXrSpatialPlaneManager::TryGetMojoFromMesh(
    MeshId mesh_id) const {
  auto it = mesh_entity_id_to_data_.find(GetMeshEntityId(mesh_id));
  if (it == mesh_entity_id_to_data_.end() || !it->second->mojo_from_mesh) {
    return std::nullopt;
  }
  return it->second->mojo_from_mesh;
}

MeshId OpenXrSpatialPlaneManager::GetMeshId(
    XrSpatialEntityIdEXT entity_id) const {
  if (entity_id == XR_NULL_SPATIAL_ENTITY_ID_EXT ||
      !mesh_entity_id_to_data_.contains(entity_id)) {
    return kInvalidMeshId;
  }
  return MeshId(static_cast<uint64_t>(entity_id));
}

XrSpatialEntityIdEXT OpenXrSpatialPlaneManager::GetMeshEntityId(
    MeshId mesh_id) const {
  if (mesh_id == kInvalidMeshId) {
    return XR_NULL_SPATIAL_ENTITY_ID_EXT;
  }
  auto entity_id = static_cast<XrSpatialEntityIdEXT>(mesh_id.GetUnsafeValue());
  if (!mesh_entity_id_to_data_.contains(entity_id)) {
    return XR_NULL_SPATIAL_ENTITY_ID_EXT;
  }
  return entity_id;
}

}  // namespace device
