// Copyright (c) 2006-2008 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 "webkit/tools/test_shell/webwidget_host.h"

#include <cairo/cairo.h>
#include <gtk/gtk.h>

#include "base/basictypes.h"
#include "base/logging.h"
#include "skia/ext/bitmap_platform_device.h"
#include "skia/ext/platform_canvas.h"
#include "skia/ext/platform_device.h"
#include "third_party/WebKit/WebKit/chromium/public/gtk/WebInputEventFactory.h"
#include "third_party/WebKit/WebKit/chromium/public/x11/WebScreenInfoFactory.h"
#include "third_party/WebKit/WebKit/chromium/public/WebInputEvent.h"
#include "third_party/WebKit/WebKit/chromium/public/WebPopupMenu.h"
#include "third_party/WebKit/WebKit/chromium/public/WebScreenInfo.h"
#include "third_party/WebKit/WebKit/chromium/public/WebSize.h"
#include "webkit/tools/test_shell/test_shell.h"
#include "webkit/tools/test_shell/test_shell_x11.h"

using WebKit::WebInputEventFactory;
using WebKit::WebKeyboardEvent;
using WebKit::WebMouseEvent;
using WebKit::WebMouseWheelEvent;
using WebKit::WebPopupMenu;
using WebKit::WebScreenInfo;
using WebKit::WebScreenInfoFactory;
using WebKit::WebSize;
using WebKit::WebWidgetClient;

namespace {

// Used to store a backpointer to WebWidgetHost from our GtkWidget.
const char kWebWidgetHostKey[] = "webwidgethost";

// In response to an invalidation, we call into WebKit to do layout. On
// Windows, WM_PAINT is a virtual message so any extra invalidates that come up
// while it's doing layout are implicitly swallowed as soon as we actually do
// drawing via BeginPaint.
//
// Though GTK does know how to collapse multiple paint requests, it won't erase
// paint requests from the future when we start drawing.  To avoid an infinite
// cycle of repaints, we track whether we're currently handling a redraw, and
// during that if we get told by WebKit that a region has become invalid, we
// still add that region to the local dirty rect but *don't* enqueue yet
// another "do a paint" message.
bool g_handling_expose = false;

// -----------------------------------------------------------------------------
// Callback functions to proxy to host...

// The web contents are completely drawn and handled by WebKit, except that
// windowed plugins are GtkSockets on top of it.  We need to place the
// GtkSockets inside a GtkContainer.  We use a GtkFixed container, and the
// GtkSocket objects override a little bit to manage their size (see the code
// in webplugin_delegate_impl_gtk.cc).  We listen on a the events we're
// interested in and forward them on to the WebWidgetHost.  This class is a
// collection of static methods, implementing the widget related code.
class WebWidgetHostGtkWidget {
 public:
  // This will create a new widget used for hosting the web contents.  We use
  // our GtkDrawingAreaContainer here, for the reasons mentioned above.
  static GtkWidget* CreateNewWidget(GtkWidget* parent_view,
                                    WebWidgetHost* host) {
    GtkWidget* widget = gtk_fixed_new();
    gtk_fixed_set_has_window(GTK_FIXED(widget), true);

    gtk_box_pack_start(GTK_BOX(parent_view), widget, TRUE, TRUE, 0);

    gtk_widget_add_events(widget, GDK_EXPOSURE_MASK |
                                  GDK_POINTER_MOTION_MASK |
                                  GDK_BUTTON_PRESS_MASK |
                                  GDK_BUTTON_RELEASE_MASK |
                                  GDK_KEY_PRESS_MASK |
                                  GDK_KEY_RELEASE_MASK);
    GTK_WIDGET_SET_FLAGS(widget, GTK_CAN_FOCUS);
    g_signal_connect(widget, "size-request",
                     G_CALLBACK(&HandleSizeRequest), host);
    g_signal_connect(widget, "size-allocate",
                     G_CALLBACK(&HandleSizeAllocate), host);
    g_signal_connect(widget, "configure-event",
                     G_CALLBACK(&HandleConfigure), host);
    g_signal_connect(widget, "expose-event",
                     G_CALLBACK(&HandleExpose), host);
    g_signal_connect(widget, "destroy",
                     G_CALLBACK(&HandleDestroy), host);
    g_signal_connect(widget, "key-press-event",
                     G_CALLBACK(&HandleKeyPress), host);
    g_signal_connect(widget, "key-release-event",
                     G_CALLBACK(&HandleKeyRelease), host);
    g_signal_connect(widget, "focus",
                     G_CALLBACK(&HandleFocus), host);
    g_signal_connect(widget, "focus-in-event",
                     G_CALLBACK(&HandleFocusIn), host);
    g_signal_connect(widget, "focus-out-event",
                     G_CALLBACK(&HandleFocusOut), host);
    g_signal_connect(widget, "button-press-event",
                     G_CALLBACK(&HandleButtonPress), host);
    g_signal_connect(widget, "button-release-event",
                     G_CALLBACK(&HandleButtonRelease), host);
    g_signal_connect(widget, "motion-notify-event",
                     G_CALLBACK(&HandleMotionNotify), host);
    g_signal_connect(widget, "scroll-event",
                     G_CALLBACK(&HandleScroll), host);

    g_object_set_data(G_OBJECT(widget), kWebWidgetHostKey, host);
    return widget;
  }

 private:
  // Our size was requested.  We let the GtkFixed do its normal calculation,
  // after which this callback is called.  The GtkFixed will come up with a
  // requisition based on its children, which include plugin windows.  Since
  // we don't want to prevent resizing smaller than a plugin window, we need to
  // control the size ourself.
  static void HandleSizeRequest(GtkWidget* widget,
                                GtkRequisition* req,
                                WebWidgetHost* host) {
    // This is arbitrary, but the WebKit scrollbars try to shrink themselves
    // if the browser window is too small.  Give them some space.
    static const int kMinWidthHeight = 64;

    req->width = kMinWidthHeight;
    req->height = kMinWidthHeight;
  }

  // Our size has changed.
  static void HandleSizeAllocate(GtkWidget* widget,
                                 GtkAllocation* allocation,
                                 WebWidgetHost* host) {
    host->Resize(WebSize(allocation->width, allocation->height));
  }

  // Size, position, or stacking of the GdkWindow changed.
  static gboolean HandleConfigure(GtkWidget* widget,
                                  GdkEventConfigure* config,
                                  WebWidgetHost* host) {
    host->Resize(WebSize(config->width, config->height));
    return FALSE;
  }

  // A portion of the GdkWindow needs to be redraw.
  static gboolean HandleExpose(GtkWidget* widget,
                               GdkEventExpose* expose,
                               WebWidgetHost* host) {
    // See comments above about what g_handling_expose is for.
    g_handling_expose = true;
    gfx::Rect rect(expose->area);
    host->UpdatePaintRect(rect);
    host->Paint();
    g_handling_expose = false;
    return FALSE;
  }

  // The GdkWindow was destroyed.
  static gboolean HandleDestroy(GtkWidget* widget, void* unused) {
    // The associated WebWidgetHost instance may have already been destroyed.
    WebWidgetHost* host = static_cast<WebWidgetHost*>(
        g_object_get_data(G_OBJECT(widget), kWebWidgetHostKey));
    if (host)
      host->WindowDestroyed();
    return FALSE;
  }

  // Keyboard key pressed.
  static gboolean HandleKeyPress(GtkWidget* widget,
                                 GdkEventKey* event,
                                 WebWidgetHost* host) {
    host->webwidget()->handleInputEvent(
        WebInputEventFactory::keyboardEvent(event));

    // In the browser we do a ton of work with IMEs.  This is some minimal
    // code to make basic text work in test_shell, but doesn't cover IME.
    // This is a copy of the logic in ProcessUnfilteredKeyPressEvent in
    // render_widget_host_view_gtk.cc .
    if (event->type == GDK_KEY_PRESS) {
      WebKeyboardEvent wke = WebInputEventFactory::keyboardEvent(event);
      if (wke.text[0]) {
        wke.type = WebKit::WebInputEvent::Char;
        host->webwidget()->handleInputEvent(wke);
      }
    }

    return FALSE;
  }

  // Keyboard key released.
  static gboolean HandleKeyRelease(GtkWidget* widget,
                                   GdkEventKey* event,
                                   WebWidgetHost* host) {
    return HandleKeyPress(widget, event, host);
  }

  // This signal is called when arrow keys or tab is pressed.  If we return
  // true, we prevent focus from being moved to another widget.  If we want to
  // allow focus to be moved outside of web contents, we need to implement
  // WebViewDelegate::TakeFocus in the test webview delegate.
  static gboolean HandleFocus(GtkWidget* widget,
                              GdkEventFocus* focus,
                              WebWidgetHost* host) {
    return TRUE;
  }

  // Keyboard focus entered.
  static gboolean HandleFocusIn(GtkWidget* widget,
                                GdkEventFocus* focus,
                                WebWidgetHost* host) {
    // Ignore focus calls in layout test mode so that tests don't mess with each
    // other's focus when running in parallel.
    if (!TestShell::layout_test_mode())
      host->webwidget()->setFocus(true);
    return FALSE;
  }

  // Keyboard focus left.
  static gboolean HandleFocusOut(GtkWidget* widget,
                                 GdkEventFocus* focus,
                                 WebWidgetHost* host) {
    // Ignore focus calls in layout test mode so that tests don't mess with each
    // other's focus when running in parallel.
    if (!TestShell::layout_test_mode())
      host->webwidget()->setFocus(false);
    return FALSE;
  }

  // Mouse button down.
  static gboolean HandleButtonPress(GtkWidget* widget,
                                    GdkEventButton* event,
                                    WebWidgetHost* host) {
    host->webwidget()->handleInputEvent(
        WebInputEventFactory::mouseEvent(event));
    return FALSE;
  }

  // Mouse button up.
  static gboolean HandleButtonRelease(GtkWidget* widget,
                                      GdkEventButton* event,
                                      WebWidgetHost* host) {
    return HandleButtonPress(widget, event, host);
  }

  // Mouse pointer movements.
  static gboolean HandleMotionNotify(GtkWidget* widget,
                                     GdkEventMotion* event,
                                     WebWidgetHost* host) {
    host->webwidget()->handleInputEvent(
        WebInputEventFactory::mouseEvent(event));
    return FALSE;
  }

  // Mouse scroll wheel.
  static gboolean HandleScroll(GtkWidget* widget,
                               GdkEventScroll* event,
                               WebWidgetHost* host) {
    host->webwidget()->handleInputEvent(
        WebInputEventFactory::mouseWheelEvent(event));
    return FALSE;
  }

  DISALLOW_IMPLICIT_CONSTRUCTORS(WebWidgetHostGtkWidget);
};

}  // namespace

// This is provided so that the webview can reuse the custom GTK window code.
// static
gfx::NativeView WebWidgetHost::CreateWidget(
    gfx::NativeView parent_view, WebWidgetHost* host) {
  return WebWidgetHostGtkWidget::CreateNewWidget(parent_view, host);
}

// static
WebWidgetHost* WebWidgetHost::Create(GtkWidget* parent_view,
                                     WebWidgetClient* client) {
  WebWidgetHost* host = new WebWidgetHost();
  host->view_ = CreateWidget(parent_view, host);
  host->webwidget_ = WebPopupMenu::create(client);
  // We manage our own double buffering because we need to be able to update
  // the expose area in an ExposeEvent within the lifetime of the event handler.
  gtk_widget_set_double_buffered(GTK_WIDGET(host->view_), false);

  return host;
}

void WebWidgetHost::UpdatePaintRect(const gfx::Rect& rect) {
  paint_rect_ = paint_rect_.Union(rect);
}

void WebWidgetHost::DidInvalidateRect(const gfx::Rect& damaged_rect) {
  DLOG_IF(WARNING, painting_) << "unexpected invalidation while painting";

  UpdatePaintRect(damaged_rect);

  if (!g_handling_expose) {
    gtk_widget_queue_draw_area(GTK_WIDGET(view_), damaged_rect.x(),
        damaged_rect.y(), damaged_rect.width(), damaged_rect.height());
  }
}

void WebWidgetHost::DidScrollRect(int dx, int dy, const gfx::Rect& clip_rect) {
  // This is used for optimizing painting when the renderer is scrolled. We're
  // currently not doing any optimizations so just invalidate the region.
  DidInvalidateRect(clip_rect);
}

WebWidgetHost::WebWidgetHost()
    : view_(NULL),
      webwidget_(NULL),
      scroll_dx_(0),
      scroll_dy_(0) {
  set_painting(false);
}

WebWidgetHost::~WebWidgetHost() {
  // We may be deleted before the view_. Clear out the signals so that we don't
  // attempt to invoke something on a deleted object.
  g_object_set_data(G_OBJECT(view_), kWebWidgetHostKey, NULL);
  g_signal_handlers_disconnect_matched(view_,
      G_SIGNAL_MATCH_DATA, 0, 0, NULL, NULL, this);
  webwidget_->close();
}

void WebWidgetHost::Resize(const gfx::Size &newsize) {
  // The pixel buffer backing us is now the wrong size
  canvas_.reset();
  logical_size_ = newsize;
  webwidget_->resize(newsize);
}

void WebWidgetHost::Paint() {
  int width = logical_size_.width();
  int height = logical_size_.height();
  gfx::Rect client_rect(width, height);

  // Allocate a canvas if necessary
  if (!canvas_.get()) {
    ResetScrollRect();
    paint_rect_ = client_rect;
    canvas_.reset(new skia::PlatformCanvas(width, height, true));
    if (!canvas_.get()) {
      // memory allocation failed, we can't paint.
      LOG(ERROR) << "Failed to allocate memory for " << width << "x" << height;
      return;
    }
  }

  // This may result in more invalidation
  webwidget_->layout();

  // Paint the canvas if necessary.  Allow painting to generate extra rects the
  // first time we call it.  This is necessary because some WebCore rendering
  // objects update their layout only when painted.
  // Store the total area painted in total_paint. Then tell the gdk window
  // to update that area after we're done painting it.
  gfx::Rect total_paint;
  for (int i = 0; i < 2; ++i) {
    paint_rect_ = client_rect.Intersect(paint_rect_);
    if (!paint_rect_.IsEmpty()) {
      gfx::Rect rect(paint_rect_);
      paint_rect_ = gfx::Rect();

      DLOG_IF(WARNING, i == 1) << "painting caused additional invalidations";
      PaintRect(rect);
      total_paint = total_paint.Union(rect);
    }
  }
  DCHECK(paint_rect_.IsEmpty());

  // Invalidate the paint region on the widget's underlying gdk window. Note
  // that gdk_window_invalidate_* will generate extra expose events, which
  // we wish to avoid. So instead we use calls to begin_paint/end_paint.
  GdkRectangle grect = {
      total_paint.x(),
      total_paint.y(),
      total_paint.width(),
      total_paint.height(),
  };
  GdkWindow* window = view_->window;
  gdk_window_begin_paint_rect(window, &grect);

  // BitBlit to the gdk window.
  cairo_t* source_surface = canvas_->beginPlatformPaint();
  cairo_t* cairo_drawable = gdk_cairo_create(window);
  cairo_set_source_surface(cairo_drawable, cairo_get_target(source_surface),
                           0, 0);
  cairo_paint(cairo_drawable);
  cairo_destroy(cairo_drawable);

  gdk_window_end_paint(window);
}

WebScreenInfo WebWidgetHost::GetScreenInfo() {
  Display* display = test_shell_x11::GtkWidgetGetDisplay(view_);
  int screen_num = test_shell_x11::GtkWidgetGetScreenNum(view_);
  return WebScreenInfoFactory::screenInfo(display, screen_num);
}

void WebWidgetHost::ResetScrollRect() {
  // This method is only needed for optimized scroll painting, which we don't
  // care about in the test shell, yet.
}

void WebWidgetHost::PaintRect(const gfx::Rect& rect) {
  set_painting(true);
  webwidget_->paint(canvas_.get(), rect);
  set_painting(false);
}

void WebWidgetHost::WindowDestroyed() {
  delete this;
}