// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/notifications/notification_ui_manager_impl.h"

#include <memory>
#include <utility>

#include "base/check_op.h"
#include "build/build_config.h"
#include "chrome/browser/notifications/fullscreen_notification_blocker.h"
#include "chrome/browser/notifications/popups_only_ui_controller.h"
#include "chrome/browser/notifications/profile_notification.h"
#include "chrome/browser/notifications/screen_lock_notification_blocker.h"
#include "chrome/browser/profiles/profile.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/url_constants.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/common/extension_set.h"
#include "extensions/common/permissions/permissions_data.h"
#include "ui/gfx/image/image_skia.h"
#include "ui/message_center/message_center.h"
#include "ui/message_center/message_center_types.h"
#include "ui/message_center/public/cpp/message_center_constants.h"
#include "ui/message_center/public/cpp/notification.h"
#include "ui/message_center/public/cpp/notifier_id.h"
#include "url/origin.h"

using message_center::MessageCenter;
using message_center::NotifierId;

// static
std::unique_ptr<NotificationUIManager> NotificationUIManager::Create() {
  // If there's no MessageCenter, there should be no NotificationUIManager to
  // manage it.
  if (!message_center::MessageCenter::Get())
    return nullptr;

  return std::make_unique<NotificationUIManagerImpl>();
}

NotificationUIManagerImpl::NotificationUIManagerImpl()
    : system_observer_(this),
      popups_only_ui_controller_(std::make_unique<PopupsOnlyUiController>()) {
  auto* message_center = MessageCenter::Get();
  message_center->AddObserver(this);

  auto screen_lock_notification_blocker =
      std::make_unique<ScreenLockNotificationBlocker>(message_center);
  screen_lock_notification_blocker->Init();
  blockers_.push_back(std::move(screen_lock_notification_blocker));
  auto fullscreen_notification_blocker =
      std::make_unique<FullscreenNotificationBlocker>(message_center);
  fullscreen_notification_blocker->Init();
  blockers_.push_back(std::move(fullscreen_notification_blocker));
}

NotificationUIManagerImpl::~NotificationUIManagerImpl() {
  // The message center may have already been shut down (on Chrome OS).
  if (MessageCenter::Get())
    MessageCenter::Get()->RemoveObserver(this);

  profile_notifications_.clear();
}

////////////////////////////////////////////////////////////////////////////////
// NotificationUIManager

void NotificationUIManagerImpl::Add(
    const message_center::Notification& notification,
    Profile* profile) {
  // We won't have time to process and act on this notification.
  if (is_shutdown_started_)
    return;

  if (Update(notification, profile))
    return;

  auto profile_notification_ptr =
      std::make_unique<ProfileNotification>(profile, notification);
  ProfileNotification* profile_notification = profile_notification_ptr.get();

  // WARNING: You MUST use AddProfileNotification or update the message center
  // via the notification within a ProfileNotification object or the profile ID
  // will not be correctly set for ChromeOS.
  // Takes ownership of profile_notification.
  AddProfileNotification(std::move(profile_notification_ptr));

  MessageCenter::Get()->AddNotification(
      std::make_unique<message_center::Notification>(
          profile_notification->notification()));

  if (profile && profile->IsOffTheRecord() &&
      !observed_otr_profiles_.IsObservingSource(profile)) {
    observed_otr_profiles_.AddObservation(profile);
  }
}

bool NotificationUIManagerImpl::Update(
    const message_center::Notification& notification,
    Profile* profile) {
  const std::string profile_id = ProfileNotification::GetProfileNotificationId(
      notification.id(), ProfileNotification::GetProfileID(profile));
  for (auto iter = profile_notifications_.begin();
       iter != profile_notifications_.end(); ++iter) {
    ProfileNotification* old_notification = (*iter).second.get();
    if (old_notification->notification().id() != profile_id)
      continue;

    // The ID should uniquely identify the notification, but as a sanity check
    // make sure we got the right origin URL and profile.
    DCHECK_EQ(old_notification->notification().origin_url(),
              notification.origin_url());
    DCHECK_EQ(old_notification->profile_id(),
              ProfileNotification::GetProfileID(profile));

    // Changing the type from non-progress to progress does not count towards
    // the immediate update allowed in the message center.
    std::string old_id = old_notification->notification().id();

    // Add/remove notification in the local list but just update the same
    // one in MessageCenter.
    auto new_profile_notification =
        std::make_unique<ProfileNotification>(profile, notification);
    const message_center::Notification& new_notification =
        new_profile_notification->notification();
    // Delete the old one after the new one is created to ensure we don't run
    // out of KeepAlives.
    profile_notifications_.erase(old_id);
    profile_notifications_[new_notification.id()] =
        std::move(new_profile_notification);

    // TODO(liyanhou): Add routing updated notifications to alternative
    // providers.

    // WARNING: You MUST use AddProfileNotification or update the message
    // center via the notification within a ProfileNotification object or the
    // profile ID will not be correctly set for ChromeOS.
    MessageCenter::Get()->UpdateNotification(
        old_id,
        std::make_unique<message_center::Notification>(new_notification));
    return true;
  }

  return false;
}

const message_center::Notification* NotificationUIManagerImpl::FindById(
    const std::string& id,
    ProfileNotification::ProfileID profile_id) const {
  std::string profile_notification_id =
      ProfileNotification::GetProfileNotificationId(id, profile_id);
  auto iter = profile_notifications_.find(profile_notification_id);
  if (iter == profile_notifications_.end())
    return nullptr;
  return &(iter->second->notification());
}

bool NotificationUIManagerImpl::CancelById(
    const std::string& id,
    ProfileNotification::ProfileID profile_id) {
  std::string profile_notification_id =
      ProfileNotification::GetProfileNotificationId(id, profile_id);
  // See if this ID hasn't been shown yet.
  // If it has been shown, remove it.
  auto iter = profile_notifications_.find(profile_notification_id);
  if (iter == profile_notifications_.end())
    return false;

  RemoveProfileNotification(iter->first);
  MessageCenter::Get()->RemoveNotification(profile_notification_id,
                                           /* by_user */ false);
  return true;
}

std::set<std::string> NotificationUIManagerImpl::GetAllIdsByProfile(
    ProfileNotification::ProfileID profile_id) {
  std::set<std::string> original_ids;
  for (const auto& pair : profile_notifications_) {
    if (pair.second->profile_id() == profile_id) {
      original_ids.insert(pair.second->original_id());
    }
  }

  return original_ids;
}

std::set<std::string> NotificationUIManagerImpl::GetAllIdsByProfileAndOrigin(
    ProfileNotification::ProfileID profile_id,
    const GURL& origin) {
  std::set<std::string> original_ids;
  for (const auto& pair : profile_notifications_) {
    if (pair.second->profile_id() == profile_id &&
        url::IsSameOriginWith(pair.second->notification().origin_url(),
                              origin)) {
      original_ids.insert(pair.second->original_id());
    }
  }

  return original_ids;
}

bool NotificationUIManagerImpl::CancelAllBySourceOrigin(const GURL& source) {
  // Same pattern as CancelById, but more complicated than the above
  // because there may be multiple notifications from the same source.
  bool removed = false;

  for (auto loopiter = profile_notifications_.begin();
       loopiter != profile_notifications_.end();) {
    auto curiter = loopiter++;
    if ((*curiter).second->notification().origin_url() == source) {
      const std::string id = curiter->first;
      RemoveProfileNotification(id);
      MessageCenter::Get()->RemoveNotification(id, /* by_user */ false);
      removed = true;
    }
  }
  return removed;
}

void NotificationUIManagerImpl::CancelAll() {
  MessageCenter::Get()->RemoveAllNotifications(
      false /* by_user */, message_center::MessageCenter::RemoveType::ALL);
}

void NotificationUIManagerImpl::StartShutdown() {
  is_shutdown_started_ = true;
  CancelAll();
  popups_only_ui_controller_.reset();
}

////////////////////////////////////////////////////////////////////////////////
// MessageCenter::Observer

void NotificationUIManagerImpl::OnNotificationRemoved(const std::string& id,
                                                      bool by_user) {
  RemoveProfileNotification(id);
}

////////////////////////////////////////////////////////////////////////////////
// ProfileObserver

void NotificationUIManagerImpl::OnProfileWillBeDestroyed(Profile* profile) {
  observed_otr_profiles_.RemoveObservation(profile);

  // Same pattern as CancelAllBySourceOrigin.
  for (auto loopiter = profile_notifications_.begin();
       loopiter != profile_notifications_.end();) {
    auto curiter = loopiter++;
    if (ProfileNotification::GetProfileID(profile) ==
        (*curiter).second->profile_id()) {
      const std::string id = curiter->first;
      RemoveProfileNotification(id);
      MessageCenter::Get()->RemoveNotification(id, /* by_user */ false);
    }
  }
}

void NotificationUIManagerImpl::ResetUiControllerForTest() {
  popups_only_ui_controller_.reset();
}

std::string NotificationUIManagerImpl::GetMessageCenterNotificationIdForTest(
    const std::string& id,
    Profile* profile) {
  return ProfileNotification::GetProfileNotificationId(
      id, ProfileNotification::GetProfileID(profile));
}

////////////////////////////////////////////////////////////////////////////////
// private

void NotificationUIManagerImpl::AddProfileNotification(
    std::unique_ptr<ProfileNotification> profile_notification) {
  const message_center::Notification& notification =
      profile_notification->notification();
  std::string id = notification.id();
  // Notification ids should be unique.
  DCHECK(profile_notifications_.find(id) == profile_notifications_.end());
  profile_notifications_[id] = std::move(profile_notification);
}

void NotificationUIManagerImpl::RemoveProfileNotification(
    const std::string& notification_id) {
  auto it = profile_notifications_.find(notification_id);
  if (it == profile_notifications_.end())
    return;

  // Delay destruction of the ProfileNotification until current task is
  // completed. This must be done because this ProfileNotification might have
  // the one ScopedKeepAlive object that was keeping the browser alive, and
  // destroying it would result in:
  // a) A reentrant call to this class. Because every method in this class
  //    touches |profile_notifications_|, |profile_notifications_| must always
  //    be in a self-consistent state in moments where re-entrance might happen.
  // b) A crash like https://crbug.com/41277292 because it can trigger
  //    shutdown process while we're still inside the call stack from UI
  //    framework.
  content::GetUIThreadTaskRunner({})->DeleteSoon(FROM_HERE,
                                                 it->second.release());
  profile_notifications_.erase(it);
}

ProfileNotification* NotificationUIManagerImpl::FindProfileNotification(
    const std::string& id) const {
  auto iter = profile_notifications_.find(id);
  if (iter == profile_notifications_.end())
    return nullptr;

  return (*iter).second.get();
}
