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

#ifndef CHROME_BROWSER_UI_VIEWS_AUTOFILL_POPUP_POPUP_ROW_VIEW_H_
#define CHROME_BROWSER_UI_VIEWS_AUTOFILL_POPUP_POPUP_ROW_VIEW_H_

#include <memory>
#include <optional>

#include "base/memory/raw_ptr.h"
#include "base/memory/raw_ref.h"
#include "base/memory/weak_ptr.h"
#include "base/scoped_observation.h"
#include "chrome/browser/ui/autofill/next_idle_barrier.h"
#include "chrome/browser/ui/views/autofill/popup/popup_interactive_row_view.h"
#include "chrome/browser/ui/views/autofill/popup/popup_view_utils.h"
#include "components/autofill/core/browser/metrics/autofill_metrics.h"
#include "components/input/native_web_keyboard_event.h"
#include "ui/base/metadata/metadata_header_macros.h"
#include "ui/events/event_handler.h"
#include "ui/gfx/geometry/rect_f.h"
#include "ui/views/view.h"
#include "ui/views/view_observer.h"

namespace autofill {

class AutofillPopupController;
class PopupRowContentView;

// `PopupRowView` represents a single selectable popup row. It contains logic
// common to all row types (like selection callbacks or a11y) but it is not
// responsible for the view layout. It expects a `PopupRowContentView` instead.
// It also supports the expanding control depending on whether the suggestion
// has children or not (see `Suggestion::children`).
class PopupRowView : public PopupInteractiveRowView,
                     public views::ViewObserver {
  METADATA_HEADER(PopupRowView, PopupInteractiveRowView)
 public:

  // Interface used to announce changes in selected cells to accessibility
  // frameworks.
  class AccessibilitySelectionDelegate {
   public:
    virtual ~AccessibilitySelectionDelegate() = default;

    // Notify accessibility that an item represented by `view` has been
    // selected.
    virtual void NotifyAXSelection(views::View& view) = 0;
  };

  // Interface used to keep track of cell selection. This may be needed if the
  // parent needs to keep state of which row is currently in order to process
  // keyboard events.
  class SelectionDelegate {
   public:
    using CellIndex = std::pair<size_t, PopupRowView::CellType>;

    virtual ~SelectionDelegate() = default;

    virtual std::optional<CellIndex> GetSelectedCell() const = 0;
    virtual void SetSelectedCell(std::optional<CellIndex> cell_index,
                                 PopupCellSelectionSource source) = 0;
  };

  // Returns the margin on the left and right of the row. When hovering in the
  // content cell, this is the distance one sees between the updated
  // background and the edge of the row.
  static int GetHorizontalMargin();

  PopupRowView(AccessibilitySelectionDelegate& a11y_selection_delegate,
               SelectionDelegate& selection_delegate,
               base::WeakPtr<AutofillPopupController> controller,
               int line_number,
               std::unique_ptr<PopupRowContentView> content_view);
  PopupRowView(const PopupRowView&) = delete;
  PopupRowView& operator=(const PopupRowView&) = delete;
  ~PopupRowView() override;

  // views::View:
  bool OnMouseDragged(const ui::MouseEvent& event) override;
  bool OnMousePressed(const ui::MouseEvent& event) override;
  void OnMouseExited(const ui::MouseEvent& event) override;
  void OnMouseReleased(const ui::MouseEvent& event) override;
  void OnGestureEvent(ui::GestureEvent* event) override;
  void OnPaint(gfx::Canvas* canvas) override;
  bool GetNeedsNotificationWhenVisibleBoundsChange() const override;
  void OnVisibleBoundsChanged() override;

  // views::ViewObserver:
  void OnViewFocused(views::View* focused_now) override;

  // PopupInteractiveRowView:
  std::optional<CellType> GetSelectedCell() const override;
  void SetSelectedCell(std::optional<CellType> cell) override;
  bool HandleKeyPressEvent(const input::NativeWebKeyboardEvent& event) override;
  bool IsSelectable() const override;

  // Sets whether the row's child suggestions are displayed in a sub-popup.
  // Note that the row doesn't control the sub-popup, but rather should be
  // synced with it by this method.
  void SetChildSuggestionsDisplayed(bool child_suggestions_displayed);

  // Returns the control cell's bounds. The cell must be present.
  gfx::RectF GetControlCellBounds() const;

  // Accepts the suggestion that corresponds to this view's line number.
  bool Accept(AutofillMetrics::SuggestionAcceptedMethod method) const;

  // Returns the view representing the content area of the row.
  PopupRowContentView& GetContentView() { return *content_view_; }

  // Returns the view representing the suggestions expanding control of the row.
  views::View* GetExpandChildSuggestionsView() {
    return expand_child_suggestions_view_.get();
  }

 protected:
  base::WeakPtr<AutofillPopupController> controller() { return controller_; }

  int line_number() const { return line_number_; }

  AccessibilitySelectionDelegate& GetA11ySelectionDelegate() {
    return a11y_selection_delegate_.get();
  }

  // Calls `selection_delegate_` when an event leading to selection is
  // triggered on the view, e.g. `ui::EventType::kMouseEntered` or the root
  // view is focused. `type` == `std::nullopt` means deselection.
  virtual void OnCellSelected(std::optional<CellType> type,
                              PopupCellSelectionSource source);

 private:
  // Updates all UI parts that may have changed based on the current state,
  // for now they are the background and expand control visibility.
  void UpdateUI();

  // Updates the background according to the control cell highlighting state.
  void UpdateBackground();

  // This method is just a getter for the `barrier_for_accepting_` which is
  // set `true` when the view's visible part is big enough and was present on
  // the screen long enough, see `OnVisibleBoundsChanged()` implementation for
  // what these "enough"s mean.
  bool IsViewVisibleEnough() const;

  // The delegate used for accessibility announcements (implemented by the
  // parent view).
  const raw_ref<AccessibilitySelectionDelegate> a11y_selection_delegate_;
  // The delegate used for selection control (implemented by the parent view).
  const raw_ref<SelectionDelegate> selection_delegate_;
  // The controller for the parent view.
  const base::WeakPtr<AutofillPopupController> controller_;
  // The position of the row in the vertical list of suggestions.
  const int line_number_;

  // Which (if any) cell of this row is currently selected.
  std::optional<CellType> selected_cell_;

  // The view wrapping the content area of the row.
  raw_ptr<PopupRowContentView> content_view_ = nullptr;
  base::ScopedObservation<PopupRowContentView, views::ViewObserver>
      content_view_observer_{this};
  // The view wrapping the control area of the row.
  raw_ptr<views::View> expand_child_suggestions_view_ = nullptr;
  base::ScopedObservation<views::View, views::ViewObserver>
      expand_child_suggestions_view_observer_{this};

  // Overriding event handles for the content and control views.
  std::unique_ptr<ui::EventHandler> content_event_handler_;
  std::unique_ptr<ui::EventHandler> control_event_handler_;

  // We want a mouse click to accept a suggestion only if the user has made an
  // explicit choice. Therefore, we shall ignore mouse clicks unless the mouse
  // has been moved into the item's screen bounds. For example, if the item is
  // hovered by the mouse at the time it's first shown, we want to ignore clicks
  // until the mouse has left and re-entered the bounds of the item
  // (crbug.com/40056900, crbug.com/40056936, crbug.com/40058496).
  // This is particularly relevant because mouse click interactions may be
  // processed with a delay, making it seem as if the two click interactions of
  // a double click were executed at intervals larger than the threshold (500ms)
  // checked in the controller (crbug.com/40063230).
  bool mouse_observed_outside_item_bounds_ = false;

  // Whether `mouse_observed_outside_item_bounds_` should be ignored for this
  // row. The policy is derived from the controller-level popup trigger and the
  // row's suggestion type.
  const bool should_ignore_mouse_observed_outside_item_bounds_check_;

  // Whether the row's child suggestions (see `Suggestion::children`) are
  // displayed in a sub-popup.
  bool child_suggestions_displayed_ = false;

  // This is used to protected users from accepting suggestions too quickly.
  // This is often used in various attacks against their data when the user is
  // tricked to press a key combination or click a specific place on the screen
  // (e.g. in a game). Having a delay gives the user a chance to notice/overview
  // what they are about to expose to the website.
  // The timer starts in `OnVisibleBoundsChanged()` only when the view is
  // visible enough and checked before triggering acceptance on the controller.
  std::optional<NextIdleBarrier> barrier_for_accepting_;

  // Has the same value as `Suggestion::is_acceptable` of the underlying
  // suggestion. If `false` the content part is not highlighted separately,
  // but the whole row is highlighted instead as for the control view.
  const bool suggestion_is_acceptable_;
};

}  // namespace autofill

#endif  // CHROME_BROWSER_UI_VIEWS_AUTOFILL_POPUP_POPUP_ROW_VIEW_H_
