// 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/extensions/api/runtime/runtime_api.h"

#include "base/memory/scoped_ptr.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/extensions/event_router.h"
#include "chrome/browser/extensions/extension_host.h"
#include "chrome/browser/extensions/extension_process_manager.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/extensions/extension_system.h"
#include "chrome/browser/extensions/lazy_background_task_queue.h"
#include "chrome/browser/extensions/updater/extension_updater.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/ui/browser_finder.h"
#include "chrome/browser/ui/browser_navigator.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/common/extensions/api/runtime.h"
#include "chrome/common/extensions/background_info.h"
#include "chrome/common/extensions/extension.h"
#include "chrome/common/omaha_query_params/omaha_query_params.h"
#include "extensions/common/error_utils.h"
#include "googleurl/src/gurl.h"

namespace GetPlatformInfo = extensions::api::runtime::GetPlatformInfo;

namespace extensions {

namespace {

const char kOnStartupEvent[] = "runtime.onStartup";
const char kOnInstalledEvent[] = "runtime.onInstalled";
const char kOnUpdateAvailableEvent[] = "runtime.onUpdateAvailable";
const char kOnBrowserUpdateAvailableEvent[] =
    "runtime.onBrowserUpdateAvailable";
const char kNoBackgroundPageError[] = "You do not have a background page.";
const char kPageLoadError[] = "Background page failed to load.";
const char kInstallReason[] = "reason";
const char kInstallReasonChromeUpdate[] = "chrome_update";
const char kInstallReasonUpdate[] = "update";
const char kInstallReasonInstall[] = "install";
const char kInstallPreviousVersion[] = "previousVersion";
const char kInvalidUrlError[] = "Invalid URL.";
const char kUpdatesDisabledError[] = "Autoupdate is not enabled.";
const char kUpdateFound[] = "update_available";
const char kUpdateNotFound[] = "no_update";
const char kUpdateThrottled[] = "throttled";

// A preference key storing the url loaded when an extension is uninstalled.
const char kUninstallUrl[] = "uninstall_url";

static void DispatchOnStartupEventImpl(
    Profile* profile,
    const std::string& extension_id,
    bool first_call,
    ExtensionHost* host) {
  // A NULL host from the LazyBackgroundTaskQueue means the page failed to
  // load. Give up.
  if (!host && !first_call)
    return;

  // Don't send onStartup events to incognito profiles.
  if (profile->IsOffTheRecord())
    return;

  if (g_browser_process->IsShuttingDown() ||
      !g_browser_process->profile_manager()->IsValidProfile(profile))
    return;
  ExtensionSystem* system = ExtensionSystem::Get(profile);
  if (!system)
    return;

  // If this is a persistent background page, we want to wait for it to load
  // (it might not be ready, since this is startup). But only enqueue once.
  // If it fails to load the first time, don't bother trying again.
  const Extension* extension =
      system->extension_service()->extensions()->GetByID(extension_id);
  if (extension && BackgroundInfo::HasPersistentBackgroundPage(extension) &&
      first_call &&
      system->lazy_background_task_queue()->
          ShouldEnqueueTask(profile, extension)) {
    system->lazy_background_task_queue()->AddPendingTask(
        profile, extension_id,
        base::Bind(&DispatchOnStartupEventImpl,
                   profile, extension_id, false));
    return;
  }

  scoped_ptr<base::ListValue> event_args(new ListValue());
  scoped_ptr<Event> event(new Event(kOnStartupEvent, event_args.Pass()));
  system->event_router()->DispatchEventToExtension(extension_id, event.Pass());
}

void SetUninstallUrl(ExtensionPrefs* prefs,
                     const std::string& extension_id,
                     const std::string& url_string) {
  prefs->UpdateExtensionPref(extension_id,
                             kUninstallUrl,
                             base::Value::CreateStringValue(url_string));
}

std::string GetUninstallUrl(ExtensionPrefs* prefs,
                            const std::string& extension_id) {
  std::string url_string;
  prefs->ReadPrefAsString(extension_id, kUninstallUrl, &url_string);
  return url_string;
}

}  // namespace

// static
void RuntimeEventRouter::DispatchOnStartupEvent(
    Profile* profile, const std::string& extension_id) {
  DispatchOnStartupEventImpl(profile, extension_id, true, NULL);
}

// static
void RuntimeEventRouter::DispatchOnInstalledEvent(
    Profile* profile,
    const std::string& extension_id,
    const Version& old_version,
    bool chrome_updated) {
  ExtensionSystem* system = ExtensionSystem::Get(profile);
  if (!system)
    return;

  // Special case: normally, extensions add their own lazy event listeners.
  // However, since the extension has just been installed, it hasn't had a
  // chance to register for events. So we register on its behalf. If the
  // extension does not actually have a listener, the event will just be
  // ignored.
  scoped_ptr<base::ListValue> event_args(new ListValue());
  base::DictionaryValue* info = new base::DictionaryValue();
  event_args->Append(info);
  if (old_version.IsValid()) {
    info->SetString(kInstallReason, kInstallReasonUpdate);
    info->SetString(kInstallPreviousVersion, old_version.GetString());
  } else if (chrome_updated) {
    info->SetString(kInstallReason, kInstallReasonChromeUpdate);
  } else {
    info->SetString(kInstallReason, kInstallReasonInstall);
  }
  DCHECK(system->event_router());
  system->event_router()->AddLazyEventListener(kOnInstalledEvent, extension_id);
  scoped_ptr<Event> event(new Event(kOnInstalledEvent, event_args.Pass()));
  system->event_router()->DispatchEventToExtension(extension_id, event.Pass());
  system->event_router()->RemoveLazyEventListener(kOnInstalledEvent,
                                                  extension_id);
}

// static
void RuntimeEventRouter::DispatchOnUpdateAvailableEvent(
    Profile* profile,
    const std::string& extension_id,
    const DictionaryValue* manifest) {
  ExtensionSystem* system = ExtensionSystem::Get(profile);
  if (!system)
    return;

  scoped_ptr<ListValue> args(new ListValue);
  args->Append(manifest->DeepCopy());
  DCHECK(system->event_router());
  scoped_ptr<Event> event(new Event(kOnUpdateAvailableEvent, args.Pass()));
  system->event_router()->DispatchEventToExtension(extension_id, event.Pass());
}

// static
void RuntimeEventRouter::DispatchOnBrowserUpdateAvailableEvent(
    Profile* profile) {
  ExtensionSystem* system = ExtensionSystem::Get(profile);
  if (!system)
    return;

  scoped_ptr<ListValue> args(new ListValue);
  DCHECK(system->event_router());
  scoped_ptr<Event> event(new Event(kOnBrowserUpdateAvailableEvent,
                                    args.Pass()));
  system->event_router()->BroadcastEvent(event.Pass());
}

// static
void RuntimeEventRouter::OnExtensionUninstalled(
    Profile *profile,
    const std::string& extension_id) {
#if defined(ENABLE_EXTENSIONS)
  GURL uninstall_url(GetUninstallUrl(ExtensionPrefs::Get(profile),
                                     extension_id));

  if (uninstall_url.is_empty())
    return;

  Browser* browser = chrome::FindLastActiveWithProfile(profile,
      chrome::GetActiveDesktop());
  if (!browser)
    browser = new Browser(Browser::CreateParams(profile,
                                                chrome::GetActiveDesktop()));

  chrome::NavigateParams params(browser, uninstall_url,
                                content::PAGE_TRANSITION_CLIENT_REDIRECT);
  params.disposition = NEW_FOREGROUND_TAB;
  params.user_gesture = false;
  chrome::Navigate(&params);
#endif  // defined(ENABLE_EXTENSIONS)
}

bool RuntimeGetBackgroundPageFunction::RunImpl() {
  ExtensionSystem* system = ExtensionSystem::Get(profile());
  ExtensionHost* host = system->process_manager()->
      GetBackgroundHostForExtension(extension_id());
  if (system->lazy_background_task_queue()->ShouldEnqueueTask(
          profile(), GetExtension())) {
    system->lazy_background_task_queue()->AddPendingTask(
       profile(), extension_id(),
       base::Bind(&RuntimeGetBackgroundPageFunction::OnPageLoaded, this));
  } else if (host) {
    OnPageLoaded(host);
  } else {
    error_ = kNoBackgroundPageError;
    return false;
  }

  return true;
}

void RuntimeGetBackgroundPageFunction::OnPageLoaded(ExtensionHost* host) {
  if (host) {
    SendResponse(true);
  } else {
    error_ = kPageLoadError;
    SendResponse(false);
  }
}

bool RuntimeSetUninstallUrlFunction::RunImpl() {
  std::string url_string;
  EXTENSION_FUNCTION_VALIDATE(args_->GetString(0, &url_string));

  GURL url(url_string);
  if (!url.is_valid()) {
    error_ = ErrorUtils::FormatErrorMessage(kInvalidUrlError, url_string);
    return false;
  }

  SetUninstallUrl(ExtensionPrefs::Get(profile()), extension_id(), url_string);
  return true;
}

bool RuntimeReloadFunction::RunImpl() {
  // We can't call ReloadExtension directly, since when this method finishes
  // it tries to decrease the reference count for the extension, which fails
  // if the extension has already been reloaded; so instead we post a task.
  base::MessageLoop::current()->PostTask(FROM_HERE,
      base::Bind(&ExtensionService::ReloadExtension,
                 profile()->GetExtensionService()->AsWeakPtr(),
                 extension_id()));
  return true;
}

RuntimeRequestUpdateCheckFunction::RuntimeRequestUpdateCheckFunction() {
  registrar_.Add(this, chrome::NOTIFICATION_EXTENSION_UPDATE_FOUND,
                 content::NotificationService::AllSources());
}

bool RuntimeRequestUpdateCheckFunction::RunImpl() {
  ExtensionSystem* system = ExtensionSystem::Get(profile());
  ExtensionService* service = system->extension_service();
  ExtensionUpdater* updater = service->updater();
  if (!updater) {
    error_ = kUpdatesDisabledError;
    return false;
  }

  did_reply_ = false;
  if (!updater->CheckExtensionSoon(extension_id(), base::Bind(
      &RuntimeRequestUpdateCheckFunction::CheckComplete, this))) {
    did_reply_ = true;
    SetResult(new base::StringValue(kUpdateThrottled));
    SendResponse(true);
  }
  return true;
}

void RuntimeRequestUpdateCheckFunction::CheckComplete() {
  if (did_reply_)
    return;

  did_reply_ = true;

  // Since no UPDATE_FOUND notification was seen, this generally would mean
  // that no update is found, but a previous update check might have already
  // queued up an update, so check for that here to make sure we return the
  // right value.
  ExtensionSystem* system = ExtensionSystem::Get(profile());
  ExtensionService* service = system->extension_service();
  const Extension* update = service->GetPendingExtensionUpdate(extension_id());
  if (update) {
    ReplyUpdateFound(update->VersionString());
  } else {
    SetResult(new base::StringValue(kUpdateNotFound));
  }
  SendResponse(true);
}

void RuntimeRequestUpdateCheckFunction::Observe(
    int type,
    const content::NotificationSource& source,
    const content::NotificationDetails& details) {
  if (did_reply_)
    return;

  DCHECK(type == chrome::NOTIFICATION_EXTENSION_UPDATE_FOUND);
  typedef const std::pair<std::string, Version> UpdateDetails;
  const std::string& id = content::Details<UpdateDetails>(details)->first;
  const Version& version = content::Details<UpdateDetails>(details)->second;
  if (id == extension_id()) {
    ReplyUpdateFound(version.GetString());
  }
}

void RuntimeRequestUpdateCheckFunction::ReplyUpdateFound(
    const std::string& version) {
  did_reply_ = true;
  results_.reset(new base::ListValue);
  results_->AppendString(kUpdateFound);
  base::DictionaryValue* details = new base::DictionaryValue;
  results_->Append(details);
  details->SetString("version", version);
  SendResponse(true);
}

bool RuntimeGetPlatformInfoFunction::RunImpl() {
  GetPlatformInfo::Results::PlatformInfo info;

  const char* os = chrome::OmahaQueryParams::getOS();
  if (strcmp(os, "mac") == 0) {
    info.os = GetPlatformInfo::Results::PlatformInfo::OS_MAC_;
  } else if (strcmp(os, "win") == 0) {
    info.os = GetPlatformInfo::Results::PlatformInfo::OS_WIN_;
  } else if (strcmp(os, "android") == 0) {
    info.os = GetPlatformInfo::Results::PlatformInfo::OS_ANDROID_;
  } else if (strcmp(os, "cros") == 0) {
    info.os = GetPlatformInfo::Results::PlatformInfo::OS_CROS_;
  } else if (strcmp(os, "linux") == 0) {
    info.os = GetPlatformInfo::Results::PlatformInfo::OS_LINUX_;
  } else if (strcmp(os, "openbsd") == 0) {
    info.os = GetPlatformInfo::Results::PlatformInfo::OS_OPENBSD_;
  } else {
    NOTREACHED();
    return false;
  }

  const char* arch = chrome::OmahaQueryParams::getArch();
  if (strcmp(arch, "arm") == 0) {
    info.arch = GetPlatformInfo::Results::PlatformInfo::ARCH_ARM;
  } else if (strcmp(arch, "x86") == 0) {
    info.arch = GetPlatformInfo::Results::PlatformInfo::ARCH_X86_32;
  } else if (strcmp(arch, "x64") == 0) {
    info.arch = GetPlatformInfo::Results::PlatformInfo::ARCH_X86_64;
  } else {
    NOTREACHED();
    return false;
  }

  const char* nacl_arch = chrome::OmahaQueryParams::getNaclArch();
  if (strcmp(nacl_arch, "arm") == 0) {
    info.nacl_arch = GetPlatformInfo::Results::PlatformInfo::NACL_ARCH_ARM;
  } else if (strcmp(nacl_arch, "x86-32") == 0) {
    info.nacl_arch = GetPlatformInfo::Results::PlatformInfo::NACL_ARCH_X86_32;
  } else if (strcmp(nacl_arch, "x86-64") == 0) {
    info.nacl_arch = GetPlatformInfo::Results::PlatformInfo::NACL_ARCH_X86_64;
  } else {
    NOTREACHED();
    return false;
  }

  results_ = GetPlatformInfo::Results::Create(info);
  return true;
}

}   // namespace extensions