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

#include "chrome/browser/notifications/message_center_notification_manager.h"

#include "base/logging.h"
#include "base/memory/scoped_ptr.h"
#include "base/prefs/pref_registry_simple.h"
#include "base/prefs/pref_service.h"
#include "base/stl_util.h"
#include "chrome/browser/chrome_notification_types.h"
#include "chrome/browser/extensions/api/notification_provider/notification_provider_api.h"
#include "chrome/browser/notifications/desktop_notification_service.h"
#include "chrome/browser/notifications/desktop_notification_service_factory.h"
#include "chrome/browser/notifications/extension_welcome_notification.h"
#include "chrome/browser/notifications/extension_welcome_notification_factory.h"
#include "chrome/browser/notifications/fullscreen_notification_blocker.h"
#include "chrome/browser/notifications/message_center_settings_controller.h"
#include "chrome/browser/notifications/notification.h"
#include "chrome/browser/notifications/notification_conversion_helper.h"
#include "chrome/browser/notifications/screen_lock_notification_blocker.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser_finder.h"
#include "chrome/browser/ui/chrome_pages.h"
#include "chrome/browser/ui/host_desktop.h"
#include "chrome/common/extensions/api/notification_provider.h"
#include "chrome/common/pref_names.h"
#include "content/public/browser/notification_service.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/url_constants.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/extension_system.h"
#include "extensions/browser/info_map.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_style.h"
#include "ui/message_center/message_center_tray.h"
#include "ui/message_center/message_center_types.h"
#include "ui/message_center/notifier_settings.h"

#if defined(OS_CHROMEOS)
#include "chrome/browser/notifications/login_state_notification_blocker_chromeos.h"
#include "chrome/browser/ui/ash/multi_user/multi_user_util.h"
#endif

#if defined(USE_ASH)
#include "ash/shell.h"
#include "ash/system/web_notification/web_notification_tray.h"
#endif

#if defined(OS_WIN)
// The first-run balloon will be shown |kFirstRunIdleDelaySeconds| after all
// popups go away and the user has notifications in the message center.
const int kFirstRunIdleDelaySeconds = 1;
#endif

MessageCenterNotificationManager::MessageCenterNotificationManager(
    message_center::MessageCenter* message_center,
    PrefService* local_state,
    scoped_ptr<message_center::NotifierSettingsProvider> settings_provider)
    : message_center_(message_center),
#if defined(OS_WIN)
      first_run_idle_timeout_(
          base::TimeDelta::FromSeconds(kFirstRunIdleDelaySeconds)),
      weak_factory_(this),
#endif
      settings_provider_(settings_provider.Pass()),
      system_observer_(this),
      stats_collector_(message_center),
      google_now_stats_collector_(message_center) {
#if defined(OS_WIN)
  first_run_pref_.Init(prefs::kMessageCenterShowedFirstRunBalloon, local_state);
#endif

  message_center_->AddObserver(this);
  message_center_->SetNotifierSettingsProvider(settings_provider_.get());

#if defined(OS_CHROMEOS)
#if !defined(USE_ATHENA)
  // TODO(oshima|hashimoto): Support notification on athena. crbug.com/408755.
  blockers_.push_back(
      new LoginStateNotificationBlockerChromeOS(message_center));
#endif
#else
  blockers_.push_back(new ScreenLockNotificationBlocker(message_center));
#endif
  blockers_.push_back(new FullscreenNotificationBlocker(message_center));

#if defined(OS_WIN) || defined(OS_MACOSX) \
  || (defined(OS_LINUX) && !defined(OS_CHROMEOS))
  // On Windows, Linux and Mac, the notification manager owns the tray icon and
  // views.Other platforms have global ownership and Create will return NULL.
  tray_.reset(message_center::CreateMessageCenterTray());
#endif
  registrar_.Add(this,
                 chrome::NOTIFICATION_FULLSCREEN_CHANGED,
                 content::NotificationService::AllSources());
}

MessageCenterNotificationManager::~MessageCenterNotificationManager() {
  message_center_->SetNotifierSettingsProvider(NULL);
  message_center_->RemoveObserver(this);

  STLDeleteContainerPairSecondPointers(profile_notifications_.begin(),
                                       profile_notifications_.end());
  profile_notifications_.clear();
}

void MessageCenterNotificationManager::RegisterPrefs(
    PrefRegistrySimple* registry) {
  registry->RegisterBooleanPref(prefs::kMessageCenterShowedFirstRunBalloon,
                                false);
  registry->RegisterBooleanPref(prefs::kMessageCenterShowIcon, true);
  registry->RegisterBooleanPref(prefs::kMessageCenterForcedOnTaskbar, false);
}

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

void MessageCenterNotificationManager::Add(const Notification& notification,
                                           Profile* profile) {
  if (Update(notification, profile))
    return;

  ExtensionWelcomeNotificationFactory::GetForBrowserContext(profile)->
      ShowWelcomeNotificationIfNecessary(notification);

  // WARNING: You MUST update the message center via the notification within a
  // ProfileNotification object or the profile ID will not be correctly set for
  // ChromeOS.
  ProfileNotification* profile_notification(
      new ProfileNotification(profile, notification, message_center_));
  AddProfileNotification(profile_notification);

  // TODO(liyanhou): Change the logic to only send notifications to one party.
  // Currently, if there is an app with notificationProvider permission,
  // notifications will go to both message center and the app.
  // Change it to send notifications to message center only when the user chose
  // default message center (extension_id.empty()).

  // If there exist apps/extensions that have notificationProvider permission,
  // route notifications to one of the apps/extensions.
  std::string extension_id = GetExtensionTakingOverNotifications(profile);
  if (!extension_id.empty())
    profile_notification->AddToAlternateProvider(extension_id);

  message_center_->AddNotification(make_scoped_ptr(
      new message_center::Notification(profile_notification->notification())));
  profile_notification->StartDownloads();
}

bool MessageCenterNotificationManager::Update(const Notification& notification,
                                              Profile* profile) {
  const base::string16& replace_id = notification.replace_id();
  if (replace_id.empty())
    return false;

  const GURL origin_url = notification.origin_url();
  DCHECK(origin_url.is_valid());

  // Since replace_id is provided by arbitrary JS, we need to use origin_url
  // (which is an app url in case of app/extension) to scope the replace ids
  // in the given profile.
  for (NotificationMap::iterator iter = profile_notifications_.begin();
       iter != profile_notifications_.end(); ++iter) {
    ProfileNotification* old_notification = (*iter).second;
    if (old_notification->notification().replace_id() == replace_id &&
        old_notification->notification().origin_url() == origin_url &&
        old_notification->profile() == 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().delegate_id();

      // Add/remove notification in the local list but just update the same
      // one in MessageCenter.
      delete old_notification;
      profile_notifications_.erase(old_id);
      ProfileNotification* new_notification =
          new ProfileNotification(profile, notification, message_center_);
      profile_notifications_[notification.delegate_id()] = new_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.
      message_center_->UpdateNotification(
          old_id,
          make_scoped_ptr(new message_center::Notification(
              new_notification->notification())));

      new_notification->StartDownloads();
      return true;
    }
  }
  return false;
}

const Notification* MessageCenterNotificationManager::FindById(
    const std::string& id) const {
  NotificationMap::const_iterator iter = profile_notifications_.find(id);
  if (iter == profile_notifications_.end())
    return NULL;
  return &(iter->second->notification());
}

bool MessageCenterNotificationManager::CancelById(const std::string& id) {
  // See if this ID hasn't been shown yet.
  // If it has been shown, remove it.
  NotificationMap::iterator iter = profile_notifications_.find(id);
  if (iter == profile_notifications_.end())
    return false;

  RemoveProfileNotification(iter->second);
  message_center_->RemoveNotification(id, /* by_user */ false);
  return true;
}

std::set<std::string>
MessageCenterNotificationManager::GetAllIdsByProfileAndSourceOrigin(
    Profile* profile,
    const GURL& source) {

  std::set<std::string> notification_ids;
  for (NotificationMap::iterator iter = profile_notifications_.begin();
       iter != profile_notifications_.end(); iter++) {
    if ((*iter).second->notification().origin_url() == source &&
        profile == (*iter).second->profile()) {
      notification_ids.insert(iter->first);
    }
  }
  return notification_ids;
}

bool MessageCenterNotificationManager::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 (NotificationMap::iterator loopiter = profile_notifications_.begin();
       loopiter != profile_notifications_.end(); ) {
    NotificationMap::iterator curiter = loopiter++;
    if ((*curiter).second->notification().origin_url() == source) {
      const std::string id = curiter->first;
      RemoveProfileNotification(curiter->second);
      message_center_->RemoveNotification(id, /* by_user */ false);
      removed = true;
    }
  }
  return removed;
}

bool MessageCenterNotificationManager::CancelAllByProfile(Profile* profile) {
  // Same pattern as CancelAllBySourceOrigin.
  bool removed = false;

  for (NotificationMap::iterator loopiter = profile_notifications_.begin();
       loopiter != profile_notifications_.end(); ) {
    NotificationMap::iterator curiter = loopiter++;
    if (profile == (*curiter).second->profile()) {
      const std::string id = curiter->first;
      RemoveProfileNotification(curiter->second);
      message_center_->RemoveNotification(id, /* by_user */ false);
      removed = true;
    }
  }
  return removed;
}

void MessageCenterNotificationManager::CancelAll() {
  message_center_->RemoveAllNotifications(/* by_user */ false);
}

////////////////////////////////////////////////////////////////////////////////
// MessageCenter::Observer
void MessageCenterNotificationManager::OnNotificationRemoved(
    const std::string& notification_id,
    bool by_user) {
  NotificationMap::const_iterator iter =
      profile_notifications_.find(notification_id);
  if (iter != profile_notifications_.end())
    RemoveProfileNotification(iter->second);

#if defined(OS_WIN)
  CheckFirstRunTimer();
#endif
}

void MessageCenterNotificationManager::OnCenterVisibilityChanged(
    message_center::Visibility visibility) {
#if defined(OS_WIN)
  if (visibility == message_center::VISIBILITY_TRANSIENT)
    CheckFirstRunTimer();
#endif
}

void MessageCenterNotificationManager::OnNotificationUpdated(
    const std::string& notification_id) {
#if defined(OS_WIN)
  CheckFirstRunTimer();
#endif
}

void MessageCenterNotificationManager::EnsureMessageCenterClosed() {
  if (tray_.get())
    tray_->GetMessageCenterTray()->HideMessageCenterBubble();

#if defined(USE_ASH)
  if (ash::Shell::HasInstance()) {
    ash::WebNotificationTray* tray =
        ash::Shell::GetInstance()->GetWebNotificationTray();
    if (tray)
      tray->GetMessageCenterTray()->HideMessageCenterBubble();
  }
#endif
}

void MessageCenterNotificationManager::SetMessageCenterTrayDelegateForTest(
    message_center::MessageCenterTrayDelegate* delegate) {
  tray_.reset(delegate);
}

void MessageCenterNotificationManager::Observe(
    int type,
    const content::NotificationSource& source,
    const content::NotificationDetails& details) {
  if (type == chrome::NOTIFICATION_FULLSCREEN_CHANGED) {
    const bool is_fullscreen = *content::Details<bool>(details).ptr();

    if (is_fullscreen && tray_.get() && tray_->GetMessageCenterTray())
      tray_->GetMessageCenterTray()->HidePopupBubble();
  }
}

////////////////////////////////////////////////////////////////////////////////
// ImageDownloads

MessageCenterNotificationManager::ImageDownloads::ImageDownloads(
    message_center::MessageCenter* message_center,
    ImageDownloadsObserver* observer)
    : message_center_(message_center),
      pending_downloads_(0),
      observer_(observer) {
}

MessageCenterNotificationManager::ImageDownloads::~ImageDownloads() { }

void MessageCenterNotificationManager::ImageDownloads::StartDownloads(
    const Notification& notification) {
  // In case all downloads are synchronous, assume a pending download.
  AddPendingDownload();

  // Notification primary icon.
  StartDownloadWithImage(
      notification,
      &notification.icon(),
      notification.icon_url(),
      base::Bind(&message_center::MessageCenter::SetNotificationIcon,
                 base::Unretained(message_center_),
                 notification.delegate_id()));

  // Notification image.
  StartDownloadWithImage(
      notification,
      NULL,
      notification.image_url(),
      base::Bind(&message_center::MessageCenter::SetNotificationImage,
                 base::Unretained(message_center_),
                 notification.delegate_id()));

  // Notification button icons.
  StartDownloadWithImage(
      notification,
      NULL,
      notification.button_one_icon_url(),
      base::Bind(&message_center::MessageCenter::SetNotificationButtonIcon,
                 base::Unretained(message_center_),
                 notification.delegate_id(),
                 0));
  StartDownloadWithImage(
      notification,
      NULL,
      notification.button_two_icon_url(),
      base::Bind(&message_center::MessageCenter::SetNotificationButtonIcon,
                 base::Unretained(message_center_),
                 notification.delegate_id(),
                 1));

  // This should tell the observer we're done if everything was synchronous.
  PendingDownloadCompleted();
}

void MessageCenterNotificationManager::ImageDownloads::StartDownloadWithImage(
    const Notification& notification,
    const gfx::Image* image,
    const GURL& url,
    const SetImageCallback& callback) {
  // Set the image directly if we have it.
  if (image && !image->IsEmpty()) {
    callback.Run(*image);
    return;
  }

  // Leave the image null if there's no URL.
  if (url.is_empty())
    return;

  content::WebContents* contents = notification.delegate()->GetWebContents();
  if (!contents) {
    LOG(WARNING) << "Notification needs an image but has no WebContents";
    return;
  }

  AddPendingDownload();

  contents->DownloadImage(
      url,
      false,  // Not a favicon
      0,  // No maximum size
      base::Bind(
          &MessageCenterNotificationManager::ImageDownloads::DownloadComplete,
          AsWeakPtr(),
          callback));
}

void MessageCenterNotificationManager::ImageDownloads::DownloadComplete(
    const SetImageCallback& callback,
    int download_id,
    int http_status_code,
    const GURL& image_url,
    const std::vector<SkBitmap>& bitmaps,
    const std::vector<gfx::Size>& original_bitmap_sizes) {
  PendingDownloadCompleted();

  if (bitmaps.empty())
    return;
  gfx::Image image = gfx::Image::CreateFrom1xBitmap(bitmaps[0]);
  callback.Run(image);
}

// Private methods.

void MessageCenterNotificationManager::ImageDownloads::AddPendingDownload() {
  ++pending_downloads_;
}

void
MessageCenterNotificationManager::ImageDownloads::PendingDownloadCompleted() {
  DCHECK(pending_downloads_ > 0);
  if (--pending_downloads_ == 0 && observer_)
    observer_->OnDownloadsCompleted();
}

////////////////////////////////////////////////////////////////////////////////
// ProfileNotification

MessageCenterNotificationManager::ProfileNotification::ProfileNotification(
    Profile* profile,
    const Notification& notification,
    message_center::MessageCenter* message_center)
    : profile_(profile),
      notification_(notification),
      downloads_(new ImageDownloads(message_center, this)) {
  DCHECK(profile);
#if defined(OS_CHROMEOS)
  notification_.set_profile_id(multi_user_util::GetUserIDFromProfile(profile));
#endif
}

MessageCenterNotificationManager::ProfileNotification::~ProfileNotification() {
}

void MessageCenterNotificationManager::ProfileNotification::StartDownloads() {
  downloads_->StartDownloads(notification_);
}

void
MessageCenterNotificationManager::ProfileNotification::OnDownloadsCompleted() {
  notification_.delegate()->ReleaseRenderViewHost();
}

void
MessageCenterNotificationManager::ProfileNotification::AddToAlternateProvider(
    const std::string extension_id) {
  // Convert data from Notification type to NotificationOptions type.
  extensions::api::notifications::NotificationOptions options;
  NotificationConversionHelper::NotificationToNotificationOptions(notification_,
                                                                  &options);

  // Send the notification to the alternate provider extension/app.
  extensions::NotificationProviderEventRouter event_router(profile_);
  event_router.CreateNotification(extension_id,
                                  notification_.notifier_id().id,
                                  notification_.delegate_id(),
                                  options);
}

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

void MessageCenterNotificationManager::AddProfileNotification(
    ProfileNotification* profile_notification) {
  std::string id = profile_notification->notification().delegate_id();
  // Notification ids should be unique.
  DCHECK(profile_notifications_.find(id) == profile_notifications_.end());
  profile_notifications_[id] = profile_notification;
}

void MessageCenterNotificationManager::RemoveProfileNotification(
    ProfileNotification* profile_notification) {
  std::string id = profile_notification->notification().delegate_id();
  profile_notifications_.erase(id);
  delete profile_notification;
}

MessageCenterNotificationManager::ProfileNotification*
    MessageCenterNotificationManager::FindProfileNotification(
        const std::string& id) const {
  NotificationMap::const_iterator iter = profile_notifications_.find(id);
  if (iter == profile_notifications_.end())
    return NULL;

  return (*iter).second;
}

std::string
MessageCenterNotificationManager::GetExtensionTakingOverNotifications(
    Profile* profile) {
  // TODO(liyanhou): When additional settings in Chrome Settings is implemented,
  // change choosing the last app with permission to a user selected app.
  extensions::ExtensionRegistry* registry =
      extensions::ExtensionRegistry::Get(profile);
  DCHECK(registry);
  std::string extension_id;
  for (extensions::ExtensionSet::const_iterator it =
           registry->enabled_extensions().begin();
       it != registry->enabled_extensions().end();
       ++it) {
    if ((*it->get()).permissions_data()->HasAPIPermission(
            extensions::APIPermission::ID::kNotificationProvider)) {
      extension_id = (*it->get()).id();
      return extension_id;
    }
  }
  return extension_id;
}