// Copyright 2013 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 "extensions/browser/lazy_background_task_queue.h"

#include "base/callback.h"
#include "base/logging.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/notification_service.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/browser/site_instance.h"
#include "content/public/browser/web_contents.h"
#include "extensions/browser/extension_host.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/extensions_browser_client.h"
#include "extensions/browser/lazy_background_task_queue_factory.h"
#include "extensions/browser/lazy_context_id.h"
#include "extensions/browser/notification_types.h"
#include "extensions/browser/process_manager.h"
#include "extensions/browser/process_map.h"
#include "extensions/common/extension.h"
#include "extensions/common/manifest_handlers/background_info.h"
#include "extensions/common/view_type.h"

namespace extensions {

namespace {

// Adapts a LazyBackgroundTaskQueue pending task callback to
// LazyContextTaskQueue's callback.
void PendingTaskAdapter(LazyContextTaskQueue::PendingTask original_task,
                        ExtensionHost* host) {
  if (!host) {
    std::move(original_task).Run(nullptr);
  } else {
    std::move(original_task)
        .Run(std::make_unique<LazyContextTaskQueue::ContextInfo>(
            host->extension()->id(), host->render_process_host(), kMainThreadId,
            host->GetURL()));
  }
}

// Attempts to create a background host for a lazy background page. Returns true
// if the background host is created.
bool CreateLazyBackgroundHost(ProcessManager* pm, const Extension* extension) {
  pm->IncrementLazyKeepaliveCount(extension);
  // Creating the background host may fail, e.g. if the extension isn't enabled
  // in incognito mode.
  return pm->CreateBackgroundHost(extension,
                                  BackgroundInfo::GetBackgroundURL(extension));
}

}  // namespace

LazyBackgroundTaskQueue::LazyBackgroundTaskQueue(
    content::BrowserContext* browser_context)
    : browser_context_(browser_context), extension_registry_observer_(this) {
  registrar_.Add(this,
                 extensions::NOTIFICATION_EXTENSION_HOST_DID_STOP_FIRST_LOAD,
                 content::NotificationService::AllBrowserContextsAndSources());
  registrar_.Add(this,
                 extensions::NOTIFICATION_EXTENSION_HOST_DESTROYED,
                 content::NotificationService::AllBrowserContextsAndSources());

  extension_registry_observer_.Add(ExtensionRegistry::Get(browser_context));
}

LazyBackgroundTaskQueue::~LazyBackgroundTaskQueue() {
}

// static
LazyBackgroundTaskQueue* LazyBackgroundTaskQueue::Get(
    content::BrowserContext* browser_context) {
  return LazyBackgroundTaskQueueFactory::GetForBrowserContext(browser_context);
}

bool LazyBackgroundTaskQueue::ShouldEnqueueTask(
    content::BrowserContext* browser_context,
    const Extension* extension) {
  // Note: browser_context may not be the same as browser_context_ for incognito
  // extension tasks.
  DCHECK(extension);
  if (BackgroundInfo::HasBackgroundPage(extension)) {
    ProcessManager* pm = ProcessManager::Get(browser_context);
    ExtensionHost* background_host =
        pm->GetBackgroundHostForExtension(extension->id());
    if (!background_host || !background_host->has_loaded_once())
      return true;
    if (pm->IsBackgroundHostClosing(extension->id()))
      pm->CancelSuspend(extension);
  }

  return false;
}

void LazyBackgroundTaskQueue::AddPendingTaskToDispatchEvent(
    LazyContextId* context_id,
    LazyContextTaskQueue::PendingTask task) {
  AddPendingTask(context_id->browser_context(), context_id->extension_id(),
                 base::BindOnce(&PendingTaskAdapter, std::move(task)));
}

void LazyBackgroundTaskQueue::AddPendingTask(
    content::BrowserContext* browser_context,
    const std::string& extension_id,
    PendingTask task) {
  if (ExtensionsBrowserClient::Get()->IsShuttingDown()) {
    std::move(task).Run(nullptr);
    return;
  }
  PendingTasksList* tasks_list = nullptr;
  PendingTasksKey key(browser_context, extension_id);
  PendingTasksMap::iterator it = pending_tasks_.find(key);
  if (it == pending_tasks_.end()) {
    const Extension* extension = ExtensionRegistry::Get(browser_context)
                                     ->enabled_extensions()
                                     .GetByID(extension_id);
    if (extension && BackgroundInfo::HasLazyBackgroundPage(extension)) {
      // If this is the first enqueued task, and we're not waiting for the
      // background page to unload, ensure the background page is loaded.
      if (!CreateLazyBackgroundHost(ProcessManager::Get(browser_context),
                                    extension)) {
        std::move(task).Run(nullptr);
        return;
      }
    }
    auto tasks_list_tmp = std::make_unique<PendingTasksList>();
    tasks_list = tasks_list_tmp.get();
    pending_tasks_[key] = std::move(tasks_list_tmp);
  } else {
    tasks_list = it->second.get();
  }

  tasks_list->push_back(std::move(task));
}

void LazyBackgroundTaskQueue::ProcessPendingTasks(
    ExtensionHost* host,
    content::BrowserContext* browser_context,
    const Extension* extension) {
  DCHECK(extension);

  if (!ExtensionsBrowserClient::Get()->IsSameContext(browser_context,
                                                     browser_context_))
    return;

  PendingTasksKey key(browser_context, extension->id());
  PendingTasksMap::iterator map_it = pending_tasks_.find(key);
  if (map_it == pending_tasks_.end()) {
    if (BackgroundInfo::HasLazyBackgroundPage(extension))
      CHECK(!host);  // lazy page should not load without any pending tasks
    return;
  }

  // Swap the pending tasks to a temporary, to avoid problems if the task
  // list is modified during processing.
  PendingTasksList tasks;
  tasks.swap(*map_it->second);
  for (auto& task : tasks)
    std::move(task).Run(host);

  pending_tasks_.erase(key);

  // Balance the keepalive in CreateLazyBackgroundHost. Note we don't do this on
  // a failure to load, because the keepalive count is reset in that case.
  if (host && BackgroundInfo::HasLazyBackgroundPage(extension)) {
    ProcessManager::Get(browser_context)
        ->DecrementLazyKeepaliveCount(extension);
  }
}

void LazyBackgroundTaskQueue::NotifyTasksExtensionFailedToLoad(
    content::BrowserContext* browser_context,
    const Extension* extension) {
  ProcessPendingTasks(nullptr, browser_context, extension);
  // If this extension is also running in an off-the-record context, notify that
  // task queue as well.
  ExtensionsBrowserClient* browser_client = ExtensionsBrowserClient::Get();
  if (browser_client->HasOffTheRecordContext(browser_context)) {
    ProcessPendingTasks(nullptr,
                        browser_client->GetOffTheRecordContext(browser_context),
                        extension);
  }
}

void LazyBackgroundTaskQueue::Observe(
    int type,
    const content::NotificationSource& source,
    const content::NotificationDetails& details) {
  switch (type) {
    case extensions::NOTIFICATION_EXTENSION_HOST_DID_STOP_FIRST_LOAD: {
      // If an on-demand background page finished loading, dispatch queued up
      // events for it.
      ExtensionHost* host =
          content::Details<ExtensionHost>(details).ptr();
      if (host->extension_host_type() == VIEW_TYPE_EXTENSION_BACKGROUND_PAGE) {
        CHECK(host->has_loaded_once());
        ProcessPendingTasks(host, host->browser_context(), host->extension());
      }
      break;
    }
    case extensions::NOTIFICATION_EXTENSION_HOST_DESTROYED: {
      // Notify consumers about the load failure when the background host dies.
      // This can happen if the extension crashes. This is not strictly
      // necessary, since we also unload the extension in that case (which
      // dispatches the tasks below), but is a good extra precaution.
      content::BrowserContext* browser_context =
          content::Source<content::BrowserContext>(source).ptr();
      ExtensionHost* host =
           content::Details<ExtensionHost>(details).ptr();
      if (host->extension() &&
          host->extension_host_type() == VIEW_TYPE_EXTENSION_BACKGROUND_PAGE) {
        ProcessPendingTasks(NULL, browser_context, host->extension());
      }
      break;
    }
    default:
      NOTREACHED();
      break;
  }
}

void LazyBackgroundTaskQueue::OnExtensionLoaded(
    content::BrowserContext* browser_context,
    const Extension* extension) {
  // If there are pending tasks for a lazy background page, and its background
  // host has not been created yet, then create it. This can happen if a pending
  // task was added while the extension is not yet enabled (e.g., component
  // extension crashed and waiting to reload, https://crbug.com/835017).
  if (!BackgroundInfo::HasLazyBackgroundPage(extension))
    return;

  CreateLazyBackgroundHostOnExtensionLoaded(browser_context, extension);

  // Also try to create the background host for the off-the-record context.
  ExtensionsBrowserClient* browser_client = ExtensionsBrowserClient::Get();
  if (browser_client->HasOffTheRecordContext(browser_context)) {
    CreateLazyBackgroundHostOnExtensionLoaded(
        browser_client->GetOffTheRecordContext(browser_context), extension);
  }
}

void LazyBackgroundTaskQueue::OnExtensionUnloaded(
    content::BrowserContext* browser_context,
    const Extension* extension,
    UnloadedExtensionReason reason) {
  NotifyTasksExtensionFailedToLoad(browser_context, extension);
}

void LazyBackgroundTaskQueue::CreateLazyBackgroundHostOnExtensionLoaded(
    content::BrowserContext* browser_context,
    const Extension* extension) {
  PendingTasksKey key(browser_context, extension->id());
  if (!base::ContainsKey(pending_tasks_, key))
    return;

  ProcessManager* pm = ProcessManager::Get(browser_context);

  // Background host already created, just wait for it to finish loading.
  if (pm->GetBackgroundHostForExtension(extension->id()))
    return;

  if (!CreateLazyBackgroundHost(pm, extension))
    ProcessPendingTasks(nullptr, browser_context, extension);
}

}  // namespace extensions
