// 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 "content/browser/browser_plugin/browser_plugin_embedder.h"

#include "base/command_line.h"
#include "base/stl_util.h"
#include "content/browser/browser_plugin/browser_plugin_guest.h"
#include "content/browser/browser_plugin/browser_plugin_host_factory.h"
#include "content/browser/renderer_host/render_view_host_impl.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/common/browser_plugin_messages.h"
#include "content/common/gpu/gpu_messages.h"
#include "content/public/browser/user_metrics.h"
#include "content/public/common/content_switches.h"
#include "content/public/common/result_codes.h"
#include "content/public/common/url_constants.h"
#include "net/base/escape.h"

namespace content {

// static
BrowserPluginHostFactory* BrowserPluginEmbedder::factory_ = NULL;

BrowserPluginEmbedder::BrowserPluginEmbedder(
    WebContentsImpl* web_contents,
    RenderViewHost* render_view_host)
    : WebContentsObserver(web_contents),
      render_view_host_(render_view_host),
      next_get_render_view_request_id_(0),
      next_instance_id_(0) {
}

BrowserPluginEmbedder::~BrowserPluginEmbedder() {
  CleanUp();
}

// static
BrowserPluginEmbedder* BrowserPluginEmbedder::Create(
    WebContentsImpl* web_contents,
    content::RenderViewHost* render_view_host) {
  if (factory_) {
    return factory_->CreateBrowserPluginEmbedder(web_contents,
                                                 render_view_host);
  }
  return new BrowserPluginEmbedder(web_contents, render_view_host);
}

void BrowserPluginEmbedder::CreateGuest(
    int instance_id,
    int routing_id,
    BrowserPluginGuest* guest_opener,
    const BrowserPluginHostMsg_CreateGuest_Params& params) {
  WebContentsImpl* guest_web_contents = NULL;
  SiteInstance* guest_site_instance = NULL;
  BrowserPluginGuest* guest = GetGuestByInstanceID(instance_id);
  CHECK(!guest);

  // Validate that the partition id coming from the renderer is valid UTF-8,
  // since we depend on this in other parts of the code, such as FilePath
  // creation. If the validation fails, treat it as a bad message and kill the
  // renderer process.
  if (!IsStringUTF8(params.storage_partition_id)) {
    content::RecordAction(UserMetricsAction("BadMessageTerminate_BPE"));
    base::KillProcess(render_view_host_->GetProcess()->GetHandle(),
                      content::RESULT_CODE_KILLED_BAD_MESSAGE, false);
    return;
  }

  const CommandLine& command_line = *CommandLine::ForCurrentProcess();
  if (command_line.HasSwitch(switches::kSitePerProcess)) {
    // When --site-per-process is specified, the behavior of BrowserPlugin
    // as <webview> is broken and we use it for rendering out-of-process
    // iframes instead. We use the src URL sent by the renderer to find the
    // right process in which to place this instance.
    // Note: Since BrowserPlugin doesn't support cross-process navigation,
    // the instance will stay in the initially assigned process, regardless
    // of the site it is navigated to.
    // TODO(nasko): Fix this, and such that cross-process navigations are
    // supported.
    guest_site_instance =
        web_contents()->GetSiteInstance()->GetRelatedSiteInstance(
            GURL(params.src));
  } else {
    const std::string& host =
        render_view_host_->GetSiteInstance()->GetSiteURL().host();
    std::string url_encoded_partition = net::EscapeQueryParamValue(
        params.storage_partition_id, false);

    if (guest_opener) {
      guest_site_instance = guest_opener->GetWebContents()->GetSiteInstance();
    } else {
      // The SiteInstance of a given webview tag is based on the fact that it's
      // a guest process in addition to which platform application the tag
      // belongs to and what storage partition is in use, rather than the URL
      // that the tag is being navigated to.
      GURL guest_site(
          base::StringPrintf("%s://%s/%s?%s", chrome::kGuestScheme,
                             host.c_str(),
                             params.persist_storage ? "persist" : "",
                             url_encoded_partition.c_str()));

      // If we already have a webview tag in the same app using the same storage
      // partition, we should use the same SiteInstance so the existing tag and
      // the new tag can script each other.
      for (ContainerInstanceMap::const_iterator it =
           guest_web_contents_by_instance_id_.begin();
           it != guest_web_contents_by_instance_id_.end(); ++it) {
        if (it->second->GetSiteInstance()->GetSiteURL() == guest_site) {
          guest_site_instance = it->second->GetSiteInstance();
          break;
        }
      }
      if (!guest_site_instance) {
        // Create the SiteInstance in a new BrowsingInstance, which will ensure
        // that webview tags are also not allowed to send messages across
        // different partitions.
        guest_site_instance = SiteInstance::CreateForURL(
            web_contents()->GetBrowserContext(), guest_site);
      }
    }
  }

  WebContentsImpl* opener_web_contents = static_cast<WebContentsImpl*>(
      guest_opener ? guest_opener->GetWebContents() : NULL);
  guest_web_contents = WebContentsImpl::CreateGuest(
      web_contents()->GetBrowserContext(),
      guest_site_instance,
      routing_id,
      static_cast<WebContentsImpl*>(web_contents()),
      opener_web_contents,
      instance_id,
      params);

  guest = guest_web_contents->GetBrowserPluginGuest();
  AddGuest(instance_id, guest_web_contents);

  guest->Initialize(params, guest_web_contents->GetRenderViewHost());
}

BrowserPluginGuest* BrowserPluginEmbedder::GetGuestByInstanceID(
    int instance_id) const {
  ContainerInstanceMap::const_iterator it =
      guest_web_contents_by_instance_id_.find(instance_id);
  if (it != guest_web_contents_by_instance_id_.end())
    return static_cast<WebContentsImpl*>(it->second)->GetBrowserPluginGuest();
  return NULL;
}

void BrowserPluginEmbedder::DestroyGuestByInstanceID(int instance_id) {
  BrowserPluginGuest* guest = GetGuestByInstanceID(instance_id);
  if (guest) {
    WebContents* guest_web_contents = guest->GetWebContents();

    // Destroy the guest's web_contents.
    delete guest_web_contents;
    guest_web_contents_by_instance_id_.erase(instance_id);
  }
}

void BrowserPluginEmbedder::GetRenderViewHostAtPosition(
    int x, int y, const WebContents::GetRenderViewHostCallback& callback) {
  // Store the callback so we can call it later when we have the response.
  pending_get_render_view_callbacks_.insert(
      std::make_pair(next_get_render_view_request_id_, callback));
  render_view_host_->Send(
      new BrowserPluginMsg_PluginAtPositionRequest(
          render_view_host_->GetRoutingID(),
          next_get_render_view_request_id_,
          gfx::Point(x, y)));
  ++next_get_render_view_request_id_;
}

void BrowserPluginEmbedder::RenderViewDeleted(
    RenderViewHost* render_view_host) {
}

void BrowserPluginEmbedder::RenderViewGone(base::TerminationStatus status) {
  CleanUp();
}

bool BrowserPluginEmbedder::OnMessageReceived(const IPC::Message& message) {
  if (ShouldForwardToBrowserPluginGuest(message)) {
    int instance_id = 0;
    // All allowed messages must have instance_id as their first parameter.
    PickleIterator iter(message);
    bool success = iter.ReadInt(&instance_id);
    DCHECK(success);
    BrowserPluginGuest* guest = GetGuestByInstanceID(instance_id);
    if (guest && guest->OnMessageReceivedFromEmbedder(message))
      return true;
  }
  bool handled = true;
  IPC_BEGIN_MESSAGE_MAP(BrowserPluginEmbedder, message)
    IPC_MESSAGE_HANDLER(BrowserPluginHostMsg_AllocateInstanceID,
                        OnAllocateInstanceID)
    IPC_MESSAGE_HANDLER(BrowserPluginHostMsg_CreateGuest,
                        OnCreateGuest)
    IPC_MESSAGE_HANDLER(BrowserPluginHostMsg_PluginAtPositionResponse,
                        OnPluginAtPositionResponse)
    IPC_MESSAGE_HANDLER(BrowserPluginHostMsg_PluginDestroyed,
                        OnPluginDestroyed)
    IPC_MESSAGE_HANDLER(BrowserPluginHostMsg_BuffersSwappedACK,
                        OnUnhandledSwapBuffersACK)
    IPC_MESSAGE_UNHANDLED(handled = false)
  IPC_END_MESSAGE_MAP()
  return handled;
}

void BrowserPluginEmbedder::AddGuest(int instance_id,
                                     WebContents* guest_web_contents) {
  DCHECK(guest_web_contents_by_instance_id_.find(instance_id) ==
         guest_web_contents_by_instance_id_.end());
  guest_web_contents_by_instance_id_[instance_id] = guest_web_contents;
}

void BrowserPluginEmbedder::CleanUp() {
  // Destroy guests that are managed by the current embedder.
  STLDeleteContainerPairSecondPointers(
      guest_web_contents_by_instance_id_.begin(),
      guest_web_contents_by_instance_id_.end());
  guest_web_contents_by_instance_id_.clear();

  // CleanUp gets called when BrowserPluginEmbedder's WebContents goes away
  // or the associated RenderViewHost is destroyed or swapped out. Therefore we
  // don't need to care about the pending callbacks anymore.
  pending_get_render_view_callbacks_.clear();
}

// static
bool BrowserPluginEmbedder::ShouldForwardToBrowserPluginGuest(
    const IPC::Message& message) {
  switch (message.type()) {
    case BrowserPluginHostMsg_BuffersSwappedACK::ID:
    case BrowserPluginHostMsg_DragStatusUpdate::ID:
    case BrowserPluginHostMsg_Go::ID:
    case BrowserPluginHostMsg_HandleInputEvent::ID:
    case BrowserPluginHostMsg_NavigateGuest::ID:
    case BrowserPluginHostMsg_Reload::ID:
    case BrowserPluginHostMsg_ResizeGuest::ID:
    case BrowserPluginHostMsg_SetAutoSize::ID:
    case BrowserPluginHostMsg_SetFocus::ID:
    case BrowserPluginHostMsg_SetName::ID:
    case BrowserPluginHostMsg_SetVisibility::ID:
    case BrowserPluginHostMsg_Stop::ID:
    case BrowserPluginHostMsg_TerminateGuest::ID:
    case BrowserPluginHostMsg_UpdateRect_ACK::ID:
      return true;
    default:
      break;
  }
  return false;
}

void BrowserPluginEmbedder::OnAllocateInstanceID(int request_id) {
  int instance_id = ++next_instance_id_;
  render_view_host_->Send(new BrowserPluginMsg_AllocateInstanceID_ACK(
      render_view_host_->GetRoutingID(), request_id, instance_id));
}

void BrowserPluginEmbedder::OnCreateGuest(
    int instance_id,
    const BrowserPluginHostMsg_CreateGuest_Params& params) {
  CreateGuest(instance_id, MSG_ROUTING_NONE, NULL, params);
}

void BrowserPluginEmbedder::OnPluginAtPositionResponse(
    int instance_id, int request_id, const gfx::Point& position) {
  const std::map<int, WebContents::GetRenderViewHostCallback>::iterator
      callback_iter = pending_get_render_view_callbacks_.find(request_id);
  if (callback_iter == pending_get_render_view_callbacks_.end())
    return;

  RenderViewHost* render_view_host;
  BrowserPluginGuest* guest = GetGuestByInstanceID(instance_id);
  if (guest)
    render_view_host = guest->GetWebContents()->GetRenderViewHost();
  else  // No plugin, use embedder's RenderViewHost.
    render_view_host = render_view_host_;

  callback_iter->second.Run(render_view_host, position.x(), position.y());
  pending_get_render_view_callbacks_.erase(callback_iter);
}

void BrowserPluginEmbedder::OnPluginDestroyed(int instance_id) {
  DestroyGuestByInstanceID(instance_id);
}

// We only get here during teardown if we have one last buffer pending,
// otherwise the ACK is handled by the guest.
void BrowserPluginEmbedder::OnUnhandledSwapBuffersACK(
    int instance_id,
    int route_id,
    int gpu_host_id,
    const std::string& mailbox_name,
    uint32 sync_point) {
  BrowserPluginGuest::AcknowledgeBufferPresent(route_id,
                                               gpu_host_id,
                                               mailbox_name,
                                               sync_point);
}

}  // namespace content