// Copyright (c) 2011 The Chromium Embedded Framework Authors.
// Portions 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 "cefclient/osrplugin.h"
#include "cefclient/osrenderer.h"

#if defined(OS_WIN)

#include <windows.h>
#define _USE_MATH_DEFINES
#include <math.h>
#include <gl/gl.h>
#include <gl/glu.h>
#include <sstream>
#include <string>
#include "include/cef_browser.h"
#include "include/cef_frame.h"
#include "cefclient/cefclient.h"
#include "cefclient/client_popup_handler.h"
#include "cefclient/resource.h"
#include "cefclient/resource_util.h"
#include "cefclient/string_util.h"
#include "cefclient/util.h"

// Initialized in NP_Initialize.
NPNetscapeFuncs* g_osrbrowser = NULL;

namespace {

CefRefPtr<CefBrowser> g_offscreenBrowser;

// If set to true alpha transparency will be used.
bool g_offscreenTransparent = false;

// Class holding pointers for the client plugin window.
class ClientPlugin {
 public:
  ClientPlugin(bool transparent)
    : renderer(transparent),
      hWnd(NULL),
      hDC(NULL),
      hRC(NULL) {
  }

  ClientOSRenderer renderer;
  HWND hWnd;
  HDC hDC;
  HGLRC hRC;
};

// Handler for off-screen rendering windows.
class ClientOSRHandler : public CefClient,
                         public CefLifeSpanHandler,
                         public CefLoadHandler,
                         public CefRequestHandler,
                         public CefDisplayHandler,
                         public CefRenderHandler {
 public:
  explicit ClientOSRHandler(ClientPlugin* plugin)
    : plugin_(plugin),
      want_pixel_value_(false) {
  }
  ~ClientOSRHandler() {
  }

  // Called immediately before the plugin is destroyed.
  void Detach() {
    plugin_ = NULL;
  }

  // Retrieve the pixel value at the requested coordinate the next time that
  // the view is painted.
  void WantPixelValue(int x, int y) {
    if (!want_pixel_value_) {
      want_pixel_value_ = true;

      // Trigger a call to OnPaint.
      if (g_offscreenBrowser.get())
        g_offscreenBrowser->Invalidate(CefRect(0,0,1,1));
    }

    coord_x_ = x;
    coord_y_ = y;
  }

  // CefClient methods
  virtual CefRefPtr<CefLifeSpanHandler> GetLifeSpanHandler() OVERRIDE {
    return this;
  }
  virtual CefRefPtr<CefLoadHandler> GetLoadHandler() OVERRIDE {
    return this;
  }
  virtual CefRefPtr<CefRequestHandler> GetRequestHandler() OVERRIDE {
    return this;
  }
  virtual CefRefPtr<CefDisplayHandler> GetDisplayHandler() OVERRIDE {
    return this;
  }
  virtual CefRefPtr<CefRenderHandler> GetRenderHandler() OVERRIDE {
    return this;
  }

  // CefLifeSpanHandler methods

  virtual bool OnBeforePopup(CefRefPtr<CefBrowser> parentBrowser,
                             const CefPopupFeatures& popupFeatures,
                             CefWindowInfo& windowInfo,
                             const CefString& url,
                             CefRefPtr<CefClient>& client,
                             CefBrowserSettings& settings) OVERRIDE {
    REQUIRE_UI_THREAD();

    windowInfo.m_bWindowRenderingDisabled = TRUE;
    client = new ClientPopupHandler(g_offscreenBrowser);
    return false;
  }

  virtual void OnAfterCreated(CefRefPtr<CefBrowser> browser) OVERRIDE {
    REQUIRE_UI_THREAD();

    g_offscreenBrowser = browser;

    // Set the off-screen browser size to match the plugin window size.
    RECT clientRect;
    ::GetClientRect(plugin_->hWnd, &clientRect);
    g_offscreenBrowser->SetSize(PET_VIEW, clientRect.right, clientRect.bottom);
  }

  virtual void OnBeforeClose(CefRefPtr<CefBrowser> browser)  OVERRIDE {
    g_offscreenBrowser = NULL;
  }

  // CefLoadHandler methods

  virtual void OnLoadStart(CefRefPtr<CefBrowser> browser,
                           CefRefPtr<CefFrame> frame) OVERRIDE {
    REQUIRE_UI_THREAD();

    if (!browser->IsPopup() && frame->IsMain()) {
      // We've just started loading a page
      SetLoading(true);
    }
  }

  virtual void OnLoadEnd(CefRefPtr<CefBrowser> browser,
                         CefRefPtr<CefFrame> frame,
                         int httpStatusCode) OVERRIDE {
    REQUIRE_UI_THREAD();

    if (!browser->IsPopup() && frame->IsMain()) {
      // We've just finished loading a page
      SetLoading(false);
    }
  }

  // CefRequestHandler methods

  virtual bool OnBeforeResourceLoad(CefRefPtr<CefBrowser> browser,
                                    CefRefPtr<CefRequest> request,
                                    CefString& redirectUrl,
                                    CefRefPtr<CefStreamReader>& resourceStream,
                                    CefRefPtr<CefResponse> response,
                                    int loadFlags) OVERRIDE {
    REQUIRE_IO_THREAD();

    std::string url = request->GetURL();
    if (url == "http://tests/transparency") {
      resourceStream = GetBinaryResourceReader(IDS_TRANSPARENCY);
      response->SetMimeType("text/html");
      response->SetStatus(200);
    }

    return false;
  }

  // CefDisplayHandler methods

  virtual void OnNavStateChange(CefRefPtr<CefBrowser> browser,
                                bool canGoBack,
                                bool canGoForward) OVERRIDE {
    REQUIRE_UI_THREAD();

    // Set the "back" and "forward" button state in the HTML.
    std::stringstream ss;
    ss << "document.getElementById('back').disabled = "
       << (canGoBack?"false":"true") << ";";
    ss << "document.getElementById('forward').disabled = "
       << (canGoForward?"false":"true") << ";";
    AppGetBrowser()->GetMainFrame()->ExecuteJavaScript(ss.str(), "", 0);
  }

  virtual void OnAddressChange(CefRefPtr<CefBrowser> browser,
                               CefRefPtr<CefFrame> frame,
                               const CefString& url) OVERRIDE {
    REQUIRE_UI_THREAD();

    // Set the "url" value in the HTML.
    std::stringstream ss;
    std::string urlStr = url;
    StringReplace(urlStr, "'", "\\'");
    ss << "document.getElementById('url').value = '" << urlStr.c_str() << "'";
    AppGetBrowser()->GetMainFrame()->ExecuteJavaScript(ss.str(), "", 0);
  }

  virtual void OnTitleChange(CefRefPtr<CefBrowser> browser,
                             const CefString& title) OVERRIDE {
    REQUIRE_UI_THREAD();

    // Set the "title" value in the HTML.
    std::stringstream ss;
    std::string titleStr = title;
    StringReplace(titleStr, "'", "\\'");
    ss << "document.getElementById('title').innerHTML = '" <<
        titleStr.c_str() << "'";
    AppGetBrowser()->GetMainFrame()->ExecuteJavaScript(ss.str(), "", 0);
  }

  // CefRenderHandler methods

  virtual bool GetViewRect(CefRefPtr<CefBrowser> browser,
                           CefRect& rect) OVERRIDE {
    REQUIRE_UI_THREAD();

    if (!plugin_)
      return false;

    // The simulated screen and view rectangle are the same. This is necessary
    // for popup menus to be located and sized inside the view.
    RECT clientRect;
    ::GetClientRect(plugin_->hWnd, &clientRect);

    rect.x = rect.y = 0;
    rect.width = clientRect.right;
    rect.height = clientRect.bottom;
    return true;
  }

  virtual bool GetScreenRect(CefRefPtr<CefBrowser> browser,
                             CefRect& rect) OVERRIDE {
    return GetViewRect(browser, rect);
  }

  virtual bool GetScreenPoint(CefRefPtr<CefBrowser> browser,
                              int viewX,
                              int viewY,
                              int& screenX,
                              int& screenY) OVERRIDE {
    REQUIRE_UI_THREAD();

    if (!plugin_)
      return false;

    // Convert the point from view coordinates to actual screen coordinates.
    POINT screen_pt = {viewX, viewY};
    MapWindowPoints(plugin_->hWnd, HWND_DESKTOP, &screen_pt, 1);
    screenX = screen_pt.x;
    screenY = screen_pt.y;
    return true;
  }

  virtual void OnPopupShow(CefRefPtr<CefBrowser> browser,
                           bool show) OVERRIDE {
    REQUIRE_UI_THREAD();

    if (!plugin_)
      return;

    plugin_->renderer.OnPopupShow(browser, show);
  }

  virtual void OnPopupSize(CefRefPtr<CefBrowser> browser,
                           const CefRect& rect) OVERRIDE {
    REQUIRE_UI_THREAD();

    if (!plugin_)
      return;

    plugin_->renderer.OnPopupSize(browser, rect);
  }

  virtual void OnPaint(CefRefPtr<CefBrowser> browser,
                       PaintElementType type,
                       const RectList& dirtyRects,
                       const void* buffer) OVERRIDE {
    REQUIRE_UI_THREAD();

    if (!plugin_)
      return;

    wglMakeCurrent(plugin_->hDC, plugin_->hRC);
    plugin_->renderer.OnPaint(browser, type, dirtyRects, buffer);
    plugin_->renderer.Render();

    SwapBuffers(plugin_->hDC);

    if (type == PET_VIEW && want_pixel_value_) {
      // Retrieve the pixel value at the requested coordinate.
      int width = plugin_->renderer.GetViewWidth();
      int height = plugin_->renderer.GetViewHeight();

      if(coord_x_ >= 0 && coord_x_ < width &&
         coord_y_ >= 0 && coord_y_ < height) {
        bool transparent = plugin_->renderer.IsTransparent();

        int index = (width * coord_y_ * 4) + (coord_x_ * 4);
        const unsigned char *pBuf = static_cast<const unsigned char*>(buffer);
        unsigned char r, g, b, a;

        b = pBuf[index];
        g = pBuf[index+1];
        r = pBuf[index+2];
        if (transparent)
          a = pBuf[index+3];

        std::stringstream ss;
        ss << coord_x_ << "," << coord_y_ << " = R:" << static_cast<int>(r)
            << " G:" << static_cast<int>(g) << " B:" << static_cast<int>(b);
        if (transparent)
          ss << " A:" << static_cast<int>(a);

        std::string js =
            "document.getElementById('pixel').innerText = '" + ss.str() + "';";
        AppGetBrowser()->GetMainFrame()->ExecuteJavaScript(js, "", 0);
      }

      want_pixel_value_ = false;
    }
  }

  virtual void OnCursorChange(CefRefPtr<CefBrowser> browser,
                              CefCursorHandle cursor) OVERRIDE {
    REQUIRE_UI_THREAD();

    if (!plugin_)
      return;

    // Change the plugin window's cursor.
    SetClassLong(plugin_->hWnd, GCL_HCURSOR,
        static_cast<LONG>(reinterpret_cast<LONG_PTR>(cursor)));
    SetCursor(cursor);
  }

 private:
  void SetLoading(bool isLoading) {
    // Set the "stop" and "reload" button state in the HTML.
    std::stringstream ss;
    ss << "document.getElementById('stop').disabled = "
       << (isLoading?"false":"true") << ";"
       << "document.getElementById('reload').disabled = "
       << (isLoading?"true":"false") << ";";
    AppGetBrowser()->GetMainFrame()->ExecuteJavaScript(ss.str(), "", 0);
  }

  ClientPlugin* plugin_;

  // Used for retrieving the pixel value.
  bool want_pixel_value_;
  int coord_x_;
  int coord_y_;

  // Include the default reference counting implementation.
  IMPLEMENT_REFCOUNTING(ClientOSRPlugin);
};

// Forward declarations of functions included in this code module:
LRESULT CALLBACK PluginWndProc(HWND hWnd, UINT message, WPARAM wParam,
                               LPARAM lParam);

// Enable GL.
void EnableGL(ClientPlugin* plugin) {
  PIXELFORMATDESCRIPTOR pfd;
  int format;

  // Get the device context.
  plugin->hDC = GetDC(plugin->hWnd);

  // Set the pixel format for the DC.
  ZeroMemory(&pfd, sizeof(pfd));
  pfd.nSize = sizeof(pfd);
  pfd.nVersion = 1;
  pfd.dwFlags = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER;
  pfd.iPixelType = PFD_TYPE_RGBA;
  pfd.cColorBits = 24;
  pfd.cDepthBits = 16;
  pfd.iLayerType = PFD_MAIN_PLANE;
  format = ChoosePixelFormat(plugin->hDC, &pfd);
  SetPixelFormat(plugin->hDC, format, &pfd);

  // Create and enable the render context.
  plugin->hRC = wglCreateContext(plugin->hDC);
  wglMakeCurrent(plugin->hDC, plugin->hRC);

  plugin->renderer.Initialize();
}

// Disable GL.
void DisableGL(ClientPlugin* plugin) {
  plugin->renderer.Cleanup();

  wglMakeCurrent(NULL, NULL);
  wglDeleteContext(plugin->hRC);
  ReleaseDC(plugin->hWnd, plugin->hDC);
}

// Size the GL view.
void SizeGL(ClientPlugin* plugin, int width, int height) {
  if (g_offscreenBrowser.get())
    g_offscreenBrowser->SetSize(PET_VIEW, width, height);
}

NPError NPP_NewImpl(NPMIMEType plugin_type, NPP instance, uint16 mode,
                    int16 argc, char* argn[], char* argv[],
                    NPSavedData* saved) {
  if (instance == NULL)
    return NPERR_INVALID_INSTANCE_ERROR;

  ClientPlugin* plugin = new ClientPlugin(g_offscreenTransparent);
  instance->pdata = reinterpret_cast<void*>(plugin);

  return NPERR_NO_ERROR;
}

NPError NPP_DestroyImpl(NPP instance, NPSavedData** save) {
  ClientPlugin* plugin = reinterpret_cast<ClientPlugin*>(instance->pdata);

  if (plugin) {
    if (g_offscreenBrowser.get()) {
      CefRefPtr<ClientOSRHandler> handler =
          static_cast<ClientOSRHandler*>(g_offscreenBrowser->GetClient().get());
      handler->Detach();
    }
    if (plugin->hWnd) {
      DestroyWindow(plugin->hWnd);
      DisableGL(plugin);
    }
    delete plugin;
  }

  return NPERR_NO_ERROR;
}

NPError NPP_SetWindowImpl(NPP instance, NPWindow* window_info) {
  if (instance == NULL)
    return NPERR_INVALID_INSTANCE_ERROR;

  if (window_info == NULL)
    return NPERR_GENERIC_ERROR;

  ClientPlugin* plugin = reinterpret_cast<ClientPlugin*>(instance->pdata);
  HWND parent_hwnd = reinterpret_cast<HWND>(window_info->window);

  if (plugin->hWnd == NULL) {
    WNDCLASS wc;
    HINSTANCE hInstance = GetModuleHandle(NULL);

    // Register the window class.
    wc.style = CS_OWNDC;
    wc.lpfnWndProc = PluginWndProc;
    wc.cbClsExtra = 0;
    wc.cbWndExtra = 0;
    wc.hInstance = hInstance;
    wc.hIcon = LoadIcon(NULL, IDI_APPLICATION);
    wc.hCursor = LoadCursor(NULL, IDC_ARROW);
    wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
    wc.lpszMenuName = NULL;
    wc.lpszClassName = L"ClientOSRPlugin";
    RegisterClass(&wc);

    // Create the main window.
    plugin->hWnd = CreateWindow(L"ClientOSRPlugin", L"Client OSR Plugin",
        WS_CHILD|WS_CLIPCHILDREN|WS_CLIPSIBLINGS, 0, 0, 0, 0, parent_hwnd, NULL,
        hInstance, NULL);

    SetWindowLongPtr(plugin->hWnd, GWLP_USERDATA,
        reinterpret_cast<LONG_PTR>(plugin));

    // Enable GL drawing for the window.
    EnableGL(plugin);

    // Create the off-screen rendering window.
    CefWindowInfo windowInfo;
    windowInfo.SetAsOffScreen(plugin->hWnd);
    if (g_offscreenTransparent)
      windowInfo.SetTransparentPainting(TRUE);

    CefBrowserSettings settings;
    AppGetBrowserSettings(settings);

    CefBrowser::CreateBrowser(windowInfo, new ClientOSRHandler(plugin),
        "http://www.google.com", settings);
  }

  // Position the plugin window and make sure it's visible.
  RECT parent_rect;
  GetClientRect(parent_hwnd, &parent_rect);
  SetWindowPos(plugin->hWnd, NULL, parent_rect.left, parent_rect.top,
      parent_rect.right - parent_rect.left,
      parent_rect.bottom - parent_rect.top, SWP_SHOWWINDOW);

  UpdateWindow(plugin->hWnd);
  ShowWindow(plugin->hWnd, SW_SHOW);

  return NPERR_NO_ERROR;
}

// Plugin window procedure.
LRESULT CALLBACK PluginWndProc(HWND hWnd, UINT message, WPARAM wParam,
                               LPARAM lParam) {
  static POINT lastMousePos, curMousePos;
  static bool mouseRotation = false;
  static bool mouseTracking = false;

  ClientPlugin* plugin =
      reinterpret_cast<ClientPlugin*>(GetWindowLongPtr(hWnd, GWLP_USERDATA));

  switch (message) {
  case WM_DESTROY:
    // Explicitly close the offscreen browser and release the reference.
    g_offscreenBrowser->CloseBrowser();
    g_offscreenBrowser = NULL;
    return 0;

  case WM_LBUTTONDOWN:
  case WM_RBUTTONDOWN:
    SetCapture(hWnd);
    SetFocus(hWnd);
    if (wParam & MK_SHIFT) {
      // Start rotation effect.
      lastMousePos.x = curMousePos.x = LOWORD(lParam);
      lastMousePos.y = curMousePos.y = HIWORD(lParam);
      mouseRotation = true;
    } else if (wParam & MK_CONTROL) {
      // Retrieve the pixel value.
      int x = LOWORD(lParam);
      int y = HIWORD(lParam);
      CefRefPtr<ClientOSRHandler> handler =
          static_cast<ClientOSRHandler*>(g_offscreenBrowser->GetClient().get());
      handler->WantPixelValue(x, y);
    } else {
      if (g_offscreenBrowser.get()) {
        g_offscreenBrowser->SendMouseClickEvent(LOWORD(lParam), HIWORD(lParam),
            (message == WM_LBUTTONDOWN?MBT_LEFT:MBT_RIGHT), false, 1);
      }
    }
    break;

  case WM_LBUTTONUP:
  case WM_RBUTTONUP:
    if (GetCapture() == hWnd)
      ReleaseCapture();
    if (mouseRotation) {
      // End rotation effect.
      mouseRotation = false;
      plugin->renderer.SetSpin(0, 0);
    } else {
      if (g_offscreenBrowser.get()) {
        g_offscreenBrowser->SendMouseClickEvent(LOWORD(lParam), HIWORD(lParam),
            (message == WM_LBUTTONUP?MBT_LEFT:MBT_RIGHT), true, 1);
      }
    }
    break;

  case WM_MOUSEMOVE:
    if (mouseRotation) {
      // Apply rotation effect.
      curMousePos.x = LOWORD(lParam);
      curMousePos.y = HIWORD(lParam);
      plugin->renderer.IncrementSpin((curMousePos.x - lastMousePos.x),
                                     (curMousePos.y - lastMousePos.y));
      lastMousePos.x = curMousePos.x;
      lastMousePos.y = curMousePos.y;
    } else {
      if (!mouseTracking) {
        // Start tracking mouse leave. Required for the WM_MOUSELEAVE event to
        // be generated.
        TRACKMOUSEEVENT tme;
        tme.cbSize = sizeof(TRACKMOUSEEVENT);
        tme.dwFlags = TME_LEAVE;
        tme.hwndTrack = hWnd;
        TrackMouseEvent(&tme);
        mouseTracking = true;
      }
      if (g_offscreenBrowser.get()) {
        g_offscreenBrowser->SendMouseMoveEvent(LOWORD(lParam), HIWORD(lParam),
            false);
      }
    }
    break;

  case WM_MOUSELEAVE:
    if (mouseTracking) {
      // Stop tracking mouse leave.
      TRACKMOUSEEVENT tme;
      tme.cbSize = sizeof(TRACKMOUSEEVENT);
      tme.dwFlags = TME_LEAVE & TME_CANCEL;
      tme.hwndTrack = hWnd;
      TrackMouseEvent(&tme);
      mouseTracking = false;
    }
    if (g_offscreenBrowser.get())
      g_offscreenBrowser->SendMouseMoveEvent(0, 0, true);
    break;

  case WM_MOUSEWHEEL:
    if (g_offscreenBrowser.get()) {
      g_offscreenBrowser->SendMouseWheelEvent(LOWORD(lParam), HIWORD(lParam),
          0, GET_WHEEL_DELTA_WPARAM(wParam));
    }
    break;

  case WM_SIZE: {
    int width  = LOWORD(lParam);
    int height = HIWORD(lParam);
    if (width > 0 && height > 0)
      SizeGL(plugin, width, height);
    break;
  }

  case WM_SETFOCUS:
  case WM_KILLFOCUS:
    if (g_offscreenBrowser.get())
      g_offscreenBrowser->SendFocusEvent(message == WM_SETFOCUS);
    break;

  case WM_CAPTURECHANGED:
  case WM_CANCELMODE:
    if (!mouseRotation) {
      if (g_offscreenBrowser.get())
        g_offscreenBrowser->SendCaptureLostEvent();
    }
    break;

  case WM_KEYDOWN:
  case WM_KEYUP:
  case WM_SYSKEYDOWN:
  case WM_SYSKEYUP:
  case WM_CHAR:
  case WM_SYSCHAR:
  case WM_IME_CHAR:
    if (g_offscreenBrowser.get()) {
      CefBrowser::KeyType type = KT_CHAR;
      CefKeyInfo keyInfo;
      keyInfo.key = wParam;

      if (message == WM_KEYDOWN || message == WM_SYSKEYDOWN)
        type = KT_KEYDOWN;
      else if (message == WM_KEYUP || message == WM_SYSKEYUP)
        type = KT_KEYUP;

      if (message == WM_SYSKEYDOWN || message == WM_SYSKEYUP ||
          message == WM_SYSCHAR)
        keyInfo.sysChar = true;

      if (message == WM_IME_CHAR)
        keyInfo.imeChar = true;

       g_offscreenBrowser->SendKeyEvent(type, keyInfo, lParam);
    }
    break;

  case WM_PAINT: {
    PAINTSTRUCT ps;
    BeginPaint(hWnd, &ps);
    EndPaint(hWnd, &ps);
    return 0;
  }

  case WM_ERASEBKGND:
    return 0;
  }

  return DefWindowProc(hWnd, message, wParam, lParam);
}

}  // namespace

NPError API_CALL NP_OSRGetEntryPoints(NPPluginFuncs* pFuncs) {
  pFuncs->newp = NPP_NewImpl;
  pFuncs->destroy = NPP_DestroyImpl;
  pFuncs->setwindow = NPP_SetWindowImpl;
  return NPERR_NO_ERROR;
}

NPError API_CALL NP_OSRInitialize(NPNetscapeFuncs* pFuncs) {
  g_osrbrowser = pFuncs;
  return NPERR_NO_ERROR;
}

NPError API_CALL NP_OSRShutdown(void) {
  g_osrbrowser = NULL;
  return NPERR_NO_ERROR;
}

CefRefPtr<CefBrowser> GetOffScreenBrowser() {
  return g_offscreenBrowser;
}

void SetOffScreenTransparent(bool transparent) {
  g_offscreenTransparent = transparent;
}

#endif  // OS_WIN
