// Copyright 2018 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_MEDIA_ROUTER_CAST_DIALOG_VIEW_H_
#define CHROME_BROWSER_UI_VIEWS_MEDIA_ROUTER_CAST_DIALOG_VIEW_H_

#include <memory>
#include <vector>

#include "base/gtest_prod_util.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
#include "base/observer_list.h"
#include "chrome/browser/ui/media_router/cast_dialog_controller.h"
#include "chrome/browser/ui/views/controls/hover_button.h"
#include "chrome/browser/ui/views/media_router/cast_dialog_access_code_cast_button.h"
#include "chrome/browser/ui/views/media_router/cast_dialog_metrics.h"
#include "ui/base/metadata/metadata_header_macros.h"
#include "ui/menus/simple_menu_model.h"
#include "ui/views/bubble/bubble_border.h"
#include "ui/views/bubble/bubble_dialog_delegate_view.h"
#include "ui/views/controls/button/md_text_button_with_down_arrow.h"
#include "ui/views/controls/menu/menu_runner.h"

class Profile;

namespace media_router {

class CastDialogSinkView;
enum class MediaRouterDialogActivationLocation;
struct UIMediaSink;

// View component of the Cast dialog that allows users to start and stop Casting
// to devices. The list of devices used to populate the dialog is supplied by
// CastDialogModel.
class CastDialogView : public views::BubbleDialogDelegateView,
                       public CastDialogController::Observer,
                       public ui::SimpleMenuModel::Delegate {
  METADATA_HEADER(CastDialogView, views::BubbleDialogDelegateView)

 public:
  class Observer : public base::CheckedObserver {
   public:
    virtual void OnDialogModelUpdated(CastDialogView* dialog_view) = 0;
    virtual void OnDialogWillClose(CastDialogView* dialog_view) = 0;
  };

  enum SourceType { kTab, kDesktop };

  CastDialogView(views::BubbleAnchor anchor,
                 views::BubbleBorder::Arrow anchor_position,
                 CastDialogController* controller,
                 Profile* profile,
                 const base::Time& start_time,
                 MediaRouterDialogActivationLocation activation_location,
                 actions::ActionItem* action_item = nullptr);
  ~CastDialogView() override;
  CastDialogView(const CastDialogView&) = delete;
  CastDialogView& operator=(const CastDialogView&) = delete;

  // views::WidgetDelegate:
  std::u16string GetWindowTitle() const override;

  // CastDialogController::Observer:
  void OnModelUpdated(const CastDialogModel& model) override;
  void OnControllerDestroying() override;

  // ui::SimpleMenuModel::Delegate:
  bool IsCommandIdChecked(int command_id) const override;
  bool IsCommandIdEnabled(int command_id) const override;
  void ExecuteCommand(int command_id, int event_flags) override;

  void AddObserver(Observer* observer);
  void RemoveObserver(Observer* observer);

  // If the dialog loses focus during a test and closes, the test can
  // fail unexpectedly. This method prevents that by keeping the dialog from
  // closing on blur.
  void KeepShownForTesting();

  // Called by tests.
  const std::vector<raw_ptr<CastDialogSinkView, DanglingUntriaged>>&
  sink_views_for_test() const {
    return sink_views_;
  }
  views::ScrollView* scroll_view_for_test() { return scroll_view_; }
  views::View* no_sinks_view_for_test() { return no_sinks_view_; }
  views::View* permission_rejected_view_for_test() {
    return permission_rejected_view_;
  }
  views::Button* sources_button_for_test() { return sources_button_; }
  HoverButton* access_code_cast_button_for_test() {
    return access_code_cast_button_;
  }
  ui::SimpleMenuModel* sources_menu_model_for_test() {
    return sources_menu_model_.get();
  }
  views::MenuRunner* sources_menu_runner_for_test() {
    return sources_menu_runner_.get();
  }

 private:
  // TODO(crbug.com/1346127): Remove friend classes.
  friend class CastDialogViewTest;
  friend class MediaRouterCastUiForTest;
  FRIEND_TEST_ALL_PREFIXES(CastDialogViewTest, DisableUnsupportedSinks);
  FRIEND_TEST_ALL_PREFIXES(CastDialogViewTest, ShowAndHideDialog);
  FRIEND_TEST_ALL_PREFIXES(CastDialogViewTest, ShowSourcesMenu);

  // views::BubbleDialogDelegateView:
  void Init() override;
  void WindowClosing() override;

  void ShowAccessCodeCastDialog();
  void MaybeShowAccessCodeCastButton();

  void ShowNoSinksView();
  void ShowPermissionRejectedView();
  void ShowScrollView();
  void ResetViews();

  // Applies the stored scroll state.
  void RestoreSinkListState();

  // Populates the scroll view containing sinks using the data in |model|.
  void PopulateScrollView(const std::vector<UIMediaSink>& sinks);

  void InitializeSourcesButton();

  // Shows the sources menu that allows the user to choose a source to cast.
  void ShowSourcesMenu();

  // Stores |source| as the source to be used when user selects a sink to start
  // casting, and updates the UI to reflect the selection.
  void SelectSource(SourceType source);

  void SinkPressed(size_t index);
  void IssuePressed(size_t index);
  void StopPressed(size_t index);
  void FreezePressed(size_t index);

  void MaybeSizeToContents();

  // Returns the cast mode that is selected in the sources menu and supported by
  // |sink|. Returns nullopt if no such cast mode exists.
  std::optional<MediaCastMode> GetCastModeToUse(const UIMediaSink& sink) const;

  // Disables sink buttons for sinks that do not support the currently selected
  // source.
  void DisableUnsupportedSinks();

  // Posts a delayed task to record the number of sinks shown with the metrics
  // recorder.
  void RecordSinkCountWithDelay();

  // Records the number of sinks shown with the metrics recorder.
  void RecordSinkCount();

  // Returns true iff feature is turned on and the access code casting policy
  // has been enabled for this user.
  bool IsAccessCodeCastingEnabled() const;

  // Title shown at the top of the dialog.
  std::u16string dialog_title_;

  // The source selected in the sources menu. This defaults to "tab"
  // (presentation or tab mirroring). "Tab" is represented by a single item in
  // the sources menu.
  SourceType selected_source_ = SourceType::kTab;

  // Contains references to sink views in the order they appear.
  std::vector<raw_ptr<CastDialogSinkView, DanglingUntriaged>> sink_views_;

  raw_ptr<CastDialogController> controller_;

  // ScrollView containing the list of sink buttons.
  raw_ptr<views::ScrollView> scroll_view_ = nullptr;

  // View shown while there are no sinks.
  raw_ptr<views::View> no_sinks_view_ = nullptr;
  raw_ptr<views::View> permission_rejected_view_ = nullptr;

  const raw_ptr<Profile> profile_;

  // How much |scroll_view_| is scrolled downwards in pixels. Whenever the sink
  // list is updated the scroll position gets reset, so we must manually restore
  // it to this value.
  int scroll_position_ = 0;

  // The access code cast button allows the user to add a cast device through
  // the chrome://access-code-cast dialog.
  raw_ptr<CastDialogAccessCodeCastButton> access_code_cast_button_ = nullptr;

  // The sources menu allows the user to choose a source to cast.
  raw_ptr<views::MdTextButtonWithDownArrow> sources_button_ = nullptr;
  std::unique_ptr<ui::SimpleMenuModel> sources_menu_model_;
  std::unique_ptr<views::MenuRunner> sources_menu_runner_;

  // Records UMA metrics for the dialog's behavior.
  CastDialogMetrics metrics_;

  // The action item tied to this dialog and its anchor view.
  // Used to handle bubble re-opening issues with Pinned Toolbar Buttons.
  const raw_ptr<actions::ActionItem> action_item_ = nullptr;

  // The sink that the user has selected to cast to. If the user is using
  // multiple sinks at the same time, the last activated sink is used.
  std::optional<size_t> selected_sink_index_;

  base::ObserverList<Observer> observers_;

  // When this is set to true, the dialog does not close on blur.
  bool keep_shown_for_testing_ = false;

  base::WeakPtrFactory<CastDialogView> weak_factory_{this};
};

}  // namespace media_router

#endif  // CHROME_BROWSER_UI_VIEWS_MEDIA_ROUTER_CAST_DIALOG_VIEW_H_
