// Copyright 2016 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 "ui/arc/notification/arc_notification_manager.h"

#include <memory>
#include <utility>

#include "ash/shell.h"
#include "ash/system/toast/toast_manager.h"
#include "base/memory/ptr_util.h"
#include "base/memory/singleton.h"
#include "base/stl_util.h"
#include "base/strings/utf_string_conversions.h"
#include "components/arc/arc_bridge_service.h"
#include "components/arc/arc_browser_context_keyed_service_factory_base.h"
#include "components/arc/arc_util.h"
#include "ui/arc/notification/arc_notification_delegate.h"
#include "ui/arc/notification/arc_notification_item_impl.h"
#include "ui/arc/notification/arc_notification_view.h"
#include "ui/message_center/views/message_view_factory.h"

namespace arc {
namespace {

constexpr char kPlayStorePackageName[] = "com.android.vending";

std::unique_ptr<message_center::MessageView> CreateCustomMessageView(
    const message_center::Notification& notification) {
  DCHECK_EQ(notification.notifier_id().type,
            message_center::NotifierId::ARC_APPLICATION);
  auto* arc_delegate =
      static_cast<ArcNotificationDelegate*>(notification.delegate());
  return arc_delegate->CreateCustomMessageView(notification);
}

// Singleton factory for ArcNotificationManager.
class ArcNotificationManagerFactory
    : public internal::ArcBrowserContextKeyedServiceFactoryBase<
          ArcNotificationManager,
          ArcNotificationManagerFactory> {
 public:
  // Factory name used by ArcBrowserContextKeyedServiceFactoryBase.
  static constexpr const char* kName = "ArcNotificationManagerFactory";

  static ArcNotificationManagerFactory* GetInstance() {
    return base::Singleton<ArcNotificationManagerFactory>::get();
  }

 private:
  friend base::DefaultSingletonTraits<ArcNotificationManagerFactory>;
  ArcNotificationManagerFactory() = default;
  ~ArcNotificationManagerFactory() override = default;
};

}  // namespace

// static
ArcNotificationManager* ArcNotificationManager::GetForBrowserContext(
    content::BrowserContext* context) {
  return ArcNotificationManagerFactory::GetForBrowserContext(context);
}

// static
std::unique_ptr<ArcNotificationManager>
ArcNotificationManager::CreateForTesting(
    ArcBridgeService* bridge_service,
    const AccountId& main_profile_id,
    message_center::MessageCenter* message_center) {
  // MakeUnique cannot be used because the used ctor is private.
  return base::WrapUnique(new ArcNotificationManager(
      bridge_service, main_profile_id, message_center));
}

// static
void ArcNotificationManager::SetCustomNotificationViewFactory() {
  message_center::MessageViewFactory::SetCustomNotificationViewFactory(
      base::Bind(&CreateCustomMessageView));
}

ArcNotificationManager::ArcNotificationManager(content::BrowserContext* context,
                                               ArcBridgeService* bridge_service)
    : ArcNotificationManager(bridge_service,
                             ArcServiceManager::Get()->account_id(),
                             message_center::MessageCenter::Get()) {}

ArcNotificationManager::ArcNotificationManager(
    ArcBridgeService* bridge_service,
    const AccountId& main_profile_id,
    message_center::MessageCenter* message_center)
    : arc_bridge_service_(bridge_service),
      main_profile_id_(main_profile_id),
      message_center_(message_center) {
  arc_bridge_service_->notifications()->SetHost(this);
  arc_bridge_service_->notifications()->AddObserver(this);
  if (!message_center::MessageViewFactory::HasCustomNotificationViewFactory())
    SetCustomNotificationViewFactory();
}

ArcNotificationManager::~ArcNotificationManager() {
  arc_bridge_service_->notifications()->RemoveObserver(this);
  arc_bridge_service_->notifications()->SetHost(nullptr);
}

void ArcNotificationManager::OnConnectionReady() {
  DCHECK(!ready_);
  // TODO(hidehiko): Replace this by ConnectionHolder::IsConnected().
  ready_ = true;
}

void ArcNotificationManager::OnConnectionClosed() {
  DCHECK(ready_);
  while (!items_.empty()) {
    auto it = items_.begin();
    std::unique_ptr<ArcNotificationItem> item = std::move(it->second);
    items_.erase(it);
    item->OnClosedFromAndroid();
  }
  ready_ = false;
}

void ArcNotificationManager::OnNotificationPosted(
    mojom::ArcNotificationDataPtr data) {
  if (ShouldIgnoreNotification(data.get())) {
    VLOG(3) << "Posted notification was ignored.";
    return;
  }

  const std::string& key = data->key;
  auto it = items_.find(key);
  if (it == items_.end()) {
    // Show a notification on the primary logged-in user's desktop.
    // TODO(yoshiki): Reconsider when ARC supports multi-user.
    auto item = std::make_unique<ArcNotificationItemImpl>(
        this, message_center_, key, main_profile_id_);
    // TODO(yoshiki): Use emplacement for performance when it's available.
    auto result = items_.insert(std::make_pair(key, std::move(item)));
    DCHECK(result.second);
    it = result.first;
  }
  it->second->OnUpdatedFromAndroid(std::move(data));
}

void ArcNotificationManager::OnNotificationUpdated(
    mojom::ArcNotificationDataPtr data) {
  if (ShouldIgnoreNotification(data.get())) {
    VLOG(3) << "Updated notification was ignored.";
    return;
  }

  const std::string& key = data->key;
  auto it = items_.find(key);
  if (it == items_.end())
    return;

  it->second->OnUpdatedFromAndroid(std::move(data));
}

void ArcNotificationManager::OnNotificationRemoved(const std::string& key) {
  auto it = items_.find(key);
  if (it == items_.end()) {
    VLOG(3) << "Android requests to remove a notification (key: " << key
            << "), but it is already gone.";
    return;
  }

  std::unique_ptr<ArcNotificationItem> item = std::move(it->second);
  items_.erase(it);
  item->OnClosedFromAndroid();
}

void ArcNotificationManager::SendNotificationRemovedFromChrome(
    const std::string& key) {
  auto it = items_.find(key);
  if (it == items_.end()) {
    VLOG(3) << "Chrome requests to remove a notification (key: " << key
            << "), but it is already gone.";
    return;
  }

  // The removed ArcNotificationItem needs to live in this scope, since the
  // given argument |key| may be a part of the removed item.
  std::unique_ptr<ArcNotificationItem> item = std::move(it->second);
  items_.erase(it);

  auto* notifications_instance = ARC_GET_INSTANCE_FOR_METHOD(
      arc_bridge_service_->notifications(), SendNotificationEventToAndroid);

  // On shutdown, the ARC channel may quit earlier than notifications.
  if (!notifications_instance) {
    VLOG(2) << "ARC Notification (key: " << key
            << ") is closed, but the ARC channel has already gone.";
    return;
  }

  notifications_instance->SendNotificationEventToAndroid(
      key, mojom::ArcNotificationEvent::CLOSED);
}

void ArcNotificationManager::SendNotificationClickedOnChrome(
    const std::string& key) {
  if (items_.find(key) == items_.end()) {
    VLOG(3) << "Chrome requests to fire a click event on notification (key: "
            << key << "), but it is gone.";
    return;
  }

  auto* notifications_instance = ARC_GET_INSTANCE_FOR_METHOD(
      arc_bridge_service_->notifications(), SendNotificationEventToAndroid);

  // On shutdown, the ARC channel may quit earlier than notifications.
  if (!notifications_instance) {
    VLOG(2) << "ARC Notification (key: " << key
            << ") is clicked, but the ARC channel has already gone.";
    return;
  }

  notifications_instance->SendNotificationEventToAndroid(
      key, mojom::ArcNotificationEvent::BODY_CLICKED);
}

void ArcNotificationManager::SendNotificationButtonClickedOnChrome(
    const std::string& key,
    int button_index) {
  if (items_.find(key) == items_.end()) {
    VLOG(3) << "Chrome requests to fire a click event on notification (key: "
            << key << "), but it is gone.";
    return;
  }

  auto* notifications_instance = ARC_GET_INSTANCE_FOR_METHOD(
      arc_bridge_service_->notifications(), SendNotificationEventToAndroid);

  // On shutdown, the ARC channel may quit earlier than notifications.
  if (!notifications_instance) {
    VLOG(2) << "ARC Notification (key: " << key
            << ")'s button is clicked, but the ARC channel has already gone.";
    return;
  }

  mojom::ArcNotificationEvent command;
  switch (button_index) {
    case 0:
      command = mojom::ArcNotificationEvent::BUTTON_1_CLICKED;
      break;
    case 1:
      command = mojom::ArcNotificationEvent::BUTTON_2_CLICKED;
      break;
    case 2:
      command = mojom::ArcNotificationEvent::BUTTON_3_CLICKED;
      break;
    case 3:
      command = mojom::ArcNotificationEvent::BUTTON_4_CLICKED;
      break;
    case 4:
      command = mojom::ArcNotificationEvent::BUTTON_5_CLICKED;
      break;
    default:
      VLOG(3) << "Invalid button index (key: " << key
              << ", index: " << button_index << ").";
      return;
  }

  notifications_instance->SendNotificationEventToAndroid(key, command);
}

void ArcNotificationManager::CreateNotificationWindow(const std::string& key) {
  if (items_.find(key) == items_.end()) {
    VLOG(3) << "Chrome requests to create window on notification (key: " << key
            << "), but it is gone.";
    return;
  }

  auto* notifications_instance = ARC_GET_INSTANCE_FOR_METHOD(
      arc_bridge_service_->notifications(), CreateNotificationWindow);
  if (!notifications_instance)
    return;

  notifications_instance->CreateNotificationWindow(key);
}

void ArcNotificationManager::CloseNotificationWindow(const std::string& key) {
  if (items_.find(key) == items_.end()) {
    VLOG(3) << "Chrome requests to close window on notification (key: " << key
            << "), but it is gone.";
    return;
  }

  auto* notifications_instance = ARC_GET_INSTANCE_FOR_METHOD(
      arc_bridge_service_->notifications(), CloseNotificationWindow);
  if (!notifications_instance)
    return;

  notifications_instance->CloseNotificationWindow(key);
}

void ArcNotificationManager::OpenNotificationSettings(const std::string& key) {
  if (items_.find(key) == items_.end()) {
    DVLOG(3) << "Chrome requests to fire a click event on the notification "
             << "settings button (key: " << key << "), but it is gone.";
    return;
  }

  auto* notifications_instance = ARC_GET_INSTANCE_FOR_METHOD(
      arc_bridge_service_->notifications(), OpenNotificationSettings);

  // On shutdown, the ARC channel may quit earlier than notifications.
  if (!notifications_instance)
    return;

  notifications_instance->OpenNotificationSettings(key);
}

bool ArcNotificationManager::IsOpeningSettingsSupported() const {
  const auto* notifications_instance = ARC_GET_INSTANCE_FOR_METHOD(
      arc_bridge_service_->notifications(), OpenNotificationSettings);
  return notifications_instance != nullptr;
}

void ArcNotificationManager::SendNotificationToggleExpansionOnChrome(
    const std::string& key) {
  if (items_.find(key) == items_.end()) {
    VLOG(3) << "Chrome requests to fire a click event on notification (key: "
            << key << "), but it is gone.";
    return;
  }

  auto* notifications_instance = ARC_GET_INSTANCE_FOR_METHOD(
      arc_bridge_service_->notifications(), SendNotificationEventToAndroid);

  // On shutdown, the ARC channel may quit earlier than notifications.
  if (!notifications_instance) {
    VLOG(2) << "ARC Notification (key: " << key
            << ") is clicked, but the ARC channel has already gone.";
    return;
  }

  notifications_instance->SendNotificationEventToAndroid(
      key, mojom::ArcNotificationEvent::TOGGLE_EXPANSION);
}

void ArcNotificationManager::OnToastPosted(mojom::ArcToastDataPtr data) {
  const base::string16 text16(
      base::UTF8ToUTF16(data->text.has_value() ? *data->text : std::string()));
  const base::string16 dismiss_text16(base::UTF8ToUTF16(
      data->dismiss_text.has_value() ? *data->dismiss_text : std::string()));
  ash::Shell::Get()->toast_manager()->Show(
      ash::ToastData(data->id, text16, data->duration, dismiss_text16));
}

void ArcNotificationManager::OnToastCancelled(mojom::ArcToastDataPtr data) {
  ash::Shell::Get()->toast_manager()->Cancel(data->id);
}

bool ArcNotificationManager::ShouldIgnoreNotification(
    arc::mojom::ArcNotificationData* data) {
  // Notifications from Play Store are ignored in Public Session and Kiosk mode.
  // TODO: Use centralized const for Play Store package.
  return data->package_name.has_value() &&
         *data->package_name == kPlayStorePackageName && IsRobotAccountMode();
}

}  // namespace arc
