// 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.

#include <memory>

#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/scoped_multi_source_observation.h"
#include "base/strings/utf_string_conversions.h"
#include "build/build_config.h"
#include "chrome/browser/ui/browser_init_state.h"
#include "chrome/browser/ui/screen_capture_notification_ui.h"
#include "chrome/browser/ui/views/chrome_views_export.h"
#include "chrome/browser/ui/views/screen_sharing_util.h"
#include "chrome/grit/generated_resources.h"
#include "chrome/grit/theme_resources.h"
#include "content/public/browser/desktop_media_id.h"
#include "content/public/browser/web_contents.h"
#include "ui/base/hit_test.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_header_macros.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/color/color_id.h"
#include "ui/color/color_provider.h"
#include "ui/display/display.h"
#include "ui/display/screen.h"
#include "ui/views/background.h"
#include "ui/views/bubble/bubble_border.h"
#include "ui/views/bubble/bubble_frame_view.h"
#include "ui/views/controls/button/md_text_button.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/link.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/property_effects.h"
#include "ui/views/view.h"
#include "ui/views/widget/widget.h"
#include "ui/views/widget/widget_delegate.h"

#if BUILDFLAG(IS_WIN)
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/shell_integration_win.h"
#include "chrome/browser/ui/browser_init_state.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/browser_window/public/global_browser_collection.h"
#include "content/public/browser/web_contents.h"
#include "ui/base/win/shell.h"
#include "ui/views/win/hwnd_util.h"
#endif

#if BUILDFLAG(IS_CHROMEOS)
#include "ash/shell.h"
#endif

namespace {

const int kHorizontalMargin = 10;
const float kWindowAlphaValue = 0.96f;

using content::DesktopMediaID;
using UserInteraction = GetDisplayMediaUserInteractionWithControls;

// A ClientView that overrides NonClientHitTest() so that the whole window area
// acts as a window caption, except a rect specified using SetClientRect().
// ScreenCaptureNotificationUIViews uses this class to make the notification bar
// draggable.
class NotificationBarClientView : public views::ClientView {
  METADATA_HEADER(NotificationBarClientView, views::ClientView)

 public:
  NotificationBarClientView(views::Widget* widget, views::View* view)
      : views::ClientView(widget, view) {}
  NotificationBarClientView(const NotificationBarClientView&) = delete;
  NotificationBarClientView& operator=(const NotificationBarClientView&) =
      delete;
  ~NotificationBarClientView() override = default;

  void SetClientRect(const gfx::Rect& rect) {
    if (rect_ == rect) {
      return;
    }
    rect_ = rect;
    OnPropertyChanged(&rect_, views::PropertyEffects::kNone);
  }
  gfx::Rect GetClientRect() const { return rect_; }

  // views::ClientView:
  int NonClientHitTest(const gfx::Point& point) override {
    if (!bounds().Contains(point)) {
      return HTNOWHERE;
    }

    // Client rect is the actionable area of the notification bar.
    // `point` is in the coordinates of the widget while `rect_` is in the
    // unmirrored coordinates of the view. In RTL, we need to convert the point
    // to the mirrored coordinates to compare.
    // GetMirroredRect() returns the rect as is in LTR.
    if (GetMirroredRect(rect_).Contains(
            gfx::PointAtOffsetFromOrigin(point - origin()))) {
      return HTCLIENT;
    }

    // Make the other part of the window draggable.
    return HTCAPTION;
  }

 private:
  gfx::Rect rect_;
};

BEGIN_METADATA(NotificationBarClientView)
ADD_PROPERTY_METADATA(gfx::Rect, ClientRect)
END_METADATA

}  // namespace

class ScreenCaptureNotificationUIViews : public views::WidgetDelegateView,
                                         public views::ViewObserver {
  METADATA_HEADER(ScreenCaptureNotificationUIViews, views::WidgetDelegateView)

 public:
  ScreenCaptureNotificationUIViews(
      const std::u16string& text,
      content::WebContents* capturing_web_contents,
      DesktopMediaID::Type captured_surface_type,
      base::OnceClosure stop_callback,
      content::MediaStreamUI::SourceCallback source_callback);
  ScreenCaptureNotificationUIViews(const ScreenCaptureNotificationUIViews&) =
      delete;
  ScreenCaptureNotificationUIViews& operator=(
      const ScreenCaptureNotificationUIViews&) = delete;
  ~ScreenCaptureNotificationUIViews() override;

  // views::WidgetDelegateView:
  views::ClientView* CreateClientView(views::Widget* widget) override;
  std::unique_ptr<views::FrameView> CreateFrameView(
      views::Widget* widget) override;

  // views::ViewObserver:
  void OnViewBoundsChanged(views::View* observed_view) override;
  void OnViewIsDeleting(views::View* observed_view) override;

 private:
  void OnUserClickedStop();
  void OnUserClickedChangeSource();
  void OnUserClickedHide();

  void HandleStopped();
  void HandleSourceChange();
  void HandleHide();

  const base::WeakPtr<content::WebContents> capturing_web_contents_;
  ScreensharingControlsHistogramLogger uma_logger_;
  base::OnceClosure stop_callback_;
  content::MediaStreamUI::SourceCallback source_callback_;
  base::ScopedMultiSourceObservation<views::View, views::ViewObserver>
      view_observations_{this};
  raw_ptr<NotificationBarClientView> client_view_ = nullptr;
  raw_ptr<views::View> source_button_ = nullptr;
  raw_ptr<views::View> stop_button_ = nullptr;
  raw_ptr<views::View> hide_link_ = nullptr;
};

ScreenCaptureNotificationUIViews::ScreenCaptureNotificationUIViews(
    const std::u16string& text,
    content::WebContents* capturing_web_contents,
    DesktopMediaID::Type captured_surface_type,
    base::OnceClosure stop_callback,
    content::MediaStreamUI::SourceCallback source_callback)
    : capturing_web_contents_(capturing_web_contents
                                  ? capturing_web_contents->GetWeakPtr()
                                  : nullptr),
      uma_logger_(captured_surface_type),
      stop_callback_(std::move(stop_callback)),
      source_callback_(std::move(source_callback)) {
  SetShowCloseButton(false);
  SetShowTitle(false);
  SetTitle(text);

  RegisterDeleteDelegateCallback(
      RegisterDeleteCallbackPassKey(),
      base::BindOnce(&ScreenCaptureNotificationUIViews::HandleStopped,
                     base::Unretained(this)));

  SetLayoutManager(std::make_unique<views::BoxLayout>(
      views::BoxLayout::Orientation::kHorizontal, gfx::Insets(),
      kHorizontalMargin));

  auto gripper = std::make_unique<views::ImageView>();
  gripper->SetImage(
      ui::ImageModel::FromResourceId(IDR_SCREEN_CAPTURE_NOTIFICATION_GRIP));
  AddChildView(std::move(gripper));

  auto label = std::make_unique<views::Label>(text);
  label->SetElideBehavior(gfx::ELIDE_MIDDLE);
  label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
  AddChildView(std::move(label));

  std::u16string source_text =
      l10n_util::GetStringUTF16(IDS_MEDIA_SCREEN_CAPTURE_NOTIFICATION_SOURCE);
  source_button_ = AddChildView(std::make_unique<views::MdTextButton>(
      base::BindRepeating(
          &ScreenCaptureNotificationUIViews::OnUserClickedChangeSource,
          base::Unretained(this)),
      source_text));

  if (source_callback_.is_null()) {
    source_button_->SetVisible(false);
  }

  std::u16string stop_text =
      l10n_util::GetStringUTF16(IDS_MEDIA_SCREEN_CAPTURE_NOTIFICATION_STOP);
  auto stop_button = std::make_unique<views::MdTextButton>(
      base::BindRepeating(&ScreenCaptureNotificationUIViews::OnUserClickedStop,
                          base::Unretained(this)),
      stop_text);
  stop_button->SetStyle(ui::ButtonStyle::kProminent);
  stop_button_ = AddChildView(std::move(stop_button));

  auto hide_link = std::make_unique<views::Link>(
      l10n_util::GetStringUTF16(IDS_MEDIA_SCREEN_CAPTURE_NOTIFICATION_HIDE));
  hide_link->SetCallback(
      base::BindRepeating(&ScreenCaptureNotificationUIViews::OnUserClickedHide,
                          base::Unretained(this)));
  hide_link_ = AddChildView(std::move(hide_link));

  // The client rect for NotificationBarClientView uses the bounds for the
  // following views.
  view_observations_.AddObservation(source_button_.get());
  view_observations_.AddObservation(stop_button_.get());
  view_observations_.AddObservation(hide_link_.get());

  SetBackground(views::CreateSolidBackground(ui::kColorDialogBackground));
}

ScreenCaptureNotificationUIViews::~ScreenCaptureNotificationUIViews() {
  source_callback_.Reset();
  stop_callback_.Reset();
}

views::ClientView* ScreenCaptureNotificationUIViews::CreateClientView(
    views::Widget* widget) {
  DCHECK(!client_view_);
  client_view_ = new NotificationBarClientView(widget, this);
  // To prevent client_view_ from dangling, we observe it for deletion.
  view_observations_.AddObservation(client_view_.get());
  return client_view_;
}

std::unique_ptr<views::FrameView>
ScreenCaptureNotificationUIViews::CreateFrameView(views::Widget* widget) {
  constexpr auto kPadding = gfx::Insets::VH(5, 10);
  auto frame =
      std::make_unique<views::BubbleFrameView>(gfx::Insets(), kPadding);
  frame->SetBubbleBorder(std::make_unique<views::BubbleBorder>(
      views::BubbleBorder::NONE, views::BubbleBorder::STANDARD_SHADOW));
  return frame;
}

void ScreenCaptureNotificationUIViews::OnViewBoundsChanged(
    views::View* observed_view) {
  if (observed_view == client_view_.get()) {
    return;
  }
  gfx::Rect client_rect = source_button_->bounds();
  client_rect.Union(stop_button_->bounds());
  client_rect.Union(hide_link_->bounds());
  client_view_->SetClientRect(client_rect);
}

void ScreenCaptureNotificationUIViews::OnViewIsDeleting(
    views::View* observed_view) {
  if (observed_view == client_view_.get()) {
    client_view_ = nullptr;
  }
}

void ScreenCaptureNotificationUIViews::OnUserClickedStop() {
  uma_logger_.Log(UserInteraction::kStopButtonClicked);
  HandleStopped();
}

void ScreenCaptureNotificationUIViews::OnUserClickedChangeSource() {
  // TODO(crbug.com/380211805): Log UMA when this code path is implemented.
  // uma_logger_.Log(...);
  HandleSourceChange();
}

void ScreenCaptureNotificationUIViews::OnUserClickedHide() {
  uma_logger_.Log(UserInteraction::kHideButtonClicked);
  HandleHide();
}

void ScreenCaptureNotificationUIViews::HandleStopped() {
  if (!stop_callback_.is_null()) {
    std::move(stop_callback_).Run();
  }
}

void ScreenCaptureNotificationUIViews::HandleSourceChange() {
  if (!source_callback_.is_null()) {
    // CSC is only supported for tab-capture, so setting it to `false` is the
    // correct behavior so long as we don't support cross-surface-type
    // switching.
    source_callback_.Run(DesktopMediaID(),
                         /*captured_surface_control_active=*/false);
  }
}

void ScreenCaptureNotificationUIViews::HandleHide() {
  GetWidget()->Minimize();
}

BEGIN_METADATA(ScreenCaptureNotificationUIViews)
END_METADATA

namespace {

// ScreenCaptureNotificationUI implementation using Views.
class ScreenCaptureNotificationUIImpl : public ScreenCaptureNotificationUI {
 public:
  ScreenCaptureNotificationUIImpl(const std::u16string& text,
                                  content::WebContents* capturing_web_contents);
  ScreenCaptureNotificationUIImpl(const ScreenCaptureNotificationUIImpl&) =
      delete;
  ScreenCaptureNotificationUIImpl& operator=(
      const ScreenCaptureNotificationUIImpl&) = delete;
  ~ScreenCaptureNotificationUIImpl() override = default;

  // ScreenCaptureNotificationUI override:
  gfx::NativeViewId OnStarted(
      base::OnceClosure stop_callback,
      content::MediaStreamUI::SourceCallback source_callback,
      const std::vector<DesktopMediaID>& media_ids) override;

 private:
  // Helper to set window id to parent browser window id for task bar grouping.
#if BUILDFLAG(IS_WIN)
  void SetWindowsAppId(views::Widget* widget);
#endif

  std::u16string text_;
  base::WeakPtr<content::WebContents> capturing_web_contents_;
  std::unique_ptr<views::Widget> widget_;
#if BUILDFLAG(IS_MAC)
  // Ensure the notification bar appears active without needing focus.
  std::unique_ptr<views::Widget::PaintAsActiveLock> paint_as_active_lock_;
#endif  // BUILDFLAG(IS_MAC)
};

ScreenCaptureNotificationUIImpl::ScreenCaptureNotificationUIImpl(
    const std::u16string& text,
    content::WebContents* capturing_web_contents)
    : text_(text),
      capturing_web_contents_(capturing_web_contents
                                  ? capturing_web_contents->GetWeakPtr()
                                  : nullptr) {}

gfx::NativeViewId ScreenCaptureNotificationUIImpl::OnStarted(
    base::OnceClosure stop_callback,
    content::MediaStreamUI::SourceCallback source_callback,
    const std::vector<DesktopMediaID>& media_ids) {
  DesktopMediaID::Type captured_surface_type = DesktopMediaID::Type::TYPE_NONE;
  if (!media_ids.empty()) {
    CHECK(std::all_of(media_ids.cbegin(), media_ids.cend(),
                      [&media_ids](const DesktopMediaID& media_id) {
                        return media_id.type == media_ids.front().type;
                      }));
    captured_surface_type = media_ids.front().type;
  }

  if (widget_) {
    return 0;
  }

  widget_ = std::make_unique<views::Widget>();
  auto screen_capture_notification_ui_views =
      std::make_unique<ScreenCaptureNotificationUIViews>(
          text_, capturing_web_contents_.get(), captured_surface_type,
          std::move(stop_callback), std::move(source_callback));

  views::Widget::InitParams params(
      views::Widget::InitParams::CLIENT_OWNS_WIDGET,
      views::Widget::InitParams::TYPE_WINDOW);
  params.delegate = screen_capture_notification_ui_views.release();
  params.opacity = views::Widget::InitParams::WindowOpacity::kTranslucent;
  params.remove_standard_frame = true;
  params.z_order = ui::ZOrderLevel::kFloatingUIElement;
  params.name = "ScreenCaptureNotificationUIViews";

#if BUILDFLAG(IS_CHROMEOS)
  // TODO(sergeyu): The notification bar must be shown on the monitor that's
  // being captured. Make sure it's always the case. Currently we always capture
  // the primary monitor.
  params.context = ash::Shell::GetPrimaryRootWindow();
#endif

  widget_->set_frame_type(views::Widget::FrameType::kForceCustom);
  widget_->Init(std::move(params));

#if BUILDFLAG(IS_MAC)
  // Prevents the notification bar from appearing inactive on macOS
  paint_as_active_lock_ = widget_->LockPaintAsActive();
#endif  // BUILDFLAG(IS_MAC)

  display::Screen* screen = display::Screen::Get();
  // TODO(sergeyu): Move the notification to the display being captured when
  // per-display screen capture is supported.
  gfx::Rect work_area = screen->GetPrimaryDisplay().work_area();

  // Place the bar in the center of the bottom of the display.
  gfx::Size size = widget_->non_client_view()->GetPreferredSize();
  gfx::Rect bounds(work_area.x() + work_area.width() / 2 - size.width() / 2,
                   work_area.y() + work_area.height() - size.height(),
                   size.width(), size.height());
  widget_->SetBounds(bounds);

#if BUILDFLAG(IS_WIN)
  SetWindowsAppId(widget_.get());
#endif

  if (media_ids.empty() ||
      media_ids.front().type == DesktopMediaID::Type::TYPE_SCREEN) {
    // Focus the notification widget if sharing a screen.
    widget_->Show();
  } else {
    // Do not focus the notification widget if sharing a window.
    widget_->ShowInactive();
  }
  // This has to be called after Show() to have effect.
  widget_->SetOpacity(kWindowAlphaValue);
  widget_->SetVisibleOnAllWorkspaces(true);

  return 0;
}

#if BUILDFLAG(IS_WIN)
void ScreenCaptureNotificationUIImpl::SetWindowsAppId(views::Widget* widget) {
  if (!capturing_web_contents_) {
    return;
  }
  BrowserWindowInterface* browser =
      GlobalBrowserCollection::GetInstance()->FindBrowserWithTab(
          capturing_web_contents_.get());
  // Can be nullptr from extension background page call.
  if (!browser) {
    return;
  }
  const base::FilePath profile_path = browser->GetProfile()->GetPath();
  std::wstring app_user_model_id =
      browser->GetType() == BrowserWindowInterface::Type::TYPE_APP
          ? shell_integration::win::GetAppUserModelIdForApp(
                base::UTF8ToWide(
                    BrowserInitState::From(browser)->create_params().app_name),
                profile_path)
          : shell_integration::win::GetAppUserModelIdForBrowser(profile_path);
  if (!app_user_model_id.empty()) {
    ui::win::SetAppIdForWindow(app_user_model_id, views::HWNDForWidget(widget));
  }
}
#endif  // BUILDFLAG(IS_WIN)

}  // namespace

std::unique_ptr<ScreenCaptureNotificationUI>
ScreenCaptureNotificationUI::Create(
    const std::u16string& text,
    content::WebContents* capturing_web_contents) {
  return std::make_unique<ScreenCaptureNotificationUIImpl>(
      text, capturing_web_contents);
}
