// Copyright 2015 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 "components/plugins/renderer/loadable_plugin_placeholder.h"

#include <memory>

#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/json/string_escape.h"
#include "base/strings/string_piece.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/threading/thread_task_runner_handle.h"
#include "base/values.h"
#include "content/public/child/v8_value_converter.h"
#include "content/public/renderer/render_frame.h"
#include "content/public/renderer/render_thread.h"
#include "third_party/WebKit/public/platform/WebInputEvent.h"
#include "third_party/WebKit/public/web/WebDOMMessageEvent.h"
#include "third_party/WebKit/public/web/WebDocument.h"
#include "third_party/WebKit/public/web/WebElement.h"
#include "third_party/WebKit/public/web/WebKit.h"
#include "third_party/WebKit/public/web/WebLocalFrame.h"
#include "third_party/WebKit/public/web/WebPluginContainer.h"
#include "third_party/WebKit/public/web/WebScriptSource.h"
#include "third_party/WebKit/public/web/WebSerializedScriptValue.h"
#include "third_party/WebKit/public/web/WebView.h"
#include "url/gurl.h"
#include "url/origin.h"

using base::UserMetricsAction;
using content::PluginInstanceThrottler;
using content::RenderFrame;
using content::RenderThread;

namespace plugins {

void LoadablePluginPlaceholder::BlockForPowerSaverPoster() {
  DCHECK(!is_blocked_for_power_saver_poster_);
  is_blocked_for_power_saver_poster_ = true;

  render_frame()->RegisterPeripheralPlugin(
      url::Origin(GURL(GetPluginParams().url)),
      base::Bind(&LoadablePluginPlaceholder::MarkPluginEssential,
                 weak_factory_.GetWeakPtr(),
                 PluginInstanceThrottler::UNTHROTTLE_METHOD_BY_WHITELIST));
}

void LoadablePluginPlaceholder::SetPremadePlugin(
    content::PluginInstanceThrottler* throttler) {
  DCHECK(throttler);
  DCHECK(!premade_throttler_);
  heuristic_run_before_ = true;
  premade_throttler_ = throttler;
}

LoadablePluginPlaceholder::LoadablePluginPlaceholder(
    RenderFrame* render_frame,
    blink::WebLocalFrame* frame,
    const blink::WebPluginParams& params,
    const std::string& html_data)
    : PluginPlaceholderBase(render_frame, frame, params, html_data),
      heuristic_run_before_(false),
      is_blocked_for_tinyness_(false),
      is_blocked_for_background_tab_(false),
      is_blocked_for_prerendering_(false),
      is_blocked_for_power_saver_poster_(false),
      power_saver_enabled_(false),
      premade_throttler_(nullptr),
      allow_loading_(false),
      finished_loading_(false),
      weak_factory_(this) {}

LoadablePluginPlaceholder::~LoadablePluginPlaceholder() {
}

void LoadablePluginPlaceholder::MarkPluginEssential(
    PluginInstanceThrottler::PowerSaverUnthrottleMethod method) {
  if (!power_saver_enabled_)
    return;

  power_saver_enabled_ = false;

  if (premade_throttler_)
    premade_throttler_->MarkPluginEssential(method);
  else if (method != PluginInstanceThrottler::UNTHROTTLE_METHOD_DO_NOT_RECORD)
    PluginInstanceThrottler::RecordUnthrottleMethodMetric(method);

  is_blocked_for_power_saver_poster_ = false;
  is_blocked_for_tinyness_ = false;
  if (!LoadingBlocked())
    LoadPlugin();
}

void LoadablePluginPlaceholder::ReplacePlugin(blink::WebPlugin* new_plugin) {
  CHECK(plugin());
  if (!new_plugin)
    return;
  blink::WebPluginContainer* container = plugin()->container();
  // This can occur if the container has been destroyed.
  if (!container) {
    new_plugin->destroy();
    return;
  }

  container->setPlugin(new_plugin);
  bool plugin_needs_initialization =
      !premade_throttler_ || new_plugin != premade_throttler_->GetWebPlugin();
  if (plugin_needs_initialization && !new_plugin->initialize(container)) {
    if (new_plugin->container()) {
      // Since the we couldn't initialize the new plugin, but the container
      // still exists, restore the placeholder and destroy the new plugin.
      container->setPlugin(plugin());
      new_plugin->destroy();
    } else {
      // The container has been destroyed, along with the new plugin. Destroy
      // our placeholder plugin also.
      plugin()->destroy();
    }
    return;
  }

  // During initialization, the new plugin might have replaced itself in turn
  // with another plugin. Make sure not to use the passed in |new_plugin| after
  // this point.
  new_plugin = container->plugin();

  container->invalidate();
  container->reportGeometry();
  if (plugin()->focused())
    new_plugin->updateFocus(true, blink::WebFocusTypeNone);
  container->element().setAttribute("title", plugin()->old_title());
  plugin()->destroy();
}

void LoadablePluginPlaceholder::SetMessage(const base::string16& message) {
  message_ = message;
  if (finished_loading_)
    UpdateMessage();
}

void LoadablePluginPlaceholder::UpdateMessage() {
  if (!plugin())
    return;
  std::string script =
      "window.setMessage(" + base::GetQuotedJSONString(message_) + ")";
  plugin()->web_view()->mainFrame()->executeScript(
      blink::WebScriptSource(base::UTF8ToUTF16(script)));
}

void LoadablePluginPlaceholder::PluginDestroyed() {
  if (power_saver_enabled_) {
    if (premade_throttler_) {
      // Since the premade plugin has been detached from the container, it will
      // not be automatically destroyed along with the page.
      premade_throttler_->GetWebPlugin()->destroy();
      premade_throttler_ = nullptr;
    } else if (is_blocked_for_power_saver_poster_) {
      // Record the NEVER unthrottle count only if there is no throttler.
      PluginInstanceThrottler::RecordUnthrottleMethodMetric(
          PluginInstanceThrottler::UNTHROTTLE_METHOD_NEVER);
    }

    // Prevent processing subsequent calls to MarkPluginEssential.
    power_saver_enabled_ = false;
  }

  PluginPlaceholderBase::PluginDestroyed();
}

v8::Local<v8::Object> LoadablePluginPlaceholder::GetV8ScriptableObject(
    v8::Isolate* isolate) const {
  // Pass through JavaScript access to the underlying throttled plugin.
  if (premade_throttler_ && premade_throttler_->GetWebPlugin()) {
    return premade_throttler_->GetWebPlugin()->v8ScriptableObject(isolate);
  }
  return v8::Local<v8::Object>();
}

void LoadablePluginPlaceholder::OnUnobscuredRectUpdate(
    const gfx::Rect& unobscured_rect) {
  DCHECK(content::RenderThread::Get());

  if (!plugin() || !finished_loading_)
    return;

  if (!is_blocked_for_tinyness_ && !is_blocked_for_power_saver_poster_)
    return;

  if (unobscured_rect_ == unobscured_rect)
    return;

  unobscured_rect_ = unobscured_rect;

  float zoom_factor = plugin()->container()->pageZoomFactor();
  int width = roundf(unobscured_rect_.width() / zoom_factor);
  int height = roundf(unobscured_rect_.height() / zoom_factor);
  int x = roundf(unobscured_rect_.x() / zoom_factor);
  int y = roundf(unobscured_rect_.y() / zoom_factor);

  // On a size update check if we now qualify as a essential plugin.
  url::Origin content_origin = url::Origin(GetPluginParams().url);
  RenderFrame::PeripheralContentStatus status =
      render_frame()->GetPeripheralContentStatus(
          render_frame()->GetWebFrame()->top()->getSecurityOrigin(),
          content_origin, gfx::Size(width, height),
          heuristic_run_before_ ? RenderFrame::DONT_RECORD_DECISION
                                : RenderFrame::RECORD_DECISION);

  bool plugin_is_tiny_and_blocked =
      is_blocked_for_tinyness_ &&
      status == RenderFrame::CONTENT_STATUS_ESSENTIAL_CROSS_ORIGIN_TINY;

  // Early exit for plugins that we've discovered to be essential.
  if (!plugin_is_tiny_and_blocked &&
      status != RenderFrame::CONTENT_STATUS_PERIPHERAL) {
    MarkPluginEssential(
        heuristic_run_before_
            ? PluginInstanceThrottler::UNTHROTTLE_METHOD_BY_SIZE_CHANGE
            : PluginInstanceThrottler::UNTHROTTLE_METHOD_DO_NOT_RECORD);

    if (!heuristic_run_before_ &&
        status == RenderFrame::CONTENT_STATUS_ESSENTIAL_CROSS_ORIGIN_BIG) {
      render_frame()->WhitelistContentOrigin(content_origin);
    }

    return;
  }
  heuristic_run_before_ = true;

  if (is_blocked_for_tinyness_) {
    if (plugin_is_tiny_and_blocked) {
      OnBlockedTinyContent();
    } else {
      is_blocked_for_tinyness_ = false;
      if (!LoadingBlocked()) {
        LoadPlugin();
      }
    }
  }

  if (is_blocked_for_power_saver_poster_) {
    // Adjust poster container padding and dimensions to center play button for
    // plugins and plugin posters that have their top or left portions obscured.
    std::string script = base::StringPrintf(
        "window.resizePoster('%dpx', '%dpx', '%dpx', '%dpx')", x, y, width,
        height);
    plugin()->web_view()->mainFrame()->executeScript(
        blink::WebScriptSource(base::UTF8ToUTF16(script)));
  }
}

void LoadablePluginPlaceholder::WasShown() {
  if (is_blocked_for_background_tab_) {
    is_blocked_for_background_tab_ = false;
    if (!LoadingBlocked())
      LoadPlugin();
  }
}

void LoadablePluginPlaceholder::OnLoadBlockedPlugins(
    const std::string& identifier) {
  if (!identifier.empty() && identifier != identifier_)
    return;

  RenderThread::Get()->RecordAction(UserMetricsAction("Plugin_Load_UI"));
  MarkPluginEssential(
      PluginInstanceThrottler::UNTHROTTLE_METHOD_BY_OMNIBOX_ICON);
  LoadPlugin();
}

void LoadablePluginPlaceholder::OnSetIsPrerendering(bool is_prerendering) {
  // Prerendering can only be enabled prior to a RenderView's first navigation,
  // so no BlockedPlugin should see the notification that enables prerendering.
  DCHECK(!is_prerendering);
  if (is_blocked_for_prerendering_) {
    is_blocked_for_prerendering_ = false;
    if (!LoadingBlocked())
      LoadPlugin();
  }
}

void LoadablePluginPlaceholder::LoadPlugin() {
  // This is not strictly necessary but is an important defense in case the
  // event propagation changes between "close" vs. "click-to-play".
  if (hidden())
    return;
  if (!plugin())
    return;
  if (!allow_loading_) {
    NOTREACHED();
    return;
  }

  if (premade_throttler_) {
    premade_throttler_->SetHiddenForPlaceholder(false /* hidden */);
    ReplacePlugin(premade_throttler_->GetWebPlugin());
    premade_throttler_ = nullptr;
  } else {
    ReplacePlugin(CreatePlugin());
  }
}

void LoadablePluginPlaceholder::LoadCallback() {
  RenderThread::Get()->RecordAction(UserMetricsAction("Plugin_Load_Click"));
  // If the user specifically clicks on the plugin content's placeholder,
  // disable power saver throttling for this instance.
  MarkPluginEssential(PluginInstanceThrottler::UNTHROTTLE_METHOD_BY_CLICK);
  LoadPlugin();
}

void LoadablePluginPlaceholder::DidFinishLoadingCallback() {
  finished_loading_ = true;
  if (message_.length() > 0)
    UpdateMessage();

  // Wait for the placeholder to finish loading to hide the premade plugin.
  // This is necessary to prevent a flicker.
  if (premade_throttler_ && power_saver_enabled_)
    premade_throttler_->SetHiddenForPlaceholder(true /* hidden */);

  // In case our initial geometry was reported before the placeholder finished
  // loading, request another one. Needed for correct large poster unthrottling.
  if (plugin()) {
    CHECK(plugin()->container());
    plugin()->container()->reportGeometry();
  }
}

void LoadablePluginPlaceholder::DidFinishIconRepositionForTestingCallback() {
  if (!plugin())
    return;

  // Set an attribute and post an event, so browser tests can wait for the
  // placeholder to be ready to receive simulated user input.
  blink::WebElement element = plugin()->container()->element();
  element.setAttribute("placeholderReady", "true");

  std::unique_ptr<content::V8ValueConverter> converter(
      content::V8ValueConverter::create());
  base::StringValue value("placeholderReady");
  blink::WebSerializedScriptValue message_data =
      blink::WebSerializedScriptValue::serialize(converter->ToV8Value(
          &value, element.document().frame()->mainWorldScriptContext()));
  blink::WebDOMMessageEvent msg_event(message_data);

  plugin()->container()->enqueueMessageEvent(msg_event);
}

void LoadablePluginPlaceholder::SetPluginInfo(
    const content::WebPluginInfo& plugin_info) {
  plugin_info_ = plugin_info;
}

const content::WebPluginInfo& LoadablePluginPlaceholder::GetPluginInfo() const {
  return plugin_info_;
}

void LoadablePluginPlaceholder::SetIdentifier(const std::string& identifier) {
  identifier_ = identifier;
}

const std::string& LoadablePluginPlaceholder::GetIdentifier() const {
  return identifier_;
}

bool LoadablePluginPlaceholder::LoadingBlocked() const {
  DCHECK(allow_loading_);
  return is_blocked_for_tinyness_ || is_blocked_for_background_tab_ ||
         is_blocked_for_power_saver_poster_ || is_blocked_for_prerendering_;
}

}  // namespace plugins
