// 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/renderer/gpu/gpu_benchmarking_extension.h"

#include <string>

#include "base/base64.h"
#include "base/file_util.h"
#include "base/files/file_path.h"
#include "base/memory/scoped_vector.h"
#include "base/strings/string_number_conversions.h"
#include "cc/layers/layer.h"
#include "content/common/browser_rendering_stats.h"
#include "content/common/gpu/gpu_rendering_stats.h"
#include "content/public/renderer/render_thread.h"
#include "content/renderer/gpu/render_widget_compositor.h"
#include "content/renderer/render_view_impl.h"
#include "content/renderer/skia_benchmarking_extension.h"
#include "third_party/WebKit/public/web/WebFrame.h"
#include "third_party/WebKit/public/web/WebImageCache.h"
#include "third_party/WebKit/public/web/WebView.h"
#include "third_party/skia/include/core/SkData.h"
#include "third_party/skia/include/core/SkGraphics.h"
#include "third_party/skia/include/core/SkPicture.h"
#include "third_party/skia/include/core/SkPixelRef.h"
#include "third_party/skia/include/core/SkStream.h"
#include "ui/gfx/codec/png_codec.h"
#include "v8/include/v8.h"
#include "webkit/renderer/compositor_bindings/web_rendering_stats_impl.h"

using WebKit::WebCanvas;
using WebKit::WebFrame;
using WebKit::WebImageCache;
using WebKit::WebPrivatePtr;
using WebKit::WebRenderingStatsImpl;
using WebKit::WebSize;
using WebKit::WebView;

const char kGpuBenchmarkingExtensionName[] = "v8/GpuBenchmarking";

static SkData* EncodeBitmapToData(size_t* offset, const SkBitmap& bm) {
    SkPixelRef* pr = bm.pixelRef();
    if (pr != NULL) {
        SkData* data = pr->refEncodedData();
        if (data != NULL) {
            *offset = bm.pixelRefOffset();
            return data;
        }
    }
    std::vector<unsigned char> vector;
    if (gfx::PNGCodec::EncodeBGRASkBitmap(bm, false, &vector)) {
        return SkData::NewWithCopy(&vector.front() , vector.size());
    }
    return NULL;
}

namespace {

class SkPictureSerializer {
 public:
  explicit SkPictureSerializer(const base::FilePath& dirpath)
      : dirpath_(dirpath),
        layer_id_(0) {
    // Let skia register known effect subclasses. This basically enables
    // reflection on those subclasses required for picture serialization.
    content::SkiaBenchmarkingExtension::InitSkGraphics();
  }

  // Recursively serializes the layer tree.
  // Each layer in the tree is serialized into a separate skp file
  // in the given directory.
  void Serialize(const cc::Layer* layer) {
    const cc::LayerList& children = layer->children();
    for (size_t i = 0; i < children.size(); ++i) {
      Serialize(children[i].get());
    }

    skia::RefPtr<SkPicture> picture = layer->GetPicture();
    if (!picture)
      return;

    // Serialize picture to file.
    // TODO(alokp): Note that for this to work Chrome needs to be launched with
    // --no-sandbox command-line flag. Get rid of this limitation.
    // CRBUG: 139640.
    std::string filename = "layer_" + base::IntToString(layer_id_++) + ".skp";
    std::string filepath = dirpath_.AppendASCII(filename).MaybeAsASCII();
    DCHECK(!filepath.empty());
    SkFILEWStream file(filepath.c_str());
    DCHECK(file.isValid());
    picture->serialize(&file, &EncodeBitmapToData);
  }

 private:
  base::FilePath dirpath_;
  int layer_id_;
};

class RenderingStatsEnumerator : public cc::RenderingStats::Enumerator {
 public:
  RenderingStatsEnumerator(v8::Handle<v8::Object> stats_object)
      : stats_object(stats_object) { }

  virtual void AddInt64(const char* name, int64 value) OVERRIDE {
    stats_object->Set(v8::String::New(name), v8::Number::New(value));
  }

  virtual void AddDouble(const char* name, double value) OVERRIDE {
    stats_object->Set(v8::String::New(name), v8::Number::New(value));
  }

  virtual void AddInt(const char* name, int value) OVERRIDE {
    stats_object->Set(v8::String::New(name), v8::Integer::New(value));
  }

  virtual void AddTimeDeltaInSecondsF(const char* name,
                                      const base::TimeDelta& value) OVERRIDE {
    stats_object->Set(v8::String::New(name),
                      v8::Number::New(value.InSecondsF()));
  }

 private:
  v8::Handle<v8::Object> stats_object;
};

}  // namespace

namespace content {

namespace {

class CallbackAndContext : public base::RefCounted<CallbackAndContext> {
 public:
  CallbackAndContext(v8::Isolate* isolate,
                     v8::Handle<v8::Function> callback,
                     v8::Handle<v8::Context> context)
      : isolate_(isolate) {
    callback_.Reset(isolate_, callback);
    context_.Reset(isolate_, context);
  }

  v8::Isolate* isolate() {
    return isolate_;
  }

  v8::Handle<v8::Function> GetCallback() {
    return v8::Local<v8::Function>::New(isolate_, callback_);
  }

  v8::Handle<v8::Context> GetContext() {
    return v8::Local<v8::Context>::New(isolate_, context_);
  }

 private:
  friend class base::RefCounted<CallbackAndContext>;

  virtual ~CallbackAndContext() {
    callback_.Dispose();
    context_.Dispose();
  }

  v8::Isolate* isolate_;
  v8::Persistent<v8::Function> callback_;
  v8::Persistent<v8::Context> context_;
  DISALLOW_COPY_AND_ASSIGN(CallbackAndContext);
};

}  // namespace

class GpuBenchmarkingWrapper : public v8::Extension {
 public:
  GpuBenchmarkingWrapper() :
      v8::Extension(kGpuBenchmarkingExtensionName,
          "if (typeof(chrome) == 'undefined') {"
          "  chrome = {};"
          "};"
          "if (typeof(chrome.gpuBenchmarking) == 'undefined') {"
          "  chrome.gpuBenchmarking = {};"
          "};"
          "chrome.gpuBenchmarking.setNeedsDisplayOnAllLayers = function() {"
          "  native function SetNeedsDisplayOnAllLayers();"
          "  return SetNeedsDisplayOnAllLayers();"
          "};"
          "chrome.gpuBenchmarking.setRasterizeOnlyVisibleContent = function() {"
          "  native function SetRasterizeOnlyVisibleContent();"
          "  return SetRasterizeOnlyVisibleContent();"
          "};"
          "chrome.gpuBenchmarking.renderingStats = function() {"
          "  native function GetRenderingStats();"
          "  return GetRenderingStats();"
          "};"
          "chrome.gpuBenchmarking.printToSkPicture = function(dirname) {"
          "  native function PrintToSkPicture();"
          "  return PrintToSkPicture(dirname);"
          "};"
          "chrome.gpuBenchmarking.smoothScrollBy = "
          "    function(pixels_to_scroll, opt_callback, opt_mouse_event_x,"
          "             opt_mouse_event_y) {"
          "  pixels_to_scroll = pixels_to_scroll || 0;"
          "  callback = opt_callback || function() { };"
          "  native function BeginSmoothScroll();"
          "  if (typeof opt_mouse_event_x !== 'undefined' &&"
          "      typeof opt_mouse_event_y !== 'undefined') {"
          "    return BeginSmoothScroll(pixels_to_scroll >= 0, callback,"
          "                             Math.abs(pixels_to_scroll),"
          "                             opt_mouse_event_x, opt_mouse_event_y);"
          "  } else {"
          "    return BeginSmoothScroll(pixels_to_scroll >= 0, callback,"
          "                             Math.abs(pixels_to_scroll));"
          "  }"
          "};"
          "chrome.gpuBenchmarking.smoothScrollBySendsTouch = function() {"
          "  native function SmoothScrollSendsTouch();"
          "  return SmoothScrollSendsTouch();"
          "};"
          "chrome.gpuBenchmarking.pinchBy = "
          "    function(zoom_in, pixels_to_move, anchor_x, anchor_y,"
          "             opt_callback) {"
          "  callback = opt_callback || function() { };"
          "  native function BeginPinch();"
          "  return BeginPinch(zoom_in, pixels_to_move,"
          "                    anchor_x, anchor_y, callback);"
          "};"
          "chrome.gpuBenchmarking.beginWindowSnapshotPNG = function(callback) {"
          "  native function BeginWindowSnapshotPNG();"
          "  BeginWindowSnapshotPNG(callback);"
          "};"
          "chrome.gpuBenchmarking.clearImageCache = function() {"
          "  native function ClearImageCache();"
          "  ClearImageCache();"
          "};") {}

  virtual v8::Handle<v8::FunctionTemplate> GetNativeFunction(
      v8::Handle<v8::String> name) OVERRIDE {
    if (name->Equals(v8::String::New("SetNeedsDisplayOnAllLayers")))
      return v8::FunctionTemplate::New(SetNeedsDisplayOnAllLayers);
    if (name->Equals(v8::String::New("SetRasterizeOnlyVisibleContent")))
      return v8::FunctionTemplate::New(SetRasterizeOnlyVisibleContent);
    if (name->Equals(v8::String::New("GetRenderingStats")))
      return v8::FunctionTemplate::New(GetRenderingStats);
    if (name->Equals(v8::String::New("PrintToSkPicture")))
      return v8::FunctionTemplate::New(PrintToSkPicture);
    if (name->Equals(v8::String::New("BeginSmoothScroll")))
      return v8::FunctionTemplate::New(BeginSmoothScroll);
    if (name->Equals(v8::String::New("SmoothScrollSendsTouch")))
      return v8::FunctionTemplate::New(SmoothScrollSendsTouch);
    if (name->Equals(v8::String::New("BeginPinch")))
      return v8::FunctionTemplate::New(BeginPinch);
    if (name->Equals(v8::String::New("BeginWindowSnapshotPNG")))
      return v8::FunctionTemplate::New(BeginWindowSnapshotPNG);
    if (name->Equals(v8::String::New("ClearImageCache")))
      return v8::FunctionTemplate::New(ClearImageCache);

    return v8::Handle<v8::FunctionTemplate>();
  }

  static void SetNeedsDisplayOnAllLayers(
      const v8::FunctionCallbackInfo<v8::Value>& args) {
    WebFrame* web_frame = WebFrame::frameForCurrentContext();
    if (!web_frame)
      return;

    WebView* web_view = web_frame->view();
    if (!web_view)
      return;

    RenderViewImpl* render_view_impl = RenderViewImpl::FromWebView(web_view);
    if (!render_view_impl)
      return;

    RenderWidgetCompositor* compositor = render_view_impl->compositor();
    if (!compositor)
      return;

    compositor->SetNeedsDisplayOnAllLayers();
  }

  static void SetRasterizeOnlyVisibleContent(
      const v8::FunctionCallbackInfo<v8::Value>& args) {
    WebFrame* web_frame = WebFrame::frameForCurrentContext();
    if (!web_frame)
      return;

    WebView* web_view = web_frame->view();
    if (!web_view)
      return;

    RenderViewImpl* render_view_impl = RenderViewImpl::FromWebView(web_view);
    if (!render_view_impl)
      return;

    RenderWidgetCompositor* compositor = render_view_impl->compositor();
    if (!compositor)
      return;

    compositor->SetRasterizeOnlyVisibleContent();
  }

  static void GetRenderingStats(
      const v8::FunctionCallbackInfo<v8::Value>& args) {

    WebFrame* web_frame = WebFrame::frameForCurrentContext();
    if (!web_frame)
      return;

    WebView* web_view = web_frame->view();
    if (!web_view)
      return;

    RenderViewImpl* render_view_impl = RenderViewImpl::FromWebView(web_view);
    if (!render_view_impl)
      return;

    WebRenderingStatsImpl stats;
    render_view_impl->GetRenderingStats(stats);

    content::GpuRenderingStats gpu_stats;
    render_view_impl->GetGpuRenderingStats(&gpu_stats);
    BrowserRenderingStats browser_stats;
    render_view_impl->GetBrowserRenderingStats(&browser_stats);
    v8::Handle<v8::Object> stats_object = v8::Object::New();

    RenderingStatsEnumerator enumerator(stats_object);
    stats.rendering_stats.EnumerateFields(&enumerator);
    gpu_stats.EnumerateFields(&enumerator);
    browser_stats.EnumerateFields(&enumerator);

    args.GetReturnValue().Set(stats_object);
  }

  static void PrintToSkPicture(
      const v8::FunctionCallbackInfo<v8::Value>& args) {
    if (args.Length() != 1)
      return;

    v8::String::AsciiValue dirname(args[0]);
    if (dirname.length() == 0)
      return;

    WebFrame* web_frame = WebFrame::frameForCurrentContext();
    if (!web_frame)
      return;

    WebView* web_view = web_frame->view();
    if (!web_view)
      return;

    RenderViewImpl* render_view_impl = RenderViewImpl::FromWebView(web_view);
    if (!render_view_impl)
      return;

    RenderWidgetCompositor* compositor = render_view_impl->compositor();
    if (!compositor)
      return;

    const cc::Layer* root_layer = compositor->GetRootLayer();
    if (!root_layer)
      return;

    base::FilePath dirpath(
        base::FilePath::StringType(*dirname, *dirname + dirname.length()));
    if (!file_util::CreateDirectory(dirpath) ||
        !base::PathIsWritable(dirpath)) {
      std::string msg("Path is not writable: ");
      msg.append(dirpath.MaybeAsASCII());
      v8::ThrowException(v8::Exception::Error(
          v8::String::New(msg.c_str(), msg.length())));
      return;
    }

    SkPictureSerializer serializer(dirpath);
    serializer.Serialize(root_layer);
  }

  static void OnSmoothScrollCompleted(
      CallbackAndContext* callback_and_context) {
    v8::HandleScope scope(callback_and_context->isolate());
    v8::Handle<v8::Context> context = callback_and_context->GetContext();
    v8::Context::Scope context_scope(context);
    WebFrame* frame = WebFrame::frameForContext(context);
    if (frame) {
      frame->callFunctionEvenIfScriptDisabled(
          callback_and_context->GetCallback(), v8::Object::New(), 0, NULL);
    }
  }

  static void SmoothScrollSendsTouch(
      const v8::FunctionCallbackInfo<v8::Value>& args) {
    // TODO(epenner): Should other platforms emulate touch events?
#if defined(OS_ANDROID) || defined(OS_CHROMEOS)
    args.GetReturnValue().Set(true);
#else
    args.GetReturnValue().Set(false);
#endif
  }

  static void BeginSmoothScroll(
      const v8::FunctionCallbackInfo<v8::Value>& args) {
    WebFrame* web_frame = WebFrame::frameForCurrentContext();
    if (!web_frame)
      return;

    WebView* web_view = web_frame->view();
    if (!web_view)
      return;

    RenderViewImpl* render_view_impl = RenderViewImpl::FromWebView(web_view);
    if (!render_view_impl)
      return;

    // Account for the 2 optional arguments, mouse_event_x and mouse_event_y.
    int arglen = args.Length();
    if (arglen < 3 ||
        !args[0]->IsBoolean() ||
        !args[1]->IsFunction() ||
        !args[2]->IsNumber()) {
      args.GetReturnValue().Set(false);
      return;
    }

    bool scroll_down = args[0]->BooleanValue();
    v8::Local<v8::Function> callback_local =
        v8::Local<v8::Function>::Cast(args[1]);

    scoped_refptr<CallbackAndContext> callback_and_context =
        new CallbackAndContext(args.GetIsolate(),
                               callback_local,
                               web_frame->mainWorldScriptContext());

    int pixels_to_scroll = args[2]->IntegerValue();

    int mouse_event_x = 0;
    int mouse_event_y = 0;

    if (arglen == 3) {
      WebKit::WebRect rect = render_view_impl->windowRect();
      mouse_event_x = rect.x + rect.width / 2;
      mouse_event_y = rect.y + rect.height / 2;
    } else {
      if (arglen != 5 ||
          !args[3]->IsNumber() ||
          !args[4]->IsNumber()) {
        args.GetReturnValue().Set(false);
        return;
      }

      mouse_event_x = args[3]->IntegerValue() * web_view->pageScaleFactor();
      mouse_event_y = args[4]->IntegerValue() * web_view->pageScaleFactor();
    }

    // TODO(nduca): If the render_view_impl is destroyed while the gesture is in
    // progress, we will leak the callback and context. This needs to be fixed,
    // somehow.
    render_view_impl->BeginSmoothScroll(
        scroll_down,
        base::Bind(&OnSmoothScrollCompleted,
                   callback_and_context),
        pixels_to_scroll,
        mouse_event_x,
        mouse_event_y);

    args.GetReturnValue().Set(true);
  }

  static void BeginPinch(
      const v8::FunctionCallbackInfo<v8::Value>& args) {
    WebFrame* web_frame = WebFrame::frameForCurrentContext();
    if (!web_frame)
      return;

    WebView* web_view = web_frame->view();
    if (!web_view)
      return;

    RenderViewImpl* render_view_impl = RenderViewImpl::FromWebView(web_view);
    if (!render_view_impl)
      return;

    int arglen = args.Length();
    if (arglen < 5 ||
        !args[0]->IsBoolean() ||
        !args[1]->IsNumber() ||
        !args[2]->IsNumber() ||
        !args[3]->IsNumber() ||
        !args[4]->IsFunction()) {
      args.GetReturnValue().Set(false);
      return;
    }

    bool zoom_in = args[0]->BooleanValue();
    int pixels_to_move = args[1]->IntegerValue();
    int anchor_x = args[2]->IntegerValue();
    int anchor_y = args[3]->IntegerValue();

    v8::Local<v8::Function> callback_local =
        v8::Local<v8::Function>::Cast(args[4]);

    scoped_refptr<CallbackAndContext> callback_and_context =
        new CallbackAndContext(args.GetIsolate(),
                               callback_local,
                               web_frame->mainWorldScriptContext());


    // TODO(nduca): If the render_view_impl is destroyed while the gesture is in
    // progress, we will leak the callback and context. This needs to be fixed,
    // somehow.
    render_view_impl->BeginPinch(
        zoom_in,
        pixels_to_move,
        anchor_x,
        anchor_y,
        base::Bind(&OnSmoothScrollCompleted,
                   callback_and_context));

    args.GetReturnValue().Set(true);
  }

  static void OnSnapshotCompleted(CallbackAndContext* callback_and_context,
                                  const gfx::Size& size,
                                  const std::vector<unsigned char>& png) {
    v8::HandleScope scope(callback_and_context->isolate());
    v8::Handle<v8::Context> context = callback_and_context->GetContext();
    v8::Context::Scope context_scope(context);
    WebFrame* frame = WebFrame::frameForContext(context);
    if (frame) {

      v8::Handle<v8::Value> result;

      if(!size.IsEmpty()) {
        v8::Handle<v8::Object> result_object;
        result_object = v8::Object::New();

        result_object->Set(v8::String::New("width"),
                           v8::Number::New(size.width()));
        result_object->Set(v8::String::New("height"),
                           v8::Number::New(size.height()));

        std::string base64_png;
        base::Base64Encode(base::StringPiece(
            reinterpret_cast<const char*>(&*png.begin()), png.size()),
            &base64_png);

        result_object->Set(v8::String::New("data"),
            v8::String::New(base64_png.c_str(), base64_png.size()));

        result = result_object;
      } else {
        result = v8::Null();
      }

      v8::Handle<v8::Value> argv[] = { result };

      frame->callFunctionEvenIfScriptDisabled(
          callback_and_context->GetCallback(), v8::Object::New(), 1, argv);
    }
  }

  static void BeginWindowSnapshotPNG(
      const v8::FunctionCallbackInfo<v8::Value>& args) {
    WebFrame* web_frame = WebFrame::frameForCurrentContext();
    if (!web_frame)
      return;

    WebView* web_view = web_frame->view();
    if (!web_view)
      return;

    RenderViewImpl* render_view_impl = RenderViewImpl::FromWebView(web_view);
    if (!render_view_impl)
      return;

    if (!args[0]->IsFunction())
      return;

    v8::Local<v8::Function> callback_local =
        v8::Local<v8::Function>::Cast(args[0]);

    scoped_refptr<CallbackAndContext> callback_and_context =
        new CallbackAndContext(args.GetIsolate(),
                               callback_local,
                               web_frame->mainWorldScriptContext());

    render_view_impl->GetWindowSnapshot(
        base::Bind(&OnSnapshotCompleted, callback_and_context));
  }

  static void ClearImageCache(
      const v8::FunctionCallbackInfo<v8::Value>& args) {
    WebImageCache::clear();
  }
};

v8::Extension* GpuBenchmarkingExtension::Get() {
  return new GpuBenchmarkingWrapper();
}

}  // namespace content
