#define BOOST_PYTHON_STATIC_LIB

#include <boost/python.hpp>

#include <API/WebCore.h>

#include "boost_python_helpers/gil.hpp"
#include "PyNpObject.h"

namespace python = boost::python;
using namespace python;

using namespace Awesomium;

//-

#define NPN_CreateObject Awesomium::WebBindings::createObject
#define NPN_ReleaseObject Awesomium::WebBindings::releaseObject

//-

namespace {

  class WebViewListenerWrap : public WebViewListener, public wrapper<WebViewListener>
  {
    WebView& view;

  public:
    WebViewListenerWrap( WebView& view_ )
      : view( view_ )
    {
    }

		virtual ~WebViewListenerWrap() {}

		virtual void onBeginNavigation(const std::string& url, const std::wstring& frameName)
    {
      lock_gil pylock;
      this->get_override("onBeginNavigation")(url, frameName);
    }

		virtual void onBeginLoading(const std::string& url, const std::wstring& frameName, int statusCode, const std::wstring& mimeType)
    {
      lock_gil pylock;
      this->get_override("onBeginLoading")(url, frameName, statusCode, mimeType);
    }

		virtual void onFinishLoading()
    {
      lock_gil pylock;
      this->get_override("onFinishLoading")();
    }

		virtual void onCallback(const std::string& name, const Awesomium::JSArguments& jsArguments)
    {
      lock_gil pylock;

      python::list arguments;

      Awesomium::JSArguments::const_iterator argument;
      Awesomium::JSArguments::const_iterator end = jsArguments.end();

      for( argument = jsArguments.begin(); argument != end; ++argument )
      {
        if( argument->isNPObject() )
        {
          NPVariant variant;
          OBJECT_TO_NPVARIANT( static_cast<NPObject*>( argument->toNPObject() ), variant);
          arguments.append( ::convert( &view, variant ) );
        }
        else
         arguments.append( python::object( *argument ) );
      }

      this->get_override("onCallback")( name, arguments );
    }

		virtual void onReceiveTitle(const std::wstring& title, const std::wstring& frameName)
    {
      lock_gil pylock;
      this->get_override("onReceiveTitle")(title, frameName);
    }

		virtual void onChangeTooltip(const std::wstring& tooltip)
    {
      lock_gil pylock;
      this->get_override("onChangeTooltip")(tooltip);
    }

#if defined(_WIN32)
		
    virtual void onChangeCursor(HCURSOR cursor)
    {
      lock_gil pylock;
      std::size_t cursor_as_integer = reinterpret_cast<std::size_t>(cursor);
      this->get_override("onChangeCursor")(cursor_as_integer);
    }

#endif

		virtual void onChangeKeyboardFocus(bool isFocused)
    {
      lock_gil pylock;
    }

		virtual void onChangeTargetURL(const std::string& url)
    {
      lock_gil pylock;
    }

    virtual void onCreateNewWebView()
    {
      lock_gil pylock;
    }
	};

  void WebView_render_wrapper(WebView& view, python::object buffer)
  {
    unsigned char* pixels = NULL;

    {
      lock_gil pylock;

      if (PyString_CheckExact(buffer.ptr()) == 0)
      {
        throw std::runtime_error("Invalid argument: 'buffer' must be a string used as a memory buffer.");
      }

      Py_ssize_t string_size = PyString_Size(buffer.ptr());
      // TODO check buffer size

      pixels = reinterpret_cast<unsigned char*>(PyString_AsString(buffer.ptr()));
    }

    allow_threading_guard pyunlock;

    view.render(pixels);
  }

  void WebView_render_begin_wrapper(WebView& view, python::object buffer)
  {
    unsigned char* pixels = NULL;
    
    {
      lock_gil pylock;

      if (PyString_CheckExact(buffer.ptr()) == 0)
      {
        throw std::runtime_error("Invalid argument: 'buffer' must be a string used as a memory buffer.");
      }

      Py_ssize_t string_size = PyString_Size(buffer.ptr());
      // TODO check buffer size

      pixels = reinterpret_cast<unsigned char*>(PyString_AsString(buffer.ptr()));
    }

    allow_threading_guard pyunlock;

    view.render_begin(pixels);
  }

  //-

  struct JSValue_to_python
  {
    static PyObject* convert(JSValue const& value)
    {
      lock_gil pylock;

      if (value.isNull())
        return boost::python::incref(boost::python::object().ptr());
      if (value.isBoolean())
        return boost::python::incref(boost::python::object(value.toBoolean()).ptr());
      if (value.isInteger())
        return boost::python::incref(boost::python::object(value.toInteger()).ptr());
      if (value.isDouble())
        return boost::python::incref(boost::python::object(value.toDouble()).ptr());
      if (value.isString())
        return boost::python::incref(boost::python::object(value.toString()).ptr());
      if (value.isNPObject())
      {
        // if we convert here, we're unable to link the javascript object with its 'core thread' for communication
        throw std::runtime_error("Javascript objects cannot be converted by other means than 'evaluateJavascriptWithResult'");
        /*
        NPVariant variant;
        OBJECT_TO_NPVARIANT( static_cast<NPObject*>( value.toNPObject() ), variant);
        python::object pyvalue = ::convert(NULL, variant);
        return boost::python::incref(pyvalue.ptr());
        */
      }
      throw std::runtime_error("JSValue value type has no to_python converter.");
    }
  };

  struct JSValue_from_python
  {
    JSValue_from_python()
    {
      boost::python::converter::registry::push_back(
        &convertible,
        &construct,
        boost::python::type_id<JSValue>());
    }

    static void* convertible(PyObject* obj_ptr)
    {
      return obj_ptr;   // every object is convertible
    }

    static void construct(
      PyObject* obj_ptr,
      boost::python::converter::rvalue_from_python_stage1_data* data)
    {
      lock_gil pylock;

      void* storage = (
        (boost::python::converter::rvalue_from_python_storage<JSValue>*)
          data)->storage.bytes;

      if (obj_ptr == Py_None) new (storage) JSValue();
      else if (PyBool_Check(obj_ptr)) new (storage) JSValue( obj_ptr == Py_True );
      else if (PyInt_CheckExact(obj_ptr)) new (storage) JSValue( PyInt_AsLong(obj_ptr) );
      else if (PyLong_CheckExact(obj_ptr)) new (storage) JSValue( PyLong_AsLong(obj_ptr) );
      else if (PyFloat_Check(obj_ptr)) new (storage) JSValue( PyFloat_AsDouble(obj_ptr) );
      else if (PyString_Check(obj_ptr)) new (storage) JSValue( PyString_AsString(obj_ptr) );
      else {
        NPObject* result_object = NPN_CreateObject(NULL, GET_NPOBJECT_CLASS(PyNpObject));
        try
        {
          PyNpObject* pythonScripting = static_cast<PyNpObject*>( result_object );
          pythonScripting->setPythonObject( python::object( python::borrowed(obj_ptr) ) );
          new (storage) JSValue(result_object);
          NPN_ReleaseObject(result_object);
        }
        catch(...)
        {
          NPN_ReleaseObject(result_object);
        }
      }
      data->convertible = storage;
    }
    
  };

  //-

  struct JSArguments_to_python
  {
    static PyObject* convert(JSArguments const& value)
    {
      lock_gil pylock;

      python::list arguments;

      for (std::size_t i=0; i<value.size(); i++)
      {
        arguments.append( python::object(value[i]) );
      }

      return boost::python::incref( python::tuple( arguments ).ptr() );
    }
  };

  //-

  void executeJavascript_wrapper(WebView& view, const std::string& javascript, const std::wstring& frameName)
  {
    // 'allow_threads' does not work with BOOST_PYTHON_MEMBER_FUNCTION_OVERLOADS, the overloads have to manually exposed.
    allow_threading_guard pyunlock;
    return view.executeJavascript(javascript, frameName);
  }

  void executeJavascript_wrapper(WebView& view, const std::string& javascript)
  {
    return executeJavascript_wrapper(view, javascript, L"");
  }

  void syncExecuteJavascript_wrapper(WebView& view, const std::string& javascript, const std::wstring& frameName)
  {
    // 'allow_threads' does not work with BOOST_PYTHON_MEMBER_FUNCTION_OVERLOADS, the overloads have to manually exposed.
    allow_threading_guard pyunlock;
    return view.syncExecuteJavascript(javascript, frameName);
  }

  void syncExecuteJavascript_wrapper(WebView& view, const std::string& javascript)
  {
    return syncExecuteJavascript_wrapper(view, javascript, L"");
  }

  python::object WebView_executeJavascriptWithResult_wrapper(WebView& view, const std::string& javascript, const std::wstring& frameName)
  {
    FutureJSValue result;
    JSValue value;
    {
      allow_threading_guard pyunlock;

      result = view.executeJavascriptWithResult(javascript, frameName);
      value = result.get();
    }
    if (value.isNPObject())
    {
      lock_gil pylock;

      NPVariant variant;
      OBJECT_TO_NPVARIANT( static_cast<NPObject*>( value.toNPObject() ), variant);
      python::object pyvalue = ::convert( &view, variant );
      return pyvalue;
    }
    else
    {
      return python::object( value );
    }
  }

  class MessageLogger_wrapper
    : public WebView::MessageLogger,
      public python::wrapper< WebView::MessageLogger >
  {
  public:
    MessageLogger_wrapper() {}
    virtual ~MessageLogger_wrapper() {}

    virtual void log(char const* message)
    {
      lock_gil pylock;
      this->get_override("log")(message);
    }
  };
};

BOOST_PYTHON_MEMBER_FUNCTION_OVERLOADS(syncExecuteJavascript_overloads, syncExecuteJavascript, 1, 2);

BOOST_PYTHON_MODULE( pyAwesomium )
{
  PyEval_InitThreads();
  
  initjs_to_python();

  //-

  python::to_python_converter<
    JSValue,
    JSValue_to_python>();

  JSValue_from_python();

  python::to_python_converter<
    JSArguments,
    JSArguments_to_python>();

  //-

  python::enum_<MouseButton>("MouseButton")
    .value( "LEFT_MOUSE_BTN", LEFT_MOUSE_BTN )
    .value( "MIDDLE_MOUSE_BTN", MIDDLE_MOUSE_BTN )
    .value( "RIGHT_MOUSE_BTN", RIGHT_MOUSE_BTN )
    ;

  python::class_<WebView, boost::noncopyable> WebView_class("WebView", no_init);
  {
    python::scope WebView_scope( WebView_class );
    {
      python::class_< MessageLogger_wrapper, boost::noncopyable >( "MessageLogger" )
        .def( "log", &MessageLogger_wrapper::log )
        ;
    }
    {
      python::enum_<WebView::TransparencyMode>("TransparencyMode")
        .value( "TM_OPAQUE", WebView::TM_OPAQUE )
        .value( "TM_CANVAS_ALPHA_CHANNEL", WebView::TM_CANVAS_ALPHA_CHANNEL )
        .value( "TM_DEDUCE_ALPHA_CHANNEL", WebView::TM_DEDUCE_ALPHA_CHANNEL )
        ;
    }
  }

  void (*executeJavascript_wrapper_fn_3)(WebView& view, const std::string& javascript, const std::wstring& frameName) = executeJavascript_wrapper;
  void (*executeJavascript_wrapper_fn_2)(WebView& view, const std::string& javascript) = executeJavascript_wrapper;
  void (*syncExecuteJavascript_wrapper_fn_3)(WebView& view, const std::string& javascript, const std::wstring& frameName) = syncExecuteJavascript_wrapper;
  void (*syncExecuteJavascript_wrapper_fn_2)(WebView& view, const std::string& javascript) = syncExecuteJavascript_wrapper;

  WebView_class
    .def( "getListener", allow_threads(&WebView::getListener), return_value_policy<reference_existing_object>() )
    .def( "setListener", allow_threads(&WebView::setListener))
    .def( "destroy", allow_threads(&WebView::destroy))

    .def( "loadFile", allow_threads(&WebView::loadFile))
    .def( "refresh", allow_threads(&WebView::refresh))
    
    .def( "setProperty", allow_threads(&WebView::setProperty))
    .def( "setCallback", allow_threads(&WebView::setCallback))

    .def( "setMessageLogger", allow_threads(&WebView::setMessageLogger))

    .def( "executeJavascript", executeJavascript_wrapper_fn_3)
    .def( "executeJavascript", executeJavascript_wrapper_fn_2)
    .def( "syncExecuteJavascript", syncExecuteJavascript_wrapper_fn_3)
    .def( "syncExecuteJavascript", syncExecuteJavascript_wrapper_fn_2)
    .def( "executeJavascriptWithResult", &WebView_executeJavascriptWithResult_wrapper )
    
    .def( "setTransparencyMode", allow_threads(&WebView::setTransparencyMode))
    .def( "getTransparencyMode", allow_threads(&WebView::getTransparencyMode))

    .def( "resize", allow_threads(&WebView::resize))
    .def( "isDirty", allow_threads(&WebView::isDirty))
    .def( "render", WebView_render_wrapper )
    .def( "render_begin", WebView_render_begin_wrapper)
    .def( "render_end", allow_threads(&WebView::render_end))
    .def( "syncLayout", allow_threads(&WebView::syncLayout))

    .def( "injectMouseMove", allow_threads(&WebView::injectMouseMove))
    .def( "injectMouseWheelXY", allow_threads(&WebView::injectMouseWheelXY))
    .def( "injectMouseDown", allow_threads(&WebView::injectMouseDown))
    .def( "injectMouseUp", allow_threads(&WebView::injectMouseUp))
    .def( "injectKeyboardEvent", allow_threads(&WebView::injectKeyboardEvent))
    ;

  python::class_<WebCore, boost::noncopyable>("WebCore", no_init )
    .def( init<>() )
    .def( "setBaseDirectory", allow_threads(&WebCore::setBaseDirectory) )
    .def( "setUserAgent", allow_threads(&WebCore::setUserAgent) )
    .def( "createWebView", allow_threads(&WebCore::createWebView), return_value_policy<reference_existing_object>() )
    .def( "update", allow_threads(&WebCore::update) )
    .def( "addExtraPluginDir", allow_threads(&WebCore::addExtraPluginDir) )
    .def( "addExtraPluginPath", allow_threads(&WebCore::addExtraPluginPath) )
    .def( "get", &WebCore::GetPointer, return_value_policy< reference_existing_object >() ).staticmethod( "get" )
    ;

  python::class_< WebViewListenerWrap, boost::noncopyable >( "WebViewListener", no_init )
    .def( init< WebView& >() )
    .def("onBeginNavigation", &WebViewListenerWrap::onBeginNavigation)
    .def("onBeginLoading", &WebViewListenerWrap::onBeginLoading)
    .def("onFinishLoading", &WebViewListenerWrap::onFinishLoading)
    .def("onCallback", &WebViewListenerWrap::onCallback)
    .def("onReceiveTitle", &WebViewListenerWrap::onReceiveTitle)
    .def("onChangeTooltip", &WebViewListenerWrap::onChangeTooltip)
    .def("onChangeCursor", &WebViewListenerWrap::onChangeCursor)
    .def("onChangeKeyboardFocus", &WebViewListenerWrap::onChangeKeyboardFocus)
    .def("onChangeTargetURL", &WebViewListenerWrap::onChangeTargetURL)
    .def("onCreateNewWebView", &WebViewListenerWrap::onCreateNewWebView)
    ;
}