// Copyright 2013 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_TOOLBAR_TOOLBAR_BUTTON_H_
#define CHROME_BROWSER_UI_VIEWS_TOOLBAR_TOOLBAR_BUTTON_H_

#include <memory>
#include <optional>

#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/raw_ptr_exclusion.h"
#include "chrome/browser/ui/color/chrome_color_id.h"
#include "chrome/browser/ui/views/chrome_views_export.h"
#include "ui/base/interaction/element_identifier.h"
#include "ui/base/metadata/metadata_header_macros.h"
#include "ui/base/models/menu_model.h"
#include "ui/base/mojom/menu_source_type.mojom-forward.h"
#include "ui/base/pointer/touch_ui_controller.h"
#include "ui/base/theme_provider.h"
#include "ui/base/ui_base_features.h"
#include "ui/gfx/animation/animation_delegate.h"
#include "ui/gfx/animation/slide_animation.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/geometry/point.h"
#include "ui/gfx/vector_icon_types.h"
#include "ui/views/context_menu_controller.h"
#include "ui/views/controls/button/label_button.h"
#include "ui/views/controls/button/single_animated_image_container.h"
#include "ui/views/controls/highlight_path_generator.h"
#include "ui/views/metadata/view_factory.h"

class TabStripModel;

namespace test {
class ToolbarButtonTestApi;
}

namespace ui {
class MenuModel;
}

namespace views {
class MenuModelAdapter;
class MenuRunner;
}  // namespace views

// This class provides basic drawing and mouse-over behavior for buttons
// appearing in the toolbar.
class ToolbarButton : public views::LabelButton,
                      public views::ContextMenuController {
  METADATA_HEADER(ToolbarButton, views::LabelButton)

 public:
  enum class Edge {
    kLeft = 0,
    kTopLeft,
    kTopRight,
    kRight,
    kBottomLeft,
    kBottomRight,
  };

  // More convenient form of the ctor below, when |model| and |tab_strip_model|
  // are both nullptr.
  explicit ToolbarButton(PressedCallback callback = PressedCallback());

  // |tab_strip_model| must outlive this class.
  // |model| can be null if no menu is to be shown.
  // |tab_strip_model| is only needed if showing the menu with |model| requires
  // an active tab. There may be no active tab in |tab_strip_model| during
  // shutdown.
  ToolbarButton(PressedCallback callback,
                std::unique_ptr<ui::MenuModel> model,
                TabStripModel* tab_strip_model,
                bool trigger_menu_on_long_press = true);
  ToolbarButton(const ToolbarButton&) = delete;
  ToolbarButton& operator=(const ToolbarButton&) = delete;
  ~ToolbarButton() override;

  // Highlights the button by setting the label to given |highlight_text|, using
  // tinting it using the |hightlight_color| if set. The highlight is displayed
  // using an animation. If some highlight is already set, it shows the new
  // highlight directly without any animation. To clear the previous highlight
  // (also using an animation), call this function with both parameters empty.
  void SetHighlight(const std::u16string& highlight_text,
                    std::optional<SkColor> highlight_color);

  // Sets the leading margin when the browser is maximized and updates layout to
  // make the focus rectangle centered.
  void SetLeadingMargin(int margin);

  // Sets the trailing margin when the browser is maximized and updates layout
  // to make the focus rectangle centered.
  void SetTrailingMargin(int margin);

  // Methods for handling ButtonDropDown-style menus.
  void ClearPendingMenu();
  bool IsMenuShowing() const;

  // Sets the button's vector icon. This icon uses the default colors and are
  // automatically updated on theme changes.
  void SetVectorIcon(const gfx::VectorIcon& icon);

  // Sets an icon and touch-mode icon for this button.
  // TODO(pbos): Investigate if touch-mode icons are just different-size ones
  // that can be folded into the same .icon file as different CANVAS_DIMENSIONS.
  void SetVectorIcons(const gfx::VectorIcon& icon,
                      const gfx::VectorIcon& touch_icon);

  // Updates the images using the given icon and the default colors returned by
  // GetForegroundColor().
  void UpdateIconsWithStandardColors(const gfx::VectorIcon& icon);

  // Updates the icon images and colors as necessary. Should be called any time
  // icon state changes, e.g. in response to theme or touch mode changes.
  virtual void UpdateIcon();

  // Returns the button's corner radius for `edge`.
  virtual float GetCornerRadiusFor(ToolbarButton::Edge edge) const;

  // Gets/Sets |layout_insets_|, see comment there.
  std::optional<gfx::Insets> GetLayoutInsets() const;
  void SetLayoutInsets(const std::optional<gfx::Insets>& insets);

  // Sets |layout_inset_delta_|, see comment there.
  void SetLayoutInsetDelta(const gfx::Insets& insets);

  // views::LabelButton:
  void StateChanged(ButtonState old_state) override;
  void OnBoundsChanged(const gfx::Rect& previous_bounds) override;
  void OnThemeChanged() override;
  gfx::Rect GetAnchorBoundsInScreen() const override;
  bool OnMousePressed(const ui::MouseEvent& event) override;
  bool OnMouseDragged(const ui::MouseEvent& event) override;
  void OnMouseReleased(const ui::MouseEvent& event) override;
  // Showing the drop down results in a MouseCaptureLost, we need to ignore it.
  void OnMouseCaptureLost() override;
  void OnMouseExited(const ui::MouseEvent& event) override;
  void OnGestureEvent(ui::GestureEvent* event) override;
  std::unique_ptr<views::ActionViewInterface> GetActionViewInterface() override;

  // When IPH is showing we suppress the tooltip text. This means that we must
  // provide an alternative accessible name, when this is the case. This is
  // because `Button::AdjustAccessibleName` will use the tooltip text when the
  // accessible name is empty, and if the tooltip text is also empty then the
  // button will have no accessible name.
  std::u16string GetAlternativeAccessibleName() const override;

  ChromeColorIds GetDefaultBackgroundColorId() const {
    return default_background_color_id_;
  }

  // views::ContextMenuController:
  void ShowContextMenuForViewImpl(
      View* source,
      const gfx::Point& point,
      ui::mojom::MenuSourceType source_type) override;

  // ui::PropertyHandler:
  void AfterPropertyChange(const void* key, int64_t old_value) override;

  ui::MenuModel* menu_model() { return model_.get(); }

  void set_menu_identifier(ui::ElementIdentifier menu_identifier) {
    menu_identifier_ = menu_identifier;
  }
  ui::ElementIdentifier menu_identifier() const { return menu_identifier_; }

  bool GetVectorIconsHasValueForTesting() { return vector_icons_.has_value(); }

  void SetInternalPadding(gfx::Insets insets);

 protected:
  struct VectorIcons {
    // RAW_PTR_EXCLUSION: Never allocated by PartitionAlloc (always points to a
    // global), so there is no benefit to using a raw_ptr, only cost.
    RAW_PTR_EXCLUSION const gfx::VectorIcon& icon;
    RAW_PTR_EXCLUSION const gfx::VectorIcon& touch_icon;
  };

  // Returns if menu should be shown. Override this to change default behavior.
  virtual bool ShouldShowMenu();

  // Returns if the button inkdrop should persist after the user interacts with
  // IPH for the button. Override this to change default behavior.
  // TODO(crbug.com/40258442): Investigate if this is still needed and if so how
  // it can be applied to all Buttons rather than just ToolbarButtons.
  virtual bool ShouldShowInkdropAfterIphInteraction();

  // Function to show the dropdown menu.
  virtual void ShowDropDownMenu(ui::mojom::MenuSourceType source_type);

  // Shows the given `menu_model` anchored to this button.
  void ShowMenuForModel(ui::mojom::MenuSourceType source_type,
                        ui::MenuModel* menu_model);

  // Sets a default background color
  void SetDefaultBackgroundColorId(ChromeColorIds color_id);

  // Updates the button's background and border.
  virtual void UpdateColorsAndInsets();

  // Returns the standard toolbar button foreground color for the given state.
  // This color is typically used for the icon and text of toolbar buttons.
  virtual SkColor GetForegroundColor(ButtonState state) const;

  // Returns the icon size of the toolbar button
  virtual int GetIconSize() const;

  // Retuns true if a non-empty border should be painted.
  virtual bool ShouldPaintBorder() const;

  // Retuns true if the background highlight color should be blended
  // with the toolbar color.
  virtual bool ShouldBlendHighlightColor() const;

  // Returns whether to directly use the highlight as background instead
  // of blending it with the toolbar colors.
  // TODO(shibalik): remove this method after fixing for profile button.
  virtual bool ShouldDirectlyUseHighlightAsBackground() const;

  // Virtual method to explicitly set the highlighted text color instead of the
  // default behavior of the HighlightColorAnimation.
  virtual std::optional<SkColor> GetHighlightTextColor() const;

  // Virtual method to explicitly set the highlighted border color instead of
  // the default behavior of the HighlightColorAnimation.
  virtual std::optional<SkColor> GetHighlightBorderColor() const;

  const std::optional<VectorIcons>& GetVectorIcons() const {
    return vector_icons_;
  }

  // Sets the spacing on the outer side of the label (not the side where the
  // image is). The spacing is applied only when the label is non-empty.
  void SetLabelSideSpacing(int spacing);

  // Returns the target insets according to the button's layout.
  const gfx::Insets GetTargetInsets() const;

  // Returns the target size according to the current button's size and insets.
  const gfx::Size GetTargetSize() const;

  // Returns the button's rounded corner radius based on its size.
  virtual int GetRoundedCornerRadius() const;

  // Updates the images using the given icons and specific colors.
  void UpdateIconsWithColors(const gfx::VectorIcon& icon,
                             SkColor normal_color,
                             SkColor hovered_color,
                             SkColor pressed_color,
                             SkColor disabled_color);

  std::optional<SkColor> GetBackgroundColor() const;

  // views::LabelButton:
  // Callers should use SetHighlight() instead which sets an optional color as
  // well.
  void SetText(std::u16string_view text) override;

  views::SingleAnimatedImageContainer& animated_image_container() {
    CHECK(image_container());
    return *static_cast<views::SingleAnimatedImageContainer*>(
        image_container());
  }

 private:
  friend test::ToolbarButtonTestApi;

  class HighlightColorAnimation : gfx::AnimationDelegate {
   public:
    explicit HighlightColorAnimation(ToolbarButton* parent);
    HighlightColorAnimation(const HighlightColorAnimation&) = delete;
    HighlightColorAnimation& operator=(const HighlightColorAnimation&) = delete;
    ~HighlightColorAnimation() override;

    // Starts a fade-in animation using the provided |highlight color| or using
    // a default color if not set.
    void Show(std::optional<SkColor> highlight_color);

    // Starts a fade-out animation. A no-op if the fade-out animation is
    // currently in progress or not shown.
    void Hide();

    // Returns current text / border / background / ink-drop base color based on
    // current |highlight_color_| and on the current animation state (which
    // influences the alpha channel). Returns no value if there is no such color
    // and we should use the default text color / paint no border / paint no
    // background / use the default ink-drop base color.
    std::optional<SkColor> GetTextColor() const;
    std::optional<SkColor> GetBorderColor() const;
    std::optional<SkColor> GetBackgroundColor() const;
    std::optional<SkColor> GetInkDropBaseColor() const;

    void AnimationEnded(const gfx::Animation* animation) override;
    void AnimationProgressed(const gfx::Animation* animation) override;

   private:
    friend test::ToolbarButtonTestApi;

    // Returns whether the animation is currently shown. Note that this returns
    // true even after calling Hide() until the fade-out animation finishes.
    bool IsShown() const;

    void ClearHighlightColor();

    const raw_ptr<ToolbarButton> parent_;

    // A highlight color is used to signal special states. When set this color
    // is used as a base for background, text, border and ink drops. When not
    // set, uses the default ToolbarButton ink drop.
    std::optional<SkColor> highlight_color_;

    // Animation for showing the highlight color (in border, text, and
    // background) when it becomes non-empty and hiding it when it becomes empty
    // again.
    gfx::SlideAnimation highlight_color_animation_;
  };

  void TouchUiChanged();

  // Clears the current highlight, i.e. it sets the label to an empty string and
  // clears the highlight color. If there was a non-empty highlight, previously,
  // it hides the current highlight using an animation. Otherwise, it is a
  // no-op.
  void ClearHighlight();

  // Callback for MenuModelAdapter.
  void OnMenuClosed();

  // Sets the in product help promo. Called after the kHasInProductHelpPromoKey
  // property changes. When this button has an in product help promo, the button
  // gets a blue highlight that pulses.
  void SetHasInProductHelpPromo(bool has_in_product_help_promo);

  // The model that populates the attached menu.
  std::unique_ptr<ui::MenuModel> model_;

  const raw_ptr<TabStripModel> tab_strip_model_;

  // Indicates if menu is currently showing.
  bool menu_showing_ = false;

  // Whether the menu should be shown when there is a long mouse press or a drag
  // event.
  const bool trigger_menu_on_long_press_;

  // Determines whether to highlight the button for in-product help.
  // TODO(crbug.com/40258442): Remove this member after issue is addressed.
  bool has_in_product_help_promo_ = false;

  // Y position of mouse when left mouse button is pressed.
  int y_position_on_lbuttondown_ = 0;

  // The model adapter for the drop down menu.
  std::unique_ptr<views::MenuModelAdapter> menu_model_adapter_;

  // Menu runner to display drop down menu.
  std::unique_ptr<views::MenuRunner> menu_runner_;

  // Optional identifier for the menu when it runs.
  ui::ElementIdentifier menu_identifier_;

  // Layout insets to use. This is used when the ToolbarButton is not actually
  // hosted inside the toolbar. If not supplied,
  // |GetLayoutInsets(TOOLBAR_BUTTON)| is used instead which is not appropriate
  // outside the toolbar.
  std::optional<gfx::Insets> layout_insets_;

  // Delta from regular toolbar-button insets. This is necessary for buttons
  // that use smaller or larger icons than regular ToolbarButton instances.
  // CastToolbarButton for instance uses larger insets for touchable mode to
  // match the expected touchable UI.
  gfx::Insets layout_inset_delta_;

  // Used to ensure the button remains highlighted while the menu is active.
  std::optional<Button::ScopedAnchorHighlight> menu_anchor_higlight_;

  // Vector icons for the ToolbarButton. The icon is chosen based on touch-ui.
  // Reacts to theme changes using default colors.
  std::optional<VectorIcons> vector_icons_;

  // Class responsible for animating highlight color (calling a callback on
  // |this| to refresh UI).
  HighlightColorAnimation highlight_color_animation_;

  // Suppress tooltip when IPH is showing.
  std::u16string suppressed_tooltip_text_;

  base::CallbackListSubscription subscription_ =
      ui::TouchUiController::Get()->RegisterCallback(
          base::BindRepeating(&ToolbarButton::TouchUiChanged,
                              base::Unretained(this)));

  // Default background color
  std::optional<SkColor> default_background_color_;
  // Default background color id
  ChromeColorIds default_background_color_id_ =
      ChromeColorIds::kChromeColorsStart;

  // A factory for tasks that show the dropdown context menu for the button.
  base::WeakPtrFactory<ToolbarButton> show_menu_factory_{this};
};

class ToolbarButtonActionViewInterface
    : public views::LabelButtonActionViewInterface {
 public:
  explicit ToolbarButtonActionViewInterface(ToolbarButton* action_view);
  ~ToolbarButtonActionViewInterface() override = default;

  // LabelButtonActionViewInterface:
  void ActionItemChangedImpl(actions::ActionItem* action_item) override;

 private:
  raw_ptr<ToolbarButton> action_view_;
};

BEGIN_VIEW_BUILDER(CHROME_VIEWS_EXPORT, ToolbarButton, views::LabelButton)
VIEW_BUILDER_PROPERTY(std::optional<gfx::Insets>, LayoutInsets)
END_VIEW_BUILDER

DEFINE_VIEW_BUILDER(CHROME_VIEWS_EXPORT, ToolbarButton)

#endif  // CHROME_BROWSER_UI_VIEWS_TOOLBAR_TOOLBAR_BUTTON_H_
