// 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/chromeos/extensions/file_browser_notifications.h"

#include "base/bind.h"
#include "base/message_loop.h"
#include "base/stl_util.h"
#include "base/string_number_conversions.h"
#include "base/utf_string_conversions.h"
#include "chrome/browser/notifications/desktop_notification_service.h"
#include "chrome/browser/notifications/notification_delegate.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_list.h"
#include "chrome/browser/ui/webui/web_ui_util.h"
#include "content/public/browser/browser_thread.h"
#include "grit/generated_resources.h"
#include "grit/theme_resources.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/resource/resource_bundle.h"

namespace {

int GetIconId(FileBrowserNotifications::NotificationType type) {
  switch (type) {
    case FileBrowserNotifications::DEVICE:
    case FileBrowserNotifications::FORMAT_SUCCESS:
    case FileBrowserNotifications::FORMAT_START:
        return IDR_PAGEINFO_INFO;
    case FileBrowserNotifications::DEVICE_FAIL:
    case FileBrowserNotifications::FORMAT_START_FAIL:
    case FileBrowserNotifications::FORMAT_FAIL:
        return IDR_PAGEINFO_WARNING_MAJOR;
    default:
      NOTREACHED();
      return 0;
  }
}

string16 GetTitle(FileBrowserNotifications::NotificationType type) {
  int id;
  switch (type) {
    case FileBrowserNotifications::DEVICE:
    case FileBrowserNotifications::DEVICE_FAIL:
      id = IDS_REMOVABLE_DEVICE_DETECTION_TITLE;
      break;
    case FileBrowserNotifications::FORMAT_START:
      id = IDS_FORMATTING_OF_DEVICE_PENDING_TITLE;
      break;
    case FileBrowserNotifications::FORMAT_START_FAIL:
    case FileBrowserNotifications::FORMAT_SUCCESS:
    case FileBrowserNotifications::FORMAT_FAIL:
      id = IDS_FORMATTING_OF_DEVICE_FINISHED_TITLE;
      break;
    default:
      NOTREACHED();
      id = 0;
  }
  return l10n_util::GetStringUTF16(id);
}

string16 GetMessage(FileBrowserNotifications::NotificationType type) {
  int id;
  switch (type) {
    case FileBrowserNotifications::DEVICE:
      id = IDS_REMOVABLE_DEVICE_SCANNING_MESSAGE;
      break;
    case FileBrowserNotifications::DEVICE_FAIL:
      id = IDS_DEVICE_UNSUPPORTED_DEFAULT_MESSAGE;
      break;
    case FileBrowserNotifications::FORMAT_FAIL:
      id = IDS_FORMATTING_FINISHED_FAILURE_MESSAGE;
      break;
    case FileBrowserNotifications::FORMAT_SUCCESS:
      id = IDS_FORMATTING_FINISHED_SUCCESS_MESSAGE;
      break;
    case FileBrowserNotifications::FORMAT_START:
      id = IDS_FORMATTING_OF_DEVICE_PENDING_MESSAGE;
      break;
    case FileBrowserNotifications::FORMAT_START_FAIL:
      id = IDS_FORMATTING_STARTED_FAILURE_MESSAGE;
      break;
    default:
      NOTREACHED();
      id = 0;
  }
  return l10n_util::GetStringUTF16(id);
}

}  // namespace

// Manages file browser notifications. Generates a desktop notification on
// construction and removes it from the host when closed. Owned by the host.
class FileBrowserNotifications::NotificationMessage {
 public:
  class Delegate : public NotificationDelegate {
   public:
    Delegate(const base::WeakPtr<FileBrowserNotifications>& host,
             const std::string& id)
        : host_(host),
          id_(id) {}
    virtual void Display() OVERRIDE {}
    virtual void Error() OVERRIDE {}
    virtual void Close(bool by_user) OVERRIDE {
      if (host_)
        host_->RemoveNotificationById(id_);
    }
    virtual void Click() OVERRIDE {
      // TODO(tbarzic): Show more info page once we have one.
    }
    virtual std::string id() const OVERRIDE { return id_; }
    virtual content::RenderViewHost* GetRenderViewHost() const OVERRIDE {
      return NULL;
    }

   private:
    virtual ~Delegate() {}

    base::WeakPtr<FileBrowserNotifications> host_;
    std::string id_;

    DISALLOW_COPY_AND_ASSIGN(Delegate);
  };

  NotificationMessage(FileBrowserNotifications* host,
                      Profile* profile,
                      NotificationType type,
                      const std::string& notification_id,
                      const string16& message)
      : profile_(profile),
        type_(type),
        notification_id_(notification_id),
        message_(message) {
    const gfx::ImageSkia& icon =
        *ResourceBundle::GetSharedInstance().GetImageSkiaNamed(
            IDR_FILES_APP_ICON);
    const string16 replace_id = UTF8ToUTF16(notification_id_);
    DesktopNotificationService::AddIconNotification(
        GURL(), GetTitle(type_), message, icon, replace_id,
        new Delegate(host->AsWeakPtr(), notification_id_), profile_);
  }

  ~NotificationMessage() {
  }

 private:
  Profile* profile_;
  NotificationType type_;
  std::string notification_id_;
  string16 message_;

  DISALLOW_COPY_AND_ASSIGN(NotificationMessage);
};

struct FileBrowserNotifications::MountRequestsInfo {
  bool mount_success_exists;
  bool fail_message_finalized;
  bool fail_notification_shown;
  bool non_parent_device_failed;
  bool device_notification_hidden;
  int fail_notifications_count;

  MountRequestsInfo() : mount_success_exists(false),
                        fail_message_finalized(false),
                        fail_notification_shown(false),
                        non_parent_device_failed(false),
                        device_notification_hidden(false),
                        fail_notifications_count(0) {
  }
};

FileBrowserNotifications::FileBrowserNotifications(Profile* profile)
    : profile_(profile) {
}

FileBrowserNotifications::~FileBrowserNotifications() {
  STLDeleteContainerPairSecondPointers(notification_map_.begin(),
                                       notification_map_.end());
}

void FileBrowserNotifications::RegisterDevice(const std::string& path) {
  mount_requests_.insert(MountRequestsMap::value_type(path,
                                                      MountRequestsInfo()));
}

void FileBrowserNotifications::UnregisterDevice(const std::string& path) {
  mount_requests_.erase(path);
}

void FileBrowserNotifications::ManageNotificationsOnMountCompleted(
    const std::string& system_path, const std::string& label, bool is_parent,
    bool success, bool is_unsupported) {
  MountRequestsMap::iterator it = mount_requests_.find(system_path);
  if (it == mount_requests_.end())
    return;

  // We have to hide device scanning notification if we haven't done it already.
  if (!it->second.device_notification_hidden) {
    HideNotification(DEVICE, system_path);
    it->second.device_notification_hidden = true;
  }

  // Check if there is fail notification for parent device. If so, disregard it.
  // (parent device contains partition table, which is unmountable).
  if (!is_parent && it->second.fail_notification_shown &&
      !it->second.non_parent_device_failed) {
    HideNotification(DEVICE_FAIL, system_path);
    it->second.fail_notification_shown = false;
  }

  // If notificaiton can't change any more, no need to continue.
  if (it->second.fail_message_finalized)
    return;

  int notification_message_id = 0;

  // Do we have a multi-partition device for which at least one mount failed.
  bool fail_on_multipartition_device =
      success ? it->second.non_parent_device_failed
              : it->second.mount_success_exists ||
                it->second.non_parent_device_failed;

  if (fail_on_multipartition_device) {
    it->second.fail_message_finalized = true;
    notification_message_id =
        label.empty() ? IDS_MULTIPART_DEVICE_UNSUPPORTED_DEFAULT_MESSAGE
                      : IDS_MULTIPART_DEVICE_UNSUPPORTED_MESSAGE;
  } else if (!success) {
    // First device failed.
    if (!is_unsupported) {
      notification_message_id =
          label.empty() ? IDS_DEVICE_UNKNOWN_DEFAULT_MESSAGE
                        : IDS_DEVICE_UNKNOWN_MESSAGE;
    } else {
      notification_message_id =
          label.empty() ? IDS_DEVICE_UNSUPPORTED_DEFAULT_MESSAGE
                        : IDS_DEVICE_UNSUPPORTED_MESSAGE;
    }
  }

  if (success) {
    it->second.mount_success_exists = true;
  } else {
    it->second.non_parent_device_failed |= !is_parent;
  }

  if (notification_message_id == 0)
    return;

  if (it->second.fail_notification_shown) {
    HideNotification(DEVICE_FAIL, system_path);
  } else {
    it->second.fail_notification_shown = true;
  }

  it->second.fail_notifications_count++;

  if (!label.empty()) {
    ShowNotificationWithMessage(DEVICE_FAIL, system_path,
        l10n_util::GetStringFUTF16(notification_message_id,
                                   ASCIIToUTF16(label)));
  } else {
    ShowNotificationWithMessage(DEVICE_FAIL, system_path,
        l10n_util::GetStringUTF16(notification_message_id));
  }
}

void FileBrowserNotifications::ShowNotification(NotificationType type,
                                                const std::string& path) {
  ShowNotificationWithMessage(type, path, GetMessage(type));
}

void FileBrowserNotifications::ShowNotificationWithMessage(
    NotificationType type,
    const std::string& path,
    const string16& message) {
  std::string notification_id = CreateNotificationId(type, path);
  hidden_notifications_.erase(notification_id);
  ShowNotificationById(type, notification_id, message);
}

void FileBrowserNotifications::ShowNotificationDelayed(
    NotificationType type,
    const std::string& path,
    base::TimeDelta delay) {
  std::string notification_id = CreateNotificationId(type, path);
  hidden_notifications_.erase(notification_id);
  MessageLoop::current()->PostDelayedTask(
      FROM_HERE,
      base::Bind(&FileBrowserNotifications::ShowNotificationById, AsWeakPtr(),
                 type, notification_id, GetMessage(type)),
      delay);
}

void FileBrowserNotifications::HideNotification(NotificationType type,
                                                const std::string& path) {
  std::string notification_id = CreateNotificationId(type, path);
  HideNotificationById(notification_id);
}

void FileBrowserNotifications::HideNotificationDelayed(
    NotificationType type, const std::string& path, base::TimeDelta delay) {
  MessageLoop::current()->PostDelayedTask(
      FROM_HERE,
      base::Bind(&FileBrowserNotifications::HideNotification, AsWeakPtr(),
                 type, path),
      delay);
}

std::string FileBrowserNotifications::CreateNotificationId(
    NotificationType type,
    const std::string& path) {
  std::string id;
  switch (type) {
    case DEVICE:
      id = "D";
      break;
    case DEVICE_FAIL:
      id = "DF";
      break;
    case FORMAT_START:
      id = "FS";
      break;
    default:
      id = "FF";
  }

  if (type == DEVICE_FAIL) {
    MountRequestsMap::const_iterator it = mount_requests_.find(path);
    if (it != mount_requests_.end())
      id.append(base::IntToString(it->second.fail_notifications_count));
  }

  id.append(path);
  return id;
}

void FileBrowserNotifications::ShowNotificationById(
    NotificationType type,
    const std::string& notification_id,
    const string16& message) {
  if (hidden_notifications_.find(notification_id) !=
      hidden_notifications_.end()) {
    // Notification was hidden after a delayed show was requested.
    hidden_notifications_.erase(notification_id);
    return;
  }
  if (notification_map_.find(notification_id) != notification_map_.end()) {
    // Remove any existing notification with |notification_id|.
    // Will trigger Delegate::Close which will call RemoveNotificationById.
    DesktopNotificationService::RemoveNotification(notification_id);
    DCHECK(notification_map_.find(notification_id) == notification_map_.end());
  }
  // Create a new notification with |notification_id|.
  NotificationMessage* new_message =
      new NotificationMessage(this, profile_, type, notification_id, message);
  notification_map_[notification_id] = new_message;
}

void FileBrowserNotifications::HideNotificationById(
    const std::string& notification_id) {
  NotificationMap::iterator it = notification_map_.find(notification_id);
  if (it != notification_map_.end()) {
    // Will trigger Delegate::Close which will call RemoveNotificationById.
    DesktopNotificationService::RemoveNotification(notification_id);
  } else {
    // Mark as hidden so it does not get shown from a delayed task.
    hidden_notifications_.insert(notification_id);
  }
}

void FileBrowserNotifications::RemoveNotificationById(
    const std::string& notification_id) {
  NotificationMap::iterator it = notification_map_.find(notification_id);
  if (it != notification_map_.end()) {
    NotificationMessage* notification = it->second;
    notification_map_.erase(it);
    delete notification;
  }
}