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

#include "ash/system/unified/notification_icons_controller.h"

#include <optional>
#include <string>

#include "ash/constants/ash_constants.h"
#include "ash/public/cpp/vm_camera_mic_constants.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shelf/shelf.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/style/ash_color_id.h"
#include "ash/system/notification_center/ash_message_center_lock_screen_controller.h"
#include "ash/system/notification_center/message_center_utils.h"
#include "ash/system/notification_center/notification_center_tray.h"
#include "ash/system/status_area_animation_controller.h"
#include "ash/system/tray/tray_constants.h"
#include "ash/system/tray/tray_container.h"
#include "ash/system/tray/tray_utils.h"
#include "ash/system/unified/notification_counter_view.h"
#include "ash/system/unified/unified_system_tray.h"
#include "base/strings/string_util.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/models/image_model.h"
#include "ui/base/ui_base_features.h"
#include "ui/color/color_id.h"
#include "ui/color/color_provider.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/message_center/message_center.h"
#include "ui/message_center/public/cpp/notification.h"
#include "ui/message_center/vector_icons.h"
#include "ui/views/controls/image_view.h"

namespace ash {

namespace {

constexpr int kIconsViewDisplaySizeThreshold = 768;

// Maximum number of notification icons shown in the system tray button.
constexpr int kMaxNotificationIconsShown = 2;

const char kCapsLockNotifierId[] = "ash.caps-lock";
const char kBatteryNotificationNotifierId[] = "ash.battery";
const char kUsbNotificationNotifierId[] = "ash.power";

bool ShouldShowNotification(message_center::Notification* notification) {
  SessionControllerImpl* session_controller =
      Shell::Get()->session_controller();
  if (!session_controller->ShouldShowNotificationTray() ||
      (session_controller->IsScreenLocked() &&
       !AshMessageCenterLockScreenController::IsEnabled())) {
    return false;
  }

  std::string notifier_id = notification->notifier_id().id;

  if (message_center::MessageCenter::Get()->IsQuietMode() &&
      notifier_id != kCapsLockNotifierId) {
    return false;
  }

  // We don't want to show these notifications since the information is
  // already indicated by another item in tray.
  if (notifier_id == kVmCameraMicNotifierId ||
      notifier_id == kBatteryNotificationNotifierId ||
      notifier_id == kUsbNotificationNotifierId ||
      notifier_id == kPrivacyIndicatorsNotifierId ||
      notifier_id == kPrivacyIndicatorsMultiCaptureNotifierId) {
    return false;
  }

  // We only show notification icon in the tray if it is either:
  // *   Pinned (generally used for background process such as sharing your
  //     screen, capslock, etc.).
  // *   Critical warning (display failure, disk space critically low, etc.).
  return notification->pinned() ||
         notification->system_notification_warning_level() ==
             message_center::SystemNotificationWarningLevel::CRITICAL_WARNING;
}

}  // namespace

NotificationIconTrayItemView::NotificationIconTrayItemView(
    Shelf* shelf,
    NotificationIconsController* controller)
    : TrayItemView(shelf), controller_(controller) {
  CreateImageView();
}

NotificationIconTrayItemView::~NotificationIconTrayItemView() = default;

void NotificationIconTrayItemView::ResetController() {
  controller_ = nullptr;
}

void NotificationIconTrayItemView::SetNotification(
    message_center::Notification* notification) {
  notification_id_ = notification->id();
  notification_ = notification->DeepCopy(
      *notification, GetColorProvider(), /*include_body_image=*/true,
      /*include_small_image=*/true, /*include_icon_images=*/true);

  UpdateImageViewColor();
  image_view()->SetTooltipText(notification->title());
}

void NotificationIconTrayItemView::MaybeReset() {
  if ((!target_visible() && IsAnimating()) ||
      shelf()
          ->status_area_widget()
          ->animation_controller()
          ->is_hide_animation_scheduled()) {
    return;
  }
  Reset();
}

void NotificationIconTrayItemView::Reset() {
  notification_id_ = std::string();
  notification_.reset();
  image_view()->SetImage(ui::ImageModel());
  image_view()->SetTooltipText(std::u16string());
}

void NotificationIconTrayItemView::ImmediatelyUpdateVisibility() {
  TrayItemView::ImmediatelyUpdateVisibility();
  if (!target_visible()) {
    Reset();
  }
}

void NotificationIconTrayItemView::AnimationEnded(
    const gfx::Animation* animation) {
  TrayItemView::AnimationEnded(animation);
  if (!target_visible()) {
    Reset();
  }
}

const std::u16string& NotificationIconTrayItemView::GetAccessibleNameString()
    const {
  if (notification_id_.empty())
    return base::EmptyString16();
  return image_view()->GetTooltipText();
}

const std::string& NotificationIconTrayItemView::GetNotificationId() const {
  return notification_id_;
}

void NotificationIconTrayItemView::HandleLocaleChange() {}

void NotificationIconTrayItemView::OnThemeChanged() {
  TrayItemView::OnThemeChanged();
  controller_->UpdateNotificationIcons();
}

void NotificationIconTrayItemView::UpdateLabelOrImageViewColor(bool active) {
  TrayItemView::UpdateLabelOrImageViewColor(active);

  UpdateImageViewColor();
}

void NotificationIconTrayItemView::UpdateImageViewColor() {
  if (!GetWidget() || !notification_) {
    return;
  }

  const auto* color_provider = GetColorProvider();
  ui::ColorId color_id = kColorAshIconColorPrimary;
  color_id = is_active() ? cros_tokens::kCrosSysSystemOnPrimaryContainer
                         : cros_tokens::kCrosSysOnSurface;
  gfx::Image masked_small_icon = notification_->GenerateMaskedSmallIcon(
      kUnifiedTrayIconSize, color_provider->GetColor(color_id),
      color_provider->GetColor(ui::kColorNotificationIconBackground),
      color_provider->GetColor(ui::kColorNotificationIconForeground));
  if (!masked_small_icon.IsEmpty()) {
    image_view()->SetImage(ui::ImageModel::FromImage(masked_small_icon));
  } else {
    image_view()->SetImage(ui::ImageModel::FromVectorIcon(
        ::features::IsRoundedIconsEnabled() ? message_center::kChromeProductIcon
                                            : message_center::kProductOldIcon,
        color_id, kUnifiedTrayIconSize));
  }
}

BEGIN_METADATA(NotificationIconTrayItemView)
END_METADATA

NotificationIconsController::NotificationIconsController(
    Shelf* shelf,
    NotificationCenterTray* notification_center_tray)
    : shelf_(shelf),
      notification_center_tray_(notification_center_tray) {
  // `notification_center_tray` should not be null.
  DCHECK(notification_center_tray);

  // Initialize `icons_view_visible_` according to display size.
  UpdateIconsViewVisibleForDisplaySize();

  message_center::MessageCenter::Get()->AddObserver(this);
  Shell::Get()->session_controller()->AddObserver(this);
}

NotificationIconsController::~NotificationIconsController() {
  message_center::MessageCenter::Get()->RemoveObserver(this);
  Shell::Get()->session_controller()->RemoveObserver(this);
  for (NotificationIconTrayItemView* tray_item : tray_items_) {
    if (tray_item) {
      tray_item->ResetController();
    }
  }
  if (notification_counter_view_) {
    notification_counter_view_->ResetController();
  }
}

void NotificationIconsController::AddNotificationTrayItems(
    TrayContainer* tray_container) {
  for (int i = 0; i < kMaxNotificationIconsShown; ++i) {
    NotificationIconTrayItemView* tray_item = tray_container->AddChildView(
        std::make_unique<NotificationIconTrayItemView>(shelf_,
                                                       /*controller=*/this));
    tray_items_.push_back(tray_item);
    notification_center_tray_->AddTooltipChangedCallbackToNotificationIcon(tray_item);
  }

  notification_counter_view_ = tray_container->AddChildView(
      std::make_unique<NotificationCounterView>(shelf_, /*controller=*/this));

  quiet_mode_view_ =
      tray_container->AddChildView(std::make_unique<QuietModeView>(shelf_));
}

bool NotificationIconsController::TrayItemHasNotification() const {
  return first_unused_item_index_ != 0;
}

size_t NotificationIconsController::TrayNotificationIconsCount() const {
  // `first_unused_item_index_` is also the total number of notification icons
  // shown in the tray.
  return first_unused_item_index_;
}

std::optional<std::u16string>
NotificationIconsController::GetAccessibleNameString() const {
  if (quiet_mode_view_ && quiet_mode_view_->GetVisible()) {
    return quiet_mode_view_->GetAccessibleNameString();
  }

  if (!notification_counter_view_) {
    return std::nullopt;
  }

  if (!TrayItemHasNotification()) {
    return notification_counter_view_->GetAccessibleNameString();
  }

  std::vector<std::u16string> status;
  status.push_back(l10n_util::GetPluralStringFUTF16(
      IDS_ASH_STATUS_TRAY_NOTIFICATIONS_IMPORTANT_COUNT_ACCESSIBLE_NAME,
      TrayNotificationIconsCount()));
  for (NotificationIconTrayItemView* tray_item : tray_items_) {
    status.push_back(tray_item->GetAccessibleNameString());
  }
  status.push_back(
      notification_counter_view_->GetAccessibleNameString().value_or(
          std::u16string()));
  return l10n_util::GetStringFUTF16(
      IDS_ASH_STATUS_TRAY_NOTIFICATIONS_ICONS_ACCESSIBLE_NAME, status, nullptr);
}

void NotificationIconsController::UpdateNotificationIcons() {
  // Iterates `tray_items_` and notifications in reverse order so new pinned
  // notifications get shown on the left side.
  auto notifications =
      message_center_utils::GetSortedNotificationsWithOwnView();

  auto tray_it = tray_items_.rbegin();
  for (auto notification_it = notifications.rbegin();
       notification_it != notifications.rend(); ++notification_it) {
    if (tray_it == tray_items_.rend()) {
      break;
    }

    if (ShouldShowNotification(*notification_it)) {
      (*tray_it)->SetNotification(*notification_it);
      (*tray_it)->SetVisible(icons_view_visible_);
      ++tray_it;
    }
  }

  first_unused_item_index_ = std::distance(tray_items_.rbegin(), tray_it);
  notification_center_tray_->UpdateAccessibleName();

  for (; tray_it != tray_items_.rend(); ++tray_it) {
    // Note: It is important to set the visibility before resetting so that the
    // icon image does not disappear while the tray item is still visible.
    (*tray_it)->SetVisible(false);
    (*tray_it)->MaybeReset();
  }
}

void NotificationIconsController::UpdateNotificationIndicators() {
  notification_counter_view_->Update();
  quiet_mode_view_->Update();
}

void NotificationIconsController::UpdateIconsViewVisibleForDisplaySize() {
  aura::Window* window = shelf_->status_area_widget()->GetNativeWindow();
  auto display = display::Screen::Get()->GetDisplayNearestWindow(window);
  const int display_size =
      std::max(display.size().width(), display.size().height());
  icons_view_visible_ = display_size >= kIconsViewDisplaySizeThreshold;
}

void NotificationIconsController::OnDisplayMetricsChanged(
    const display::Display& display,
    uint32_t changed_metrics) {
  aura::Window* window = shelf_->status_area_widget()->GetNativeWindow();
  if (display::Screen::Get()->GetDisplayNearestWindow(window).id() !=
      display.id()) {
    return;
  }
  auto old_icons_view_visible = icons_view_visible_;
  UpdateIconsViewVisibleForDisplaySize();
  if (old_icons_view_visible == icons_view_visible_)
    return;

  UpdateNotificationIcons();
  UpdateNotificationIndicators();
}

void NotificationIconsController::OnNotificationAdded(const std::string& id) {
  message_center::Notification* notification =
      message_center::MessageCenter::Get()->FindVisibleNotificationById(id);
  // `notification` is null if it is not visible.
  if (notification && ShouldShowNotification(notification)) {
    // Reset the notification icons if a notification is added since we don't
    // know the position where its icon should be added.
    UpdateNotificationIcons();
  }

  UpdateNotificationIndicators();

  // There are certain sequences of notification updates that can require
  // explicitly updating the notification center tray even after its tray items
  // have been updated - for instance, adding an initial notification on the
  // lock screen. See http://b/297579552.
  notification_center_tray_->UpdateVisibility();
}

void NotificationIconsController::OnNotificationRemoved(const std::string& id,
                                                        bool by_user) {
  // If the notification removed is displayed in an icon, call update to show
  // another notification if needed.
  if (GetNotificationIconShownInTray(id))
    UpdateNotificationIcons();

  UpdateNotificationIndicators();

  // There are certain sequences of notification updates that can require
  // explicitly updating the notification center tray even after its tray items
  // have been updated - for instance, removing a notification group's parent
  // notification when the only remaining notifications belong to that group.
  // See http://b/296918234.
  notification_center_tray_->UpdateVisibility();
}

void NotificationIconsController::OnNotificationUpdated(const std::string& id) {
  // A notification update may impact certain notification icon(s) visibility
  // in the tray, so update all notification icons.
  UpdateNotificationIcons();
  UpdateNotificationIndicators();
}

void NotificationIconsController::OnNotificationDisplayed(
    const std::string& notification_id,
    const message_center::DisplaySource source) {
    UpdateNotificationIcons();
    UpdateNotificationIndicators();
}

void NotificationIconsController::OnQuietModeChanged(bool in_quiet_mode) {
  UpdateNotificationIcons();
  UpdateNotificationIndicators();
}

void NotificationIconsController::OnSessionStateChanged(
    session_manager::SessionState state) {
  UpdateNotificationIcons();
  UpdateNotificationIndicators();
}

NotificationIconTrayItemView*
NotificationIconsController::GetNotificationIconShownInTray(
    const std::string& id) {
  for (NotificationIconTrayItemView* tray_item : tray_items_) {
    if (tray_item->GetNotificationId() == id)
      return tray_item;
  }
  return nullptr;
}

}  // namespace ash
