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

#ifndef COMPONENTS_LIVE_CAPTION_VIEWS_CAPTION_BUBBLE_H_
#define COMPONENTS_LIVE_CAPTION_VIEWS_CAPTION_BUBBLE_H_

#include <memory>
#include <string>

#include "base/functional/callback.h"
#include "base/functional/callback_helpers.h"
#include "base/gtest_prod_util.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
#include "build/buildflag.h"
#include "components/live_caption/caption_bubble_settings.h"
#include "components/live_caption/views/caption_bubble_model.h"
#include "components/live_caption/views/translation_view_wrapper_base.h"
#include "ui/base/metadata/metadata_header_macros.h"
#include "ui/gfx/animation/slide_animation.h"
#include "ui/gfx/font.h"
#include "ui/gfx/font_list.h"
#include "ui/native_theme/caption_style.h"
#include "ui/views/bubble/bubble_dialog_delegate_view.h"
#include "ui/views/controls/button/button.h"
#include "ui/views/controls/button/md_text_button.h"
#include "ui/views/controls/scroll_view.h"
#include "ui/views/controls/styled_label.h"
#include "ui/views/metadata/view_factory.h"

namespace views {
class Checkbox;
class ImageButton;
class ImageView;
class Label;
}  // namespace views

namespace captions {

class CaptionBubbleEventObserver;
class CaptionBubbleFrameView;
class CaptionBubbleLabel;
class CaptionBubbleScrollView;
class ScrollLockButton;

// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused. These should be the same as
// LiveCaptionSessionEvent in enums.xml.
// LINT.IfChange(SessionEvent)
enum class SessionEvent {
  // We began showing captions for an audio stream.
  kStreamStarted = 0,
  // The audio stream ended and the caption bubble closes.
  kStreamEnded = 1,
  // The close button was clicked, so we stopped listening to an audio stream.
  kCloseButtonClicked = 2,
  kMaxValue = kCloseButtonClicked,
};
// LINT.ThenChange(/tools/metrics/histograms/metadata/accessibility/enums.xml:LiveCaptionSessionEvent)

#if BUILDFLAG(IS_CHROMEOS)
// Used by ash window manager to place the caption bubble in the correct
// container.
extern const ui::ClassProperty<bool>* const kIsCaptionBubbleKey;
#endif

///////////////////////////////////////////////////////////////////////////////
// Caption Bubble
//
//  A caption bubble that floats above all other windows and shows
//  automatically- generated text captions for audio and media streams. The
//  captions bubble's widget is a top-level window that has top z order and is
//  visible on all workspaces. It is draggable in and out of the tab.
//
class CaptionBubble : public views::BubbleDialogDelegateView,
                      public gfx::AnimationDelegate,
                      public TranslationViewWrapperBase::Delegate {
  METADATA_HEADER(CaptionBubble, views::BubbleDialogDelegateView)

 public:
  using NewFontListGetter = base::RepeatingCallback<gfx::FontList(
      const std::vector<std::string>& font_names,
      int font_style,
      int font_size,
      gfx::Font::Weight font_weight)>;

  CaptionBubble(
      CaptionBubbleSettings* caption_bubble_settings,
      std::unique_ptr<TranslationViewWrapperBase> translation_view_wrapper,
      const std::string& application_locale,
      base::OnceClosure destroyed_callback);
  CaptionBubble(const CaptionBubble&) = delete;
  CaptionBubble& operator=(const CaptionBubble&) = delete;
  ~CaptionBubble() override;

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

  // Sets the caption bubble model currently being used for this caption bubble.
  // There exists one CaptionBubble per profile, but one CaptionBubbleModel per
  // media stream. A new CaptionBubbleModel is set when transcriptions from a
  // different media stream are received. A CaptionBubbleModel is owned by the
  // CaptionBubbleControllerViews. It is created when transcriptions from a new
  // media stream are received and exists until the audio stream ends for that
  // stream.
  void SetModel(CaptionBubbleModel* model);

  // Changes the caption style of the caption bubble.
  void UpdateCaptionStyle(std::optional<ui::CaptionStyle> caption_style);

  views::Label* GetLabelForTesting();
  views::ScrollView* GetScrollViewForTesting();
  views::Label* GetDownloadProgressLabelForTesting();
  views::Label* GetScrollLockLabelForTesting();
  bool IsGenericErrorMessageVisibleForTesting() const;
  views::Button* GetCloseButtonForTesting();
  views::Button* GetBackToTabButtonForTesting();
  views::MdTextButton* GetScrollLockButtonForTesting();
  views::View* GetHeaderForTesting();
  TranslationViewWrapperBase* GetTranslationViewWrapperForTesting();
  void SetNewFontListGetterForTesting(NewFontListGetter callback);

  void SetCaptionBubbleStyle();

#if BUILDFLAG(IS_WIN)
  void OnContentSettingsLinkClicked();
#endif

  void UpdateControlsVisibility(bool show_controls);
  void OnMouseEnteredOrExitedWindow(bool entered);

  void SetTitleTextForTesting(const std::u16string title_text) {
    title_->SetText(title_text);
  }

 protected:
  // views::BubbleDialogDelegateView:
  void Init() override;
  void OnBeforeBubbleWidgetInit(views::Widget::InitParams* params,
                                views::Widget* widget) const override;
  bool ShouldShowCloseButton() const override;
  std::unique_ptr<views::FrameView> CreateFrameView(
      views::Widget* widget) override;
  gfx::Rect GetBubbleBounds() override;
  void OnWidgetActivationChanged(views::Widget* widget, bool active) override;
  std::u16string GetAccessibleWindowTitle() const override;
  void OnThemeChanged() override;

 private:
  friend class CaptionBubbleControllerViewsTest;
  friend class CaptionBubbleModel;
  FRIEND_TEST_ALL_PREFIXES(CaptionBubbleControllerViewsTest,
                           AccessibleProperties);

  void BackToTabButtonPressed();
  void CloseButtonPressed();
  void ExpandOrCollapseButtonPressed();
  void SwapButtons(views::Button* first_button,
                   views::Button* second_button,
                   bool show_first_button);
  // TranslationViewWrapperBase::Delegate:
  void CaptionSettingsButtonPressed() override;
  void ScrollLockButtonPressed();

  // Called by CaptionBubbleModel to notify this object that the model's text
  // has changed. Sets the text of the caption bubble to the model's text.
  void OnTextChanged();

  // Called by CaptionBubbleModel to notify this object that the model's
  // download progress text has changed. Sets the text of the caption bubble to
  // the model's download progress text.
  void OnDownloadProgressTextChanged();

  void OnLanguagePackInstalled();

  // Called by CaptionBubbleModel to notify this object that the model's
  // auto-detected language has changed.
  void OnAutoDetectedLanguageChanged();

  // Used to prevent propagating theme changes when no theme colors have
  // changed. Returns whether the caption theme colors have changed since the
  // last time this function was called.
  bool ThemeColorsChanged();

  // Called by CaptionBubbleModel to notify this object that the model's error
  // state has changed. Makes the caption bubble display an error message if
  // the model has an error, otherwise displays the latest text.
  void OnErrorChanged(CaptionBubbleErrorType error_type,
                      OnErrorClickedCallback callback,
                      OnDoNotShowAgainClickedCallback error_silenced_callback);

  // The caption bubble manages its own visibility based on whether there's
  // space for it to be shown, and if it has an error or text to display.
  void UpdateBubbleVisibility();
  void UpdateBubbleAndTitleVisibility();

  // For the provided line index, gets the corresponding rendered line in the
  // label and returns the text position of the first character of that line.
  // Returns the same value regardless of whether the label is visible or not.
  size_t GetTextIndexOfLineInLabel(size_t line) const;

  // Returns the number of lines in the caption bubble label that are rendered.
  size_t GetNumLinesInLabel() const;
  int GetNumLinesVisible();

  // Internal service methods.
  void MaybeScrollToBottom();
  void UpdateContentSize();
  void Redraw();
  void ShowInactive();
  void Hide();

  // The following methods set the caption bubble style based on the user's
  // preferences, which are stored in `caption_style_`.
  double GetTextScaleFactor();
  const gfx::FontList GetFontList(int font_size);
  void SetTextSizeAndFontFamily();
  void SetTextColor();
  void SetBackgroundColor();

  // TranslationViewWrapperBase::Delegate:
  void OnLanguageChanged(const std::string& display_language) override;
  void UpdateLanguageDirection(const std::string& display_language) override;

  // Places the bubble at the bottom center of the context widget for the active
  // model, ensuring that it's positioned where the user will spot it. If there
  // are multiple browser windows open, and the user plays media on the second
  // window, the caption bubble will show up in the bottom center of the second
  // window, which is where the user is already looking. It also ensures that
  // the caption bubble will appear in the right workspace if a user has Chrome
  // windows open on multiple workspaces. This method has no effect if the
  // active model has changed between when it was posted and executed, which
  // is ensured by passing the active model's id as |model_id|.
  void RepositionInContextRect(CaptionBubbleModel::Id model_id,
                               const gfx::Rect& context_rect);

  void AdjustPosition(CaptionBubbleModel::Id model_id,
                      const gfx::Rect& context_rect);

  void MediaFoundationErrorCheckboxPressed();
  bool HasMediaFoundationError();

  void LogSessionEvent(SessionEvent event);

  std::vector<raw_ptr<views::View, VectorExperimental>> GetButtons();

  void OnTitleTextChanged();

  void UpdateAccessibleName();

  bool IsScrollabilityEnabled() const;
  void ResetScrollIfLocked(gfx::PointF current_offset,
                           views::ScrollView* scrollable);

  // Unowned. Owned by views hierarchy.
  raw_ptr<CaptionBubbleLabel> label_;
  raw_ptr<CaptionBubbleScrollView> scrollable_;
  raw_ptr<views::Label> title_;
  raw_ptr<views::Label> generic_error_text_;
  raw_ptr<views::Label> download_progress_label_;
  raw_ptr<ScrollLockButton> scroll_lock_button_;
  raw_ptr<views::View> header_container_;
  raw_ptr<views::View> left_header_container_;
  raw_ptr<views::View> translate_header_container_;
  raw_ptr<views::ImageView> generic_error_icon_;
  raw_ptr<views::View> generic_error_message_;
  raw_ptr<views::ImageButton> back_to_tab_button_;
  raw_ptr<views::ImageButton> close_button_;
  raw_ptr<views::ImageButton> expand_button_;
  raw_ptr<views::ImageButton> collapse_button_;
  raw_ptr<CaptionBubbleFrameView> frame_;

#if BUILDFLAG(IS_WIN)
  raw_ptr<views::StyledLabel> media_foundation_renderer_error_text_;
  raw_ptr<views::ImageView> media_foundation_renderer_error_icon_;
  raw_ptr<views::View> media_foundation_renderer_error_message_;

  // Checkbox the user can use to indicate whether to silence the error message
  // for the origin.
  raw_ptr<views::Checkbox> media_foundation_renderer_error_checkbox_ = nullptr;
#endif

  std::optional<ui::CaptionStyle> caption_style_;
  raw_ptr<CaptionBubbleModel> model_ = nullptr;
  const raw_ptr<CaptionBubbleSettings> caption_bubble_settings_;
  std::unique_ptr<TranslationViewWrapperBase> translation_view_wrapper_;

  OnErrorClickedCallback error_clicked_callback_;
  OnDoNotShowAgainClickedCallback error_silenced_callback_;
  base::ScopedClosureRunner destroyed_callback_;

  const std::string application_locale_;

  // Whether the caption bubble is expanded to show more lines of text.
  bool is_expanded_;

  bool has_been_shown_ = false;

  // Used to determine whether to propagate theme changes to the widget.
  SkColor text_color_ = gfx::kPlaceholderColor;
  SkColor icon_color_ = gfx::kPlaceholderColor;
  SkColor icon_disabled_color_ = gfx::kPlaceholderColor;
  SkColor link_color_ = gfx::kPlaceholderColor;
  SkColor checkbox_color_ = gfx::kPlaceholderColor;
  SkColor background_color_ = gfx::kPlaceholderColor;

  gfx::SlideAnimation controls_animation_;

  bool render_active_ = false;
  bool mouse_inside_window_ = false;
  std::unique_ptr<CaptionBubbleEventObserver> caption_bubble_event_observer_;

  base::CallbackListSubscription title_text_changed_callback_;

  NewFontListGetter new_font_list_getter_;

  base::WeakPtrFactory<CaptionBubble> weak_ptr_factory_{this};
};

BEGIN_VIEW_BUILDER(/* no export */,
                   CaptionBubble,
                   views::BubbleDialogDelegateView)
END_VIEW_BUILDER

}  // namespace captions

DEFINE_VIEW_BUILDER(/* no export */, captions::CaptionBubble)

#endif  // COMPONENTS_LIVE_CAPTION_VIEWS_CAPTION_BUBBLE_H_
