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

#include "ui/message_center/views/message_view.h"

#include "base/observer_list.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "build/build_config.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/color/color_id.h"
#include "ui/color/color_provider.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/scoped_layer_animation_settings.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/color_palette.h"
#include "ui/gfx/geometry/rounded_corners_f.h"
#include "ui/gfx/image/image_skia_operations.h"
#include "ui/gfx/shadow_util.h"
#include "ui/gfx/shadow_value.h"
#include "ui/message_center/message_center.h"
#include "ui/message_center/public/cpp/message_center_constants.h"
#include "ui/message_center/views/notification_background_painter.h"
#include "ui/message_center/views/notification_control_buttons_view.h"
#include "ui/strings/grit/ui_strings.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/background.h"
#include "ui/views/border.h"
#include "ui/views/controls/button/image_button.h"
#include "ui/views/controls/focus_ring.h"
#include "ui/views/controls/highlight_path_generator.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/scroll_view.h"
#include "ui/views/focus/focus_manager.h"
#include "ui/views/widget/widget.h"

#if BUILDFLAG(IS_CHROMEOS)
#include "base/time/time.h"
#endif

namespace message_center {

namespace {

bool ShouldShowAeroShadowBorder() {
#if BUILDFLAG(IS_WIN)
  return true;
#else
  return false;
#endif
}

// Helper function to setup focus ring shapes for `MessageView`.
void InstallHighlightPathGenerator(views::View* view,
                                   float top_radius,
                                   float bottom_radius) {
  auto corners = gfx::RoundedCornersF{top_radius, top_radius, bottom_radius,
                                      bottom_radius};
  // Shrink focus ring size by -`kDefaultHaloInset` on each side to draw
  // them on top of the notifications. We need to do this because
  // `TrayBubbleView` and `MessagePopupView` has a layer that masks to bounds
  // due to which the focus ring can not extend outside the view.
  views::HighlightPathGenerator::Install(
      view, std::make_unique<views::RoundRectHighlightPathGenerator>(
                gfx::Insets(-views::FocusRing::kDefaultHaloInset), corners));
}

}  // namespace

MessageView::MessageView(const Notification& notification)
    : notification_id_(notification.id()),
      notifier_id_(notification.notifier_id()),
      timestamp_(notification.timestamp()),
      slide_out_controller_(this, this) {
  SetNotifyEnterExitOnChild(true);
  slide_out_controller_.set_trackpad_gestures_enabled(true);
  SetFocusBehavior(FocusBehavior::ALWAYS);
  views::FocusRing::Install(this);
  views::FocusRing::Get(this)->SetOutsetFocusRingDisabled(true);
  // Paint to a dedicated layer to make the layer non-opaque.
  SetPaintToLayer();
  layer()->SetFillsBoundsOpaquely(false);

  GetViewAccessibility().SetRole(ax::mojom::Role::kGenericContainer);
  GetViewAccessibility().SetRoleDescription(
      l10n_util::GetStringUTF8(IDS_MESSAGE_NOTIFICATION_ACCESSIBLE_NAME));

  UpdateWithNotification(notification);

  UpdateCornerRadius(0, 0);

  // If Aero is enabled, set shadow border.
  if (ShouldShowAeroShadowBorder()) {
    const auto& shadow =
        gfx::ShadowDetails::Get(/*elevation=*/2, gfx::RoundedCornersF());
    gfx::Insets ninebox_insets = gfx::ShadowValue::GetBlurRegion(shadow.values);
    SetBorder(views::CreateBorderPainter(
        views::Painter::CreateImagePainter(shadow.nine_patch_image,
                                           ninebox_insets),
        -gfx::ShadowValue::GetMargin(shadow.values)));
  }
}

MessageView::~MessageView() {
  RemovedFromWidget();
}

views::View* MessageView::FindGroupNotificationView(
    const std::string& notification_id) {
  // Not implemented by default.
  return nullptr;
}

// Creates text for spoken feedback from the data contained in the
// notification.
std::u16string MessageView::CreateAccessibleName(
    const Notification& notification) {
  if (!notification.accessible_name().empty())
    return notification.accessible_name();

  // Fall back to text constructed from the notification.
  // Add non-empty elements.

  std::vector<std::u16string> accessible_lines;
  if (!notification.title().empty())
    accessible_lines.push_back(notification.title());

  if (!notification.message().empty())
    accessible_lines.push_back(notification.message());

  if (!notification.context_message().empty())
    accessible_lines.push_back(notification.context_message());
  std::vector<NotificationItem> items = notification.items();
  for (size_t i = 0; i < items.size() && i < kNotificationMaximumItems; ++i) {
    accessible_lines.push_back(items[i].title() + u" " + items[i].message());
  }
  return base::JoinString(accessible_lines, u"\n");
}

void MessageView::UpdateWithNotification(const Notification& notification) {
  pinned_ = notification.pinned();

  std::u16string name = CreateAccessibleName(notification);
  if (name.empty()) {
    GetViewAccessibility().SetName(
        std::u16string(), ax::mojom::NameFrom::kAttributeExplicitlyEmpty);
  } else {
    GetViewAccessibility().SetName(name);
  }
  if (updated_name_callback_) {
    updated_name_callback_.Run(
        notification.rich_notification_data()
            .should_make_spoken_feedback_for_popup_updates);
  }
  slide_out_controller_.set_slide_mode(CalculateSlideMode());
}

void MessageView::SetIsNested() {
  DCHECK(!is_nested_) << "MessageView::SetIsNested() is called twice wrongly.";

  is_nested_ = true;
  // Update enability since it might be changed by "is_nested" flag.
  slide_out_controller_.set_slide_mode(CalculateSlideMode());
  slide_out_controller_.set_update_opacity(false);

  UpdateNestedBorder();

  if (GetControlButtonsView())
    GetControlButtonsView()->ShowCloseButton(GetMode() != Mode::PINNED);
}

void MessageView::CloseSwipeControl() {
  slide_out_controller_.CloseSwipeControl();
}

void MessageView::SlideOutAndClose(int direction) {
  // Do not process events once the message view is animating out.
  // crbug.com/940719
  SetEnabled(false);

  slide_out_controller_.SlideOutAndClose(direction);
}

void MessageView::SetExpanded(bool expanded) {
  MessageCenter::Get()->OnSetExpanded(notification_id_, expanded);
}

bool MessageView::IsExpanded() const {
  // Not implemented by default.
  return false;
}

bool MessageView::IsAutoExpandingAllowed() const {
  // Allowed by default.
  return true;
}

bool MessageView::IsManuallyExpandedOrCollapsed() const {
  // Not implemented by default.
  return false;
}

void MessageView::SetManuallyExpandedOrCollapsed(ExpandState state) {
  // Not implemented by default.
}

void MessageView::ToggleInlineSettings(const ui::Event& event) {
  // Not implemented by default.
}

void MessageView::ToggleSnoozeSettings(const ui::Event& event) {
  // Not implemented by default.
}

void MessageView::UpdateCornerRadius(int top_radius, int bottom_radius) {
  SetCornerRadius(top_radius, bottom_radius);
  if (!GetWidget()) {
    return;
  }
  UpdateBackgroundPainter();
  SchedulePaint();
}

void MessageView::OnContainerAnimationStarted() {
  // Not implemented by default.
}

void MessageView::OnContainerAnimationEnded() {
  // Not implemented by default.
}

bool MessageView::OnMousePressed(const ui::MouseEvent& event) {
  return true;
}

bool MessageView::OnMouseDragged(const ui::MouseEvent& event) {
  return true;
}

void MessageView::OnMouseReleased(const ui::MouseEvent& event) {
  if (!event.IsOnlyLeftMouseButton()) {
    return;
  }

  MessageCenter::Get()->ClickOnNotification(notification_id_);
}

void MessageView::OnMouseEntered(const ui::MouseEvent& event) {
  UpdateControlButtonsVisibility();
  MessageCenter::Get()->OnMessageViewHovered(notification_id_);
}

void MessageView::OnMouseExited(const ui::MouseEvent& event) {
  UpdateControlButtonsVisibility();
}

bool MessageView::OnKeyPressed(const ui::KeyEvent& event) {
  if (event.flags() != ui::EF_NONE) {
    return false;
  }

  if (event.key_code() == ui::VKEY_RETURN) {
    MessageCenter::Get()->ClickOnNotification(notification_id_);
    return true;
  } else if ((event.key_code() == ui::VKEY_DELETE ||
              event.key_code() == ui::VKEY_BACK)) {
    MessageCenter::Get()->RemoveNotification(notification_id_,
                                             true /* by_user */);
    return true;
  }

  return false;
}

bool MessageView::OnKeyReleased(const ui::KeyEvent& event) {
  // Space key handling is triggered at key-release timing. See
  // ui/views/controls/buttons/button.cc for why.
  if (event.flags() != ui::EF_NONE || event.key_code() != ui::VKEY_SPACE)
    return false;

  MessageCenter::Get()->ClickOnNotification(notification_id_);
  return true;
}

void MessageView::OnPaint(gfx::Canvas* canvas) {
  if (ShouldShowAeroShadowBorder()) {
    // If the border is shadow, paint border first.
    OnPaintBorder(canvas);
    // Clip at the border so we don't paint over it.
    canvas->ClipRect(GetContentsBounds());
    OnPaintBackground(canvas);
  } else {
    views::View::OnPaint(canvas);
  }
}

void MessageView::OnBlur() {
  views::View::OnBlur();
  // We paint a focus indicator.
  SchedulePaint();
}

void MessageView::OnGestureEvent(ui::GestureEvent* event) {
  if (event->type() == ui::EventType::kGestureTap) {
    MessageCenter::Get()->ClickOnNotification(notification_id_);
    event->SetHandled();
    return;
  }

  if (!event->IsScrollGestureEvent() && !event->IsFlingScrollEvent()) {
    return;
  }

  if (scroller_) {
    scroller_->OnGestureEvent(event);
  }
  event->SetHandled();
}

void MessageView::RemovedFromWidget() {
  if (!focus_manager_) {
    return;
  }
  focus_manager_->RemoveFocusChangeListener(this);
  focus_manager_ = nullptr;
}

void MessageView::AddedToWidget() {
  focus_manager_ = GetFocusManager();
  if (focus_manager_) {
    focus_manager_->AddFocusChangeListener(this);
  }
}

void MessageView::OnThemeChanged() {
  View::OnThemeChanged();
  UpdateNestedBorder();
  UpdateBackgroundPainter();
}

ui::Layer* MessageView::GetSlideOutLayer() {
  // If a message view is contained in a parent message view it should give up
  // slide behavior to the parent message view when the parent view is
  // collapsed.
  auto* nested_layer = (ShouldParentHandleSlide() && parent_message_view_)
                           ? parent_message_view_->layer()
                           : layer();
  bool is_nested = (ShouldParentHandleSlide() && parent_message_view_)
                       ? parent_message_view_->is_nested()
                       : is_nested_;
  return is_nested ? nested_layer : GetWidget()->GetLayer();
}

void MessageView::OnSlideStarted() {
  observers_.Notify(&Observer::OnSlideStarted, notification_id_);
}

void MessageView::OnSlideChanged(bool in_progress) {
  // crbug/1333664: We need to make sure to disable scrolling while a
  // notification view is sliding. This is to ensure the notification view can
  // only move horizontally or vertically at one time.
  if (scroller_ && !is_sliding_ && slide_out_controller_.GetGestureAmount()) {
    is_sliding_ = true;
    scroller_->SetVerticalScrollBarMode(
        views::ScrollView::ScrollBarMode::kDisabled);
  }

  if (scroller_ && !in_progress) {
    is_sliding_ = false;
    scroller_->SetVerticalScrollBarMode(
        views::ScrollView::ScrollBarMode::kEnabled);
  }

  if (in_progress) {
    observers_.Notify(&Observer::OnSlideChanged, notification_id_);
  } else {
    observers_.Notify(&Observer::OnSlideEnded, notification_id_);
  }
}

void MessageView::AddObserver(MessageView::Observer* observer) {
  observers_.AddObserver(observer);
}

void MessageView::RemoveObserver(MessageView::Observer* observer) {
  observers_.RemoveObserver(observer);
}

void MessageView::OnSlideOut() {
  if (ShouldParentHandleSlide() && parent_message_view_)
    return parent_message_view_->OnSlideOut();

  // The notification may be deleted after slide out, so give observers a
  // chance to handle the notification before fulling sliding out.
  observers_.Notify(&Observer::OnPreSlideOut, notification_id_);

  // Copy the |notification_id| here as calling OnSlideOut() might destroy
  // |this| but we still want to call RemoveNotification(). Note that the
  // iteration over |observers_| is still safe and will simply stop.
  std::string notification_id_copy = notification_id_;

  observers_.Notify(&Observer::OnSlideOut, notification_id_);

  auto* message_center = MessageCenter::Get();
  if (message_center->FindPopupNotificationById(notification_id_copy)) {
    message_center->MarkSinglePopupAsShown(notification_id_copy,
                                           /*mark_notification_as_read=*/true);
    return;
  }
  message_center->RemoveNotification(notification_id_copy, /*by_user=*/true);
}

void MessageView::OnDidChangeFocus(views::View* before, views::View* now) {
  if (Contains(before) || Contains(now) ||
      (GetControlButtonsView() && (GetControlButtonsView()->Contains(before) ||
                                   GetControlButtonsView()->Contains(now)))) {
    UpdateControlButtonsVisibility();
  }
}

views::SlideOutController::SlideMode MessageView::CalculateSlideMode() const {
  if (disable_slide_) {
    return views::SlideOutController::SlideMode::kNone;
  }

  switch (GetMode()) {
    case Mode::SETTING:
      return views::SlideOutController::SlideMode::kNone;
    case Mode::PINNED:
      return views::SlideOutController::SlideMode::kPartial;
    case Mode::NORMAL:
      return views::SlideOutController::SlideMode::kFull;
  }

  NOTREACHED();
}

MessageView::Mode MessageView::GetMode() const {
  if (setting_mode_) {
    return Mode::SETTING;
  }

  // Only nested notifications can be pinned. Standalones (i.e. popups) can't
  // be.
  if (pinned_ && is_nested_) {
    return Mode::PINNED;
  }

  return Mode::NORMAL;
}

float MessageView::GetSlideAmount() const {
  if (slide_out_controller_.mode() ==
      views::SlideOutController::SlideMode::kNone) {
    // The return value of this method is used by NotificationSwipeControlView
    // to determine visibility of the setting button. Returning 0 not to show
    // the setting button with SlideMode::kNone.
    return 0.f;
  }
  return slide_out_controller_.gesture_amount();
}

void MessageView::SetSettingMode(bool setting_mode) {
  setting_mode_ = setting_mode;
  slide_out_controller_.set_slide_mode(CalculateSlideMode());
  UpdateControlButtonsVisibility();
}

void MessageView::DisableNotification() {
  MessageCenter::Get()->DisableNotification(notification_id());
}

void MessageView::DisableSlideForcibly(bool disable) {
  disable_slide_ = disable;
  slide_out_controller_.set_slide_mode(CalculateSlideMode());
}

void MessageView::SetSlideButtonWidth(int control_button_width) {
  slide_out_controller_.SetSwipeControlWidth(control_button_width);
}

void MessageView::SetCornerRadius(int top_radius, int bottom_radius) {
  top_radius_ = top_radius;
  bottom_radius_ = bottom_radius;

  InstallHighlightPathGenerator(this, top_radius, bottom_radius);
}

void MessageView::OnCloseButtonPressed() {
  observers_.Notify(&Observer::OnCloseButtonPressed, notification_id_);
  MessageCenter::Get()->RemoveNotification(notification_id_,
                                           true /* by_user */);
}

void MessageView::OnSettingsButtonPressed(const ui::Event& event) {
  observers_.Notify(&Observer::OnSettingsButtonPressed, notification_id_);

  if (inline_settings_enabled_) {
    ToggleInlineSettings(event);
  } else {
    MessageCenter::Get()->ClickOnSettingsButton(notification_id_);
  }
}

void MessageView::OnSnoozeButtonPressed(const ui::Event& event) {
  observers_.Notify(&Observer::OnSnoozeButtonPressed, notification_id_);

  if (snooze_settings_enabled_) {
    ToggleSnoozeSettings(event);
  } else {
    MessageCenter::Get()->ClickOnSnoozeButton(notification_id());
  }
}

#if BUILDFLAG(IS_CHROMEOS)
base::TimeDelta MessageView::GetBoundsAnimationDuration(
    const Notification& notification) const {
  return base::Milliseconds(0);
}
#endif

bool MessageView::ShouldShowControlButtons() const {
#if BUILDFLAG(IS_CHROMEOS)
  // Users on ChromeOS are used to the Settings and Close buttons not being
  // visible at all times, but users on other platforms expect them to be
  // visible.
  auto* control_buttons_view = GetControlButtonsView();
  return control_buttons_view &&
         (control_buttons_view->IsAnyButtonFocused() ||
          (GetMode() != Mode::SETTING && IsMouseHovered()) ||
          MessageCenter::Get()->IsSpokenFeedbackEnabled());
#else
  return true;
#endif
}

bool MessageView::ShouldParentHandleSlide() const {
  if (!parent_message_view_) {
    return false;
  }

  return !parent_message_view_->IsExpanded();
}

void MessageView::UpdateBackgroundPainter() {
  const auto* color_provider = GetColorProvider();
  SkColor background_color =
      color_provider->GetColor(ui::kColorNotificationBackgroundActive);

  SetBackground(views::CreateBackgroundFromPainter(
      std::make_unique<NotificationBackgroundPainter>(
          top_radius_, bottom_radius_, background_color)));
}

void MessageView::UpdateNestedBorder() {
  if (!is_nested_ || !GetWidget()) {
    return;
  }

  SkColor border_color;
#if BUILDFLAG(IS_CHROMEOS)
  border_color = SK_ColorTRANSPARENT;
#else
  border_color =
      GetColorProvider()->GetColor(ui::kColorFocusableBorderUnfocused);
#endif

  SetBorder(views::CreateRoundedRectBorder(
      kNotificationBorderThickness, kNotificationCornerRadius, border_color));
}

void MessageView::UpdateControlButtonsVisibility() {
  auto* control_buttons_view = GetControlButtonsView();
  if (control_buttons_view)
    control_buttons_view->ShowButtons(ShouldShowControlButtons());
}

void MessageView::UpdateControlButtonsVisibilityWithNotification(
    const Notification& notification) {
  auto* control_buttons_view = GetControlButtonsView();
  if (control_buttons_view) {
    control_buttons_view->ShowButtons(ShouldShowControlButtons());
    control_buttons_view->ShowSettingsButton(
        notification.should_show_settings_button());
    control_buttons_view->ShowSnoozeButton(
        notification.should_show_snooze_button());
    control_buttons_view->ShowCloseButton(GetMode() != Mode::PINNED);
  }
  UpdateControlButtonsVisibility();
}

void MessageView::SetUpdatedNameCallback(UpdatedNameCallback callback) {
  CHECK(callback);
  updated_name_callback_ = std::move(callback);
}

BEGIN_METADATA(MessageView)
END_METADATA

}  // namespace message_center
