// Copyright 2024 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/user_education/views/help_bubble_event_relay.h"

#include "base/functional/bind.h"
#include "components/user_education/views/help_bubble_view.h"
#include "ui/events/event.h"
#include "ui/events/types/event_type.h"
#include "ui/gfx/geometry/point.h"
#include "ui/views/animation/ink_drop.h"
#include "ui/views/controls/button/button.h"
#include "ui/views/controls/button/button_controller.h"
#include "ui/views/controls/button/md_text_button.h"
#include "ui/views/view.h"
#include "ui/views/widget/widget.h"

namespace user_education {

namespace {

bool IsInButton(const gfx::Point& screen_coords, const views::Button* button) {
  return button && button->HitTestPoint(views::View::ConvertPointFromScreen(
                       button, screen_coords));
}

}  // namespace

HelpBubbleEventRelay::HelpBubbleEventRelay() = default;
HelpBubbleEventRelay::~HelpBubbleEventRelay() = default;

void HelpBubbleEventRelay::Init(HelpBubbleView* help_bubble) {
  CHECK(!help_bubble_);
  help_bubble_ = help_bubble;
}

views::Button* HelpBubbleEventRelay::GetButtonAt(
    const gfx::Point& screen_coords) const {
  if (!help_bubble_) {
    return nullptr;
  }
  if (IsInButton(screen_coords, help_bubble_->close_button_)) {
    return help_bubble_->close_button_;
  }
  if (IsInButton(screen_coords, help_bubble_->default_button_)) {
    return help_bubble_->default_button_;
  }
  for (views::MdTextButton* const button : help_bubble_->non_default_buttons_) {
    if (IsInButton(screen_coords, button)) {
      return button;
    }
  }
  return nullptr;
}

bool HelpBubbleEventRelay::OnEvent(const ui::LocatedEvent& event,
                                   const gfx::Point& screen_coords) {
  if (!help_bubble_) {
    return false;
  }

  const views::Widget* const widget = help_bubble_->GetWidget();
  if (!widget || !widget->GetWindowBoundsInScreen().Contains(screen_coords)) {
    return false;
  }

  views::Button* const target_button = GetButtonAt(screen_coords);
  const gfx::Point target_point =
      target_button
          ? views::View::ConvertPointFromScreen(target_button, screen_coords)
          : gfx::Point();

  switch (event.type()) {
    // Pass mouse events on to the button as normal.
    case ui::EventType::kMousePressed:
      if (target_button) {
        const auto weak_this = weak_ptr_factory_.GetWeakPtr();
        auto* const mouse_event = event.AsMouseEvent();
        target_button->OnMousePressed(ui::MouseEvent(
            ui::EventType::kMousePressed, gfx::PointF(target_point),
            gfx::PointF(screen_coords), mouse_event->time_stamp(),
            mouse_event->flags(), mouse_event->changed_button_flags()));
        if (weak_this) {
          weak_this->sent_click_ = true;
        }
      }
      break;
    case ui::EventType::kMouseReleased:
      if (target_button) {
        auto* const mouse_event = event.AsMouseEvent();
        target_button->OnMouseReleased(ui::MouseEvent(
            ui::EventType::kMouseReleased, gfx::PointF(target_point),
            gfx::PointF(screen_coords), mouse_event->time_stamp(),
            mouse_event->flags(), mouse_event->changed_button_flags()));
      }
      break;

    // Touch events are not processed directly by Views; they are typically
    // converted to something else. So, convert them to mouse clicks for the
    // purpose of pressing buttons.
    case ui::EventType::kTouchPressed:
      if (target_button) {
        const auto weak_this = weak_ptr_factory_.GetWeakPtr();
        auto* const touch_event = event.AsTouchEvent();
        target_button->OnMousePressed(ui::MouseEvent(
            ui::EventType::kMousePressed, gfx::PointF(target_point),
            gfx::PointF(screen_coords), touch_event->time_stamp(),
            touch_event->flags() | ui::EF_LEFT_MOUSE_BUTTON | ui::EF_FROM_TOUCH,
            ui::EF_LEFT_MOUSE_BUTTON));
        if (weak_this) {
          weak_this->sent_click_ = true;
        }
      }
      break;
    case ui::EventType::kTouchReleased:
      if (target_button) {
        auto* const touch_event = event.AsTouchEvent();
        target_button->OnMouseReleased(ui::MouseEvent(
            ui::EventType::kMouseReleased, gfx::PointF(target_point),
            gfx::PointF(screen_coords), touch_event->time_stamp(),
            touch_event->flags() | ui::EF_LEFT_MOUSE_BUTTON | ui::EF_FROM_TOUCH,
            ui::EF_LEFT_MOUSE_BUTTON));
      }
      break;

    // If a gesture is received, forward it as-is.
    case ui::EventType::kGestureTap:
      if (target_button) {
        const auto weak_this = weak_ptr_factory_.GetWeakPtr();
        auto* const gesture = event.AsGestureEvent();
        ui::GestureEvent tap(gesture->x(), gesture->y(), gesture->flags(),
                             gesture->time_stamp(), gesture->details(),
                             gesture->unique_touch_event_id());
        target_button->button_controller()->OnGestureEvent(&tap);
        if (weak_this) {
          weak_this->sent_click_ = true;
        }
      }
      break;

    // Mouse moves could be routed through the inkdrop controller but it's
    // easier to just set hovered state directly.
    case ui::EventType::kMouseMoved:
      if (target_button != hovered_button_) {
        if (hovered_button_) {
          views::InkDrop* const ink_drop =
              views::InkDrop::Get(hovered_button_)->GetInkDrop();
          if (ink_drop) {
            ink_drop->SetHovered(false);
          }
        }
        if (target_button) {
          views::InkDrop* const ink_drop =
              views::InkDrop::Get(target_button)->GetInkDrop();
          if (ink_drop) {
            ink_drop->SetHovered(true);
          }
        }
        hovered_button_ = target_button;
      }
      break;

    // For cases where the help bubble overlaps the target area, the mouse
    // exiting should result in un-hovering the current button.
    case ui::EventType::kMouseExited:
      if (ShouldUnHoverOnMouseExit() && hovered_button_) {
        if (views::InkDrop* const ink_drop =
                views::InkDrop::Get(hovered_button_)->GetInkDrop()) {
          ink_drop->SetHovered(false);
        }
        hovered_button_ = nullptr;
      }
      return false;

    default:
      return false;
  }

  return true;
}

void HelpBubbleEventRelay::OnConnectionLost() {
  if (!sent_click_ && help_bubble_ && help_bubble_->GetWidget() &&
      !help_bubble_->GetWidget()->IsClosed()) {
    help_bubble_->GetWidget()->CloseWithReason(
        views::Widget::ClosedReason::kLostFocus);
  }
}

namespace internal {

MenuHelpBubbleEventProcessor::MenuHelpBubbleEventProcessor(
    views::MenuItemView* menu_item)
    : callback_handle_(menu_item->GetMenuController()->AddAnnotationCallback(
          base::BindRepeating(&MenuHelpBubbleEventProcessor::OnEvent,
                              base::Unretained(this)))) {}

MenuHelpBubbleEventProcessor::~MenuHelpBubbleEventProcessor() = default;

bool MenuHelpBubbleEventProcessor::OnEvent(const ui::LocatedEvent& event) {
  return HelpBubbleEventRelay::OnEvent(event, event.root_location());
}

bool MenuHelpBubbleEventProcessor::ShouldHelpBubbleProcessEvents() const {
  return true;
}

bool MenuHelpBubbleEventProcessor::ShouldUnHoverOnMouseExit() const {
  return false;
}

}  // namespace internal

}  // namespace user_education
