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

#include "components/viz/common/hit_test/hit_test_query.h"

#include <sstream>
#include <string>
#include <string_view>
#include <utility>

#include "base/containers/stack.h"
#include "base/strings/string_util.h"
#include "components/viz/common/features.h"
#include "components/viz/common/hit_test/hit_test_region_list.h"
#include "ui/gfx/geometry/point_conversions.h"
#include "ui/gfx/geometry/rect_f.h"

namespace viz {
namespace {

// If we want to add new source type here, consider switching to use
// ui::EventPointerType instead of EventSource.
bool RegionMatchEventSource(EventSource event_source, uint32_t flags) {
  if (event_source == EventSource::TOUCH)
    return (flags & HitTestRegionFlags::kHitTestTouch) != 0u;
  if (event_source == EventSource::MOUSE)
    return (flags & HitTestRegionFlags::kHitTestMouse) != 0u;
  return (flags & (HitTestRegionFlags::kHitTestMouse |
                   HitTestRegionFlags::kHitTestTouch)) != 0u;
}

bool CheckChildCount(int32_t child_count, size_t child_count_max) {
  return (child_count >= 0) &&
         (static_cast<size_t>(child_count) < child_count_max);
}

const std::string GetFlagNames(uint32_t flag) {
  std::vector<std::string_view> names;

  if (flag & kHitTestMine)
    names.emplace_back("Mine");
  if (flag & kHitTestIgnore)
    names.emplace_back("Ignore");
  if (flag & kHitTestChildSurface)
    names.emplace_back("ChildSurface");
  if (flag & kHitTestAsk)
    names.emplace_back("Ask");
  if (flag & kHitTestMouse)
    names.emplace_back("Mouse");
  if (flag & kHitTestTouch)
    names.emplace_back("Touch");
  if (flag & kHitTestNotActive)
    names.emplace_back("NotActive");

  return base::JoinString(std::move(names), ", ");
}

const std::string GetAsyncHitTestReasons(uint32_t async_hit_test_reasons) {
  std::vector<std::string_view> reasons;

  if (async_hit_test_reasons & kOverlappedRegion)
    reasons.emplace_back("OverlappedRegion");
  if (async_hit_test_reasons & kIrregularClip)
    reasons.emplace_back("IrregularClip");
  if (async_hit_test_reasons & kRegionNotActive)
    reasons.emplace_back("RegionNotActive");

  return reasons.empty() ? "None" : base::JoinString(std::move(reasons), ", ");
}

}  // namespace

HitTestQuery::~HitTestQuery() = default;

HitTestQuery::HitTestQuery(std::optional<base::SafeRef<DataProvider>> provider)
    : provider_(provider) {}

void HitTestQuery::OnAggregatedHitTestRegionListUpdated(
    const std::vector<AggregatedHitTestRegion>& hit_test_data) {
  CHECK(!provider_.has_value());
  hit_test_data_.clear();
  hit_test_data_ = hit_test_data;
}

Target HitTestQuery::FindTargetForLocation(
    EventSource event_source,
    const gfx::PointF& location_in_root) const {
  if (GetHitTestRegionData().empty()) {
    return Target();
  }
  return FindTargetForLocationStartingFromImpl(
      event_source, location_in_root, GetHitTestRegionData()[0].frame_sink_id,
      /* is_location_relative_to_parent */ true);
}

Target HitTestQuery::FindTargetForLocationStartingFrom(
    EventSource event_source,
    const gfx::PointF& location,
    const FrameSinkId& frame_sink_id) const {
  return FindTargetForLocationStartingFromImpl(
      event_source, location, frame_sink_id,
      /* is_location_relative_to_parent */ false);
}

bool HitTestQuery::TransformLocationForTarget(
    const std::vector<FrameSinkId>& target_ancestors,
    const gfx::PointF& location_in_root,
    gfx::PointF* transformed_location) const {
  if (GetHitTestRegionData().empty()) {
    return false;
  }

  // Use GetTransformToTarget if |target_ancestors| only has the target.
  if (target_ancestors.size() == 1u) {
    gfx::Transform transform;
    if (!GetTransformToTarget(target_ancestors.front(), &transform))
      return false;

    *transformed_location = transform.MapPoint(location_in_root);
    return true;
  }

  if (target_ancestors.size() == 0u ||
      target_ancestors[target_ancestors.size() - 1] !=
          GetHitTestRegionData()[0].frame_sink_id) {
    return false;
  }

  // TODO(crbug.com/41460941): Cache the matrix product such that the transform
  // can be done immediately.
  *transformed_location = location_in_root;
  return TransformLocationForTargetRecursively(
      target_ancestors, target_ancestors.size() - 1, 0, transformed_location);
}

bool HitTestQuery::GetTransformToTarget(const FrameSinkId& target,
                                        gfx::Transform* transform) const {
  if (GetHitTestRegionData().empty()) {
    return false;
  }

  return GetTransformToTargetRecursively(target, 0, transform);
}

bool HitTestQuery::ContainsActiveFrameSinkId(
    const FrameSinkId& frame_sink_id) const {
  for (auto& it : GetHitTestRegionData()) {
    if (it.frame_sink_id == frame_sink_id &&
        !(it.flags & HitTestRegionFlags::kHitTestNotActive)) {
      return true;
    }
  }
  return false;
}

Target HitTestQuery::FindTargetForLocationStartingFromImpl(
    EventSource event_source,
    const gfx::PointF& location,
    const FrameSinkId& frame_sink_id,
    bool is_location_relative_to_parent) const {
  if (GetHitTestRegionData().empty()) {
    return Target();
  }

  Target target;
  size_t start_index = 0;
  if (!FindIndexOfFrameSink(frame_sink_id, &start_index))
    return Target();

  FindTargetInRegionForLocation(event_source, location, start_index,
                                is_location_relative_to_parent, frame_sink_id,
                                &target);
  return target;
}

bool HitTestQuery::FindTargetInRegionForLocation(
    EventSource event_source,
    const gfx::PointF& location,
    size_t region_index,
    bool is_location_relative_to_parent,
    const FrameSinkId& root_view_frame_sink_id,
    Target* target) const {
  gfx::PointF location_transformed(location);

  // Exclude a region and all its descendants if the region has the ignore bit
  // set.
  if (GetHitTestRegionData()[region_index].flags &
      HitTestRegionFlags::kHitTestIgnore) {
    return false;
  }

  if (is_location_relative_to_parent) {
    // HasPerspective() is checked for the transform because the point will not
    // be transformed correctly for a plane with a different normal.
    // See https://crbug.com/854247.
    if (GetHitTestRegionData()[region_index].transform.HasPerspective()) {
      target->frame_sink_id =
          GetHitTestRegionData()[region_index].frame_sink_id;
      target->location_in_target = gfx::PointF();
      target->flags = HitTestRegionFlags::kHitTestAsk;
      return true;
    }

    location_transformed =
        GetHitTestRegionData()[region_index].transform.MapPoint(
            location_transformed);
    // TODO(crbug.com/40572334): Replace with RRectF::Contains(PointF&) when the
    // Skia dependency (https://skia-review.googlesource.com/c/skia/+/1286236)
    // is available.
    if (!GetHitTestRegionData()[region_index].rect.rect().Contains(
            location_transformed)) {
      return false;
    }
  }

  const int32_t region_child_count =
      GetHitTestRegionData()[region_index].child_count;
  if (!CheckChildCount(region_child_count,
                       GetHitTestRegionData().size() - region_index)) {
    return false;
  }

  size_t child_region = region_index + 1;
  size_t child_region_end = child_region + region_child_count;
  gfx::PointF location_in_target = location_transformed;

  const uint32_t flags = GetHitTestRegionData()[region_index].flags;

  DCHECK(GetHitTestRegionData()[region_index].frame_sink_id.is_valid());
  // Root view should not be overlapped by others. However, the root view
  // information is not visible to LTHI::BuildHitTestData(). Therefore the
  // kOverlappedRegion flag could still be set for the root view upon building
  // hit test data, e.g. overlapped by ShelfApp on ChromeOS.
  // The kHitTestAsk flag should be ignored in such a case because there is no
  // need to do async hit testing on the root merely because it was overlapped.
  // TODO(crbug.com/40646023): Do not set the kHitTestAsk and kOverlappedRegion
  // flags for root when building hit test data.
  bool root_view_overlapped =
      GetHitTestRegionData()[region_index].frame_sink_id ==
          root_view_frame_sink_id &&
      GetHitTestRegionData()[region_index].async_hit_test_reasons ==
          AsyncHitTestReasons::kOverlappedRegion;

  // Verify that async_hit_test_reasons is set if and only if there's
  // a kHitTestAsk flag.
  DCHECK_EQ(!!(flags & HitTestRegionFlags::kHitTestAsk),
            !!GetHitTestRegionData()[region_index].async_hit_test_reasons);

  if ((flags & HitTestRegionFlags::kHitTestAsk) &&
      !(flags & HitTestRegionFlags::kHitTestIgnore) && !root_view_overlapped) {
    target->frame_sink_id = GetHitTestRegionData()[region_index].frame_sink_id;
    target->location_in_target = location_in_target;
    target->flags = flags;
    return true;
  }

  // If the current region is not the root view, neither is its children.
  // Therefore when recursively calling FindTargetInRegionForLocation, pass an
  // invalid frame sink id as "root".
  while (child_region < child_region_end) {
    if (FindTargetInRegionForLocation(
            event_source, location_in_target, child_region,
            /*is_location_relative_to_parent=*/true, FrameSinkId(), target)) {
      return true;
    }

    const int32_t child_region_child_count =
        GetHitTestRegionData()[child_region].child_count;
    if (!CheckChildCount(child_region_child_count, region_child_count))
      return false;

    child_region = child_region + child_region_child_count + 1;
  }

  if (!RegionMatchEventSource(event_source, flags))
    return false;

  if ((flags & HitTestRegionFlags::kHitTestMine) &&
      !(flags & HitTestRegionFlags::kHitTestIgnore)) {
    target->frame_sink_id = GetHitTestRegionData()[region_index].frame_sink_id;
    target->location_in_target = location_in_target;
    uint32_t target_flags = flags;
    if (root_view_overlapped) {
      DCHECK_EQ(GetHitTestRegionData()[region_index].async_hit_test_reasons,
                AsyncHitTestReasons::kOverlappedRegion);
      target_flags &= ~HitTestRegionFlags::kHitTestAsk;
    }
    target->flags = target_flags;
    // We record fast path hit testing instances with reason kNotAsyncHitTest.
    return true;
  }
  return false;
}

bool HitTestQuery::TransformLocationForTargetRecursively(
    const std::vector<FrameSinkId>& target_ancestors,
    size_t target_ancestor,
    size_t region_index,
    gfx::PointF* location_in_target) const {
  *location_in_target = GetHitTestRegionData()[region_index].transform.MapPoint(
      *location_in_target);
  if (!target_ancestor)
    return true;

  const int32_t region_child_count =
      GetHitTestRegionData()[region_index].child_count;
  if (!CheckChildCount(region_child_count,
                       GetHitTestRegionData().size() - region_index)) {
    return false;
  }

  size_t child_region = region_index + 1;
  size_t child_region_end = child_region + region_child_count;
  while (child_region < child_region_end) {
    if (GetHitTestRegionData()[child_region].frame_sink_id ==
        target_ancestors[target_ancestor - 1]) {
      return TransformLocationForTargetRecursively(
          target_ancestors, target_ancestor - 1, child_region,
          location_in_target);
    }

    const int32_t child_region_child_count =
        GetHitTestRegionData()[child_region].child_count;
    if (!CheckChildCount(child_region_child_count, region_child_count))
      return false;

    child_region = child_region + child_region_child_count + 1;
  }

  return false;
}

bool HitTestQuery::GetTransformToTargetRecursively(
    const FrameSinkId& target,
    size_t region_index,
    gfx::Transform* transform) const {
  // TODO(crbug.com/41460941): Cache the matrix product such that the transform
  // can be found immediately.
  if (GetHitTestRegionData()[region_index].frame_sink_id == target) {
    *transform = GetHitTestRegionData()[region_index].transform;
    return true;
  }

  const int32_t region_child_count =
      GetHitTestRegionData()[region_index].child_count;
  if (!CheckChildCount(region_child_count,
                       GetHitTestRegionData().size() - region_index)) {
    return false;
  }

  size_t child_region = region_index + 1;
  size_t child_region_end = child_region + region_child_count;
  while (child_region < child_region_end) {
    gfx::Transform transform_to_child;
    if (GetTransformToTargetRecursively(target, child_region,
                                        &transform_to_child)) {
      gfx::Transform region_transform(
          GetHitTestRegionData()[region_index].transform);
      *transform = transform_to_child * region_transform;
      return true;
    }

    const int32_t child_region_child_count =
        GetHitTestRegionData()[child_region].child_count;
    if (!CheckChildCount(child_region_child_count, region_child_count))
      return false;

    child_region = child_region + child_region_child_count + 1;
  }

  return false;
}

const std::vector<AggregatedHitTestRegion>& HitTestQuery::GetHitTestRegionData()
    const {
  // |provider_| is not null when input is being handled on VizCompositor
  // thread.
  if (provider_.has_value()) {
    return provider_.value()->GetHitTestData();
  } else {
    return hit_test_data_;
  }
}

std::string HitTestQuery::PrintHitTestData() const {
  std::ostringstream oss;
  base::stack<uint32_t> parents;
  std::string tabs = "";

  for (uint32_t i = 0; i < GetHitTestRegionData().size(); ++i) {
    const AggregatedHitTestRegion& htr = GetHitTestRegionData()[i];

    oss << tabs << "Index: " << i << '\n';
    oss << tabs << "Children: " << htr.child_count << '\n';
    oss << tabs << "Flags: " << GetFlagNames(htr.flags) << '\n';
    oss << tabs << "AsyncHitTestReasons: "
        << GetAsyncHitTestReasons(htr.async_hit_test_reasons) << '\n';
    oss << tabs << "Frame Sink Id: " << htr.frame_sink_id.ToString() << '\n';
    oss << tabs << "Rect: " << htr.rect.ToString() << '\n';
    oss << tabs << "Transform:" << '\n';

    // gfx::Transform::ToString spans multiple lines, so we use an additional
    // stringstream.
    {
      std::string s;
      std::stringstream transform_ss;

      transform_ss << htr.transform.ToString() << '\n';

      while (getline(transform_ss, s)) {
        oss << tabs << s << '\n';
      }
    }

    tabs += "\t\t";
    parents.push(i);

    while (!parents.empty() &&
           parents.top() + GetHitTestRegionData()[parents.top()].child_count <=
               i) {
      tabs.pop_back();
      tabs.pop_back();

      parents.pop();
    }
  }

  return oss.str();
}

bool HitTestQuery::FindIndexOfFrameSink(const FrameSinkId& id,
                                        size_t* index) const {
  for (uint32_t i = 0; i < GetHitTestRegionData().size(); ++i) {
    if (GetHitTestRegionData()[i].frame_sink_id == id) {
      *index = i;
      return true;
    }
  }
  return false;
}

}  // namespace viz
