// 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_MEDIA_ROUTER_MEDIA_ROUTER_UI_H_
#define CHROME_BROWSER_UI_MEDIA_ROUTER_MEDIA_ROUTER_UI_H_

#include <set>
#include <string>
#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/media_router/cast_dialog_model.h"
#include "chrome/browser/ui/media_router/media_cast_mode.h"
#include "chrome/browser/ui/media_router/media_route_starter.h"
#include "chrome/browser/ui/media_router/media_router_ui_helper.h"
#include "chrome/browser/ui/media_router/media_sink_with_cast_modes.h"
#include "chrome/browser/ui/media_router/media_sink_with_cast_modes_observer.h"
#include "chrome/browser/ui/media_router/presentation_request_source_observer.h"
#include "components/media_router/browser/issues_observer.h"
#include "components/media_router/browser/media_router_dialog_controller.h"
#include "components/media_router/browser/mirroring_media_controller_host.h"
#include "components/media_router/common/issue.h"
#include "components/media_router/common/media_source.h"

namespace content {
struct PresentationRequest;
class WebContents;
}  // namespace content

namespace U_ICU_NAMESPACE {
class Collator;
}

namespace media_router {

class MediaRoute;
class MediaRouter;
class MediaRoutesObserver;
class MediaSink;
class RouteRequestResult;
class WebContentsDisplayObserver;

// Functions as an intermediary between MediaRouter and Views Cast dialog.
class MediaRouterUI : public CastDialogController,
                      public MediaSinkWithCastModesObserver,
                      public PresentationRequestSourceObserver,
                      public MirroringMediaControllerHost::Observer {
 public:
  // MediaRouterUI's are typically created with one of the CreateWith* methods
  // below.
  explicit MediaRouterUI(
      std::unique_ptr<MediaRouteStarter> media_route_starter);

  MediaRouterUI(const MediaRouterUI&) = delete;
  MediaRouterUI& operator=(const MediaRouterUI&) = delete;

  ~MediaRouterUI() override;

  // Creates a |MediaRouterUI| (e.g. starts listening for MediaSinks) for
  // targeting the default MediaSource (if any) of |initiator|. The contents
  // of the UI will change as the default MediaSource changes. If there is a
  // default MediaSource, then PRESENTATION MediaCastMode will be added to
  // |cast_modes_|.
  static std::unique_ptr<MediaRouterUI> CreateWithDefaultMediaSource(
      content::WebContents* initiator);
  // Initializes mirroring sources of the tab in addition to what is done by
  // |CreateWithDefaultMediaSource()|.
  static std::unique_ptr<MediaRouterUI>
  CreateWithDefaultMediaSourceAndMirroring(content::WebContents* initiator);

  // Initializes internal state targeting the presentation specified in
  // |context|. This is different from CreateWithDefaultMediaSource*() in that
  // it does not listen for default media source changes, as the UI is fixed to
  // the source in |context|. Init* methods can only be called once.
  // |context|: Context object for the PresentationRequest. This instance will
  // take ownership of it. Must not be null.
  static std::unique_ptr<MediaRouterUI> CreateWithStartPresentationContext(
      content::WebContents* initiator,
      std::unique_ptr<StartPresentationContext> context);
  // Initializes mirroring sources of the tab in addition to what is done by
  // |CreateWithStartPresentationContext()|.
  static std::unique_ptr<MediaRouterUI>
  CreateWithStartPresentationContextAndMirroring(
      content::WebContents* initiator,
      std::unique_ptr<StartPresentationContext> context);
  // Initializes RemotePlayback source only.
  static std::unique_ptr<MediaRouterUI> CreateWithMediaSessionRemotePlayback(
      content::WebContents* initiator,
      media::VideoCodec video_codec,
      media::AudioCodec audio_codec);

  // CastDialogController:
  void AddObserver(CastDialogController::Observer* observer) override;
  void RemoveObserver(CastDialogController::Observer* observer) override;
  void StartCasting(const std::string& sink_id,
                    MediaCastMode cast_mode) override;
  void StopCasting(const std::string& route_id) override;
  void ClearIssue(const Issue::Id& issue_id) override;
  void FreezeRoute(const std::string& route_id) override;
  void UnfreezeRoute(const std::string& route_id) override;
  // Note that |MediaRouterUI| should not be used after |TakeMediaRouteStarter|
  // is called. To enforce that, |TakeMediaRouteStarter| calls the destructor
  // callback given to |RegisterDestructor| to destroy itself.
  std::unique_ptr<MediaRouteStarter> TakeMediaRouteStarter() override;
  void RegisterDestructor(base::OnceClosure destructor) override;

  // Requests a route be created from the source mapped to
  // |cast_mode|, to the sink given by |sink_id|.
  // Returns true if a route request is successfully submitted.
  // |OnRouteResponseReceived()| will be invoked when the route request
  // completes.
  virtual bool CreateRoute(const MediaSink::Id& sink_id,
                           MediaCastMode cast_mode);

  // Calls MediaRouter to terminate the given route.
  void TerminateRoute(const MediaRoute::Id& route_id);

  // Returns a subset of |sinks_| that should be listed in the dialog. This
  // excludes the wired display that the initiator WebContents is on.
  // Also filters cloud sinks in incognito windows.
  std::vector<MediaSinkWithCastModes> GetEnabledSinks() const;

  // Calls MediaRouter to add the given issue.
  void AddIssue(const IssueInfo& issue);

  // Calls MediaRouter to remove the given issue.
  void RemoveIssue(const Issue::Id& issue_id);

  // Uses LoggerImpl to log current available sinks.
  void LogMediaSinkStatus();

  const std::vector<MediaRoute>& routes() const { return routes_; }
  content::WebContents* initiator() const {
    return media_route_starter()->GetWebContents();
  }

  base::WeakPtr<MediaRouterUI> GetWeakPtr();

 private:
  friend class MediaRouterViewsUITest;
  friend class MediaRouterCastUiForTest;
  FRIEND_TEST_ALL_PREFIXES(MediaRouterViewsUITest, SetDialogHeader);
  FRIEND_TEST_ALL_PREFIXES(MediaRouterViewsUITest,
                           UpdateSinksWhenDialogMovesToAnotherDisplay);
  FRIEND_TEST_ALL_PREFIXES(MediaRouterViewsUITest, NotifyObserver);
  FRIEND_TEST_ALL_PREFIXES(MediaRouterViewsUITest, SinkFriendlyName);
  FRIEND_TEST_ALL_PREFIXES(MediaRouterViewsUITest, ConnectingState);
  FRIEND_TEST_ALL_PREFIXES(MediaRouterViewsUITest, DisconnectingState);
  FRIEND_TEST_ALL_PREFIXES(MediaRouterViewsUITest, AddAndRemoveIssue);
  FRIEND_TEST_ALL_PREFIXES(MediaRouterViewsUITest, ShowDomainForHangouts);
  FRIEND_TEST_ALL_PREFIXES(MediaRouterViewsUIIncognitoTest,
                           HidesCloudSinksForIncognito);
  FRIEND_TEST_ALL_PREFIXES(MediaRouterViewsUITest, RouteCreationTimeout);
  FRIEND_TEST_ALL_PREFIXES(MediaRouterViewsUITest,
                           RouteCreationTimeoutIssueTitle);
  FRIEND_TEST_ALL_PREFIXES(MediaRouterViewsUITest,
                           DesktopMirroringFailsWhenDisallowedOnMac);
  FRIEND_TEST_ALL_PREFIXES(MediaRouterViewsUITest,
                           RouteCreationLocalFileModeInTab);
  FRIEND_TEST_ALL_PREFIXES(MediaRouterUITest,
                           UIMediaRoutesObserverAssignsCurrentCastModes);
  FRIEND_TEST_ALL_PREFIXES(MediaRouterUITest,
                           UIMediaRoutesObserverSkipsUnavailableCastModes);
  FRIEND_TEST_ALL_PREFIXES(MediaRouterUITest,
                           UpdateSinksWhenDialogMovesToAnotherDisplay);
  FRIEND_TEST_ALL_PREFIXES(MediaRouterViewsUITest, OnFreezeInfoChanged);

  class WebContentsFullscreenOnLoadedObserver;

  // This class calls to refresh the UI when the highest priority issue is
  // updated.
  class UiIssuesObserver : public IssuesObserver {
   public:
    UiIssuesObserver(IssueManager* issue_manager, MediaRouterUI* ui);

    UiIssuesObserver(const UiIssuesObserver&) = delete;
    UiIssuesObserver& operator=(const UiIssuesObserver&) = delete;

    ~UiIssuesObserver() override;

    // IssuesObserver:
    void OnIssue(const Issue& issue) override;
    void OnIssuesCleared() override;

   private:
    // Reference back to the owning MediaRouterUI instance.
    const raw_ptr<MediaRouterUI> ui_;
  };

  class UIMediaRoutesObserver : public MediaRoutesObserver {
   public:
    using RoutesUpdatedCallback =
        base::RepeatingCallback<void(const std::vector<MediaRoute>&)>;
    UIMediaRoutesObserver(MediaRouter* router,
                          const RoutesUpdatedCallback& callback);

    UIMediaRoutesObserver(const UIMediaRoutesObserver&) = delete;
    UIMediaRoutesObserver& operator=(const UIMediaRoutesObserver&) = delete;

    ~UIMediaRoutesObserver() override;

    // MediaRoutesObserver:
    void OnRoutesUpdated(const std::vector<MediaRoute>& routes) override;

   private:
    // Callback to the owning MediaRouterUI instance.
    RoutesUpdatedCallback callback_;
  };

  virtual void Init();

  // Removes |MediaRouteStarter| listeners and alerts observers that this
  // controller is now invalid.
  void DetachFromMediaRouteStarter();

  // PresentationRequestSourceObserver
  void OnSourceUpdated(std::u16string& source_name) override;

  // MirroringObserver
  void OnFreezeInfoChanged() override;

  // Called to update the dialog with the current list of of enabled sinks.
  void UpdateSinks();

  std::u16string GetSinkFriendlyNameFromId(const MediaSink::Id& sink_id);

  // Creates and sends an issue if route creation timed out.
  void SendIssueForRouteTimeout(
      MediaCastMode cast_mode,
      const MediaSink::Id& sink_id,
      const std::u16string& presentation_request_source_name);

  // Creates and sends an issue if casting fails due to lack of screen
  // permissions.
  void SendIssueForScreenPermission(const MediaSink::Id& sink_id);

  // Creates and sends an issue if casting fails for any reason other than
  // those above.
  void SendIssueForUnableToCast(MediaCastMode cast_mode,
                                const MediaSink::Id& sink_id);

  // Creates and sends an issue for notifying the user that the tab audio cannot
  // be mirrored from their device.
  void SendIssueForTabAudioNotSupported(const MediaSink::Id& sink_id);

  // Creates and sends an issue when the user rejects the Cast request on the
  // receiver device.
  void SendIssueForUserNotAllowed(const MediaSink::Id& sink_id);

  // Creates and send an issue when casting did not start because notifications
  // are disabled on the receiver device.
  void SendIssueForNotificationDisabled(const MediaSink::Id& sink_id);

  // Returns the IssueManager associated with |router_|.
  IssueManager* GetIssueManager();

  // Instantiates and initializes the issues observer.
  void StartObservingIssues();

  void OnIssue(const Issue& issue);
  void OnIssueCleared();

  // Called by |routes_observer_| when the set of active routes has changed.
  void OnRoutesUpdated(const std::vector<MediaRoute>& routes);

  // MediaSinkWithCastModesObserver:
  void OnSinksUpdated(
      const std::vector<MediaSinkWithCastModes>& sinks) override;

  // Callback passed to MediaRouter to receive response to route creation
  // requests.
  virtual void OnRouteResponseReceived(
      int route_request_id,
      const MediaSink::Id& sink_id,
      MediaCastMode cast_mode,
      const std::u16string& presentation_request_source_name,
      const RouteRequestResult& result);

  // Update the header text in the dialog model and notify observers.
  void UpdateModelHeader(const std::u16string& source_name);

  UIMediaSink ConvertToUISink(const MediaSinkWithCastModes& sink,
                              const MediaRoute* route,
                              const std::optional<Issue>& issue);

  void StopObservingMirroringMediaControllerHosts();

  // Returns the MediaRouter for this instance's BrowserContext.
  virtual MediaRouter* GetMediaRouter() const;

  const std::optional<RouteRequest> current_route_request() const {
    return current_route_request_;
  }

  MediaRouteStarter* media_route_starter() const {
    DCHECK(media_route_starter_)
        << "can't call media_route_starter() after TakeMediaRouteStarter()!";
    return media_route_starter_.get();
  }

  // Helper factory that creates both the |MediaRouteStarter| and then a
  // |MediaRouterUI| in a single step.
  static std::unique_ptr<MediaRouterUI> CreateMediaRouterUI(
      MediaRouterUIParameters params);

  raw_ptr<content::WebContentsObserver> web_contents_observer_for_test_ =
      nullptr;

  // This value is set whenever there is an outstanding issue.
  std::optional<Issue> issue_;

  // Contains up-to-date data to show in the dialog.
  CastDialogModel model_;

  // This value is set when the UI requests a route to be terminated, and gets
  // reset when the route is removed.
  std::optional<MediaRoute::Id> terminating_route_id_;

  // Observers for dialog model updates.
  // TODO(takumif): CastDialogModel should manage the observers.
  base::ObserverList<CastDialogController::Observer>::Unchecked observers_;

  // This is non-null while this instance is registered to receive
  // updates from them.
  std::unique_ptr<MediaRoutesObserver> routes_observer_;

  // This contains a value only when tracking a pending route request.
  std::optional<RouteRequest> current_route_request_;

  // Used for locale-aware sorting of sinks by name. Set during
  // Init() using the current locale.
  std::unique_ptr<icu::Collator> collator_;

  std::vector<MediaSinkWithCastModes> sinks_;
  std::vector<MediaRoute> routes_;

  std::unique_ptr<MediaRouteStarter> media_route_starter_;

  std::unique_ptr<IssuesObserver> issues_observer_;

  // Keeps track of which display the initiator WebContents is on. This is used
  // to make sure we don't show a wired display presentation over the
  // controlling window.
  std::unique_ptr<WebContentsDisplayObserver> display_observer_;

  raw_ptr<MediaRouter> router_;
  raw_ptr<LoggerImpl> logger_;

  base::OnceClosure destructor_;

  // NOTE: Weak pointers must be invalidated before all other member variables.
  // Therefore |weak_factory_| must be placed at the end.
  base::WeakPtrFactory<MediaRouterUI> weak_factory_{this};
};

}  // namespace media_router

#endif  // CHROME_BROWSER_UI_MEDIA_ROUTER_MEDIA_ROUTER_UI_H_
