/*
 *  npw-player.c - Standalone plugin player
 *
 *  nspluginwrapper (C) 2005-2009 Gwenole Beauchesne
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License along
 *  with this program; if not, write to the Free Software Foundation, Inc.,
 *  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

#include "sysdeps.h"

#include <errno.h>
#include <dlfcn.h>
#include <unistd.h>
#include <glib.h>
#include <glib/gstdio.h>
#include <glib/gthread.h>
#include <gtk/gtk.h>
#include <gdk/gdkx.h>
#include <gdk/gdkkeysyms.h>

#include "rpc.h"
#include "utils.h"
#include "glibcurl.h"
#include "gtk2xtbin.h"
#include "npw-rpc.h"

#define XP_UNIX 1
#define MOZ_X11 1
#include <npapi.h>
#include <npfunctions.h>
#include <npruntime.h>

#define DEBUG 1
#include "debug.h"

// Define to use XEMBED
#define USE_XEMBED 1

#define DEFAULT_WIDTH  640
#define DEFAULT_HEIGHT 480

enum
  {
    BACKEND_GTK = 1,
  };

static gboolean g_verbose     = FALSE;
static guint g_backend        = BACKEND_GTK;
static guint g_xid            = 0;
static guint g_n_plugins      = 0;

typedef struct _Plugin		Plugin;
typedef struct _PluginDataType	PluginDataType;
typedef struct _StreamInstance  StreamInstance;
typedef struct _StreamBuffer    StreamBuffer;
typedef struct _PlayerApp	PlayerApp;
typedef struct _GtkDisplay	GtkDisplay;

#define GTK_DISPLAY(display)		((GtkDisplay *)(display))
#define APP_GTK_DISPLAY(app)		GTK_DISPLAY ((app)->display)
#define DISPLAY_WIDTH(display)		GTK_DISPLAY (display)->width
#define DISPLAY_HEIGHT(display)		GTK_DISPLAY (display)->height

struct _GtkDisplay
{
  GtkWidget *window;
  GtkWidget *canvas;
  gint width;
  gint height;
};

struct _PlayerApp
{
  GtkWidget  *window;
  gpointer    display;
  guint       width;
  guint       height;
  guint16     mode;
  GHashTable *attrs;
  Plugin     *plugin;
  GList      *plugins;
};

struct _Plugin
{
  gchar                   *path;
  gchar                   *src;
  gchar                   *mime_type;
  void                    *module;
  GList                   *data_types;
  GtkWidget               *window;
  gboolean                 use_xembed;

  NPP                       instance;
  NPWindow                  np_window;
  NP_InitializeFunc         NP_Initialize;
  NP_ShutdownFunc           NP_Shutdown;
  NP_GetValueFunc           NP_GetValue;
  NP_GetMIMEDescriptionFunc NP_GetMIMEDescription;
  NPPluginFuncs             plugin_funcs;
  NPNetscapeFuncs           mozilla_funcs;
};

enum
  {
    PLUGIN_DATA_TYPE_MIME        = 0,
    PLUGIN_DATA_TYPE_EXTENSION   = 1,
    PLUGIN_DATA_TYPE_DESCRIPTION = 2,
    PLUGIN_DATA_TYPE_VALUE_COUNT
  };

struct _PluginDataType
{
  gchar *values[PLUGIN_DATA_TYPE_VALUE_COUNT];
};

enum
  {
    STREAM_STATUS_IDLE    = 0,
    STREAM_STATUS_ACTIVE  = 1 << 1,
    STREAM_STATUS_ERROR   = 1 << 2,
    STREAM_STATUS_FINISH  = 1 << 3,
    STREAM_STATUS_DESTROY = 1 << 4
  };

enum
  {
    URI_TYPE_UNKNOWN = 0,
    URI_TYPE_FILE,
    URI_TYPE_HTTP,
    URI_TYPE_FTP
  };

struct _StreamInstance
{
  NPStream  *np_stream;
  Plugin    *plugin;
  gchar     *mime_type;
  gpointer   notify_data;
  guint16    stype;
  guint      uri_type;
  gboolean   seekable;
  guint      status;

  /* XXX: machinery to download stream then propagate it to the plugin */
  CURL      *curl_handle;
  FILE      *temp_file;
  gchar     *temp_filename;
  gint       commit_source;
  GList     *buffers;
  guint      offset;
};

struct _StreamBuffer
{
  guchar    *bytes;
  guint      size;
  guint      offset;
};

static GList *
plugin_get_data_types (Plugin *plugin);

static gboolean
on_stream_open_cb (gpointer user_data);

static void
on_stream_close_cb (gpointer user_data);

static gboolean
on_stream_commit_cb (gpointer user_data);

static StreamInstance *
stream_new (Plugin *plugin,
	    const gchar *src, const gchar *type, void *notify_data,
	    NPError *error);

static void
stream_destroy (StreamInstance *pstream);

static StreamBuffer *
stream_buffer_new (guchar *bytes, gsize nbytes);

static void
stream_buffer_destroy (StreamBuffer *buffer);


/* ====================================================================== */
/* === Utility functions                                              === */
/* ====================================================================== */

static int
uri_type_from_url (const gchar *url)
{
  int uri_type = URI_TYPE_UNKNOWN;
  if (g_ascii_strncasecmp (url, "file://", 7) == 0)
    uri_type   = URI_TYPE_FILE;
  else if (g_ascii_strncasecmp (url, "http://", 7) == 0)
    uri_type   = URI_TYPE_HTTP;
  else if (g_ascii_strncasecmp (url, "ftp://", 6) == 0)
    uri_type   = URI_TYPE_FTP;
  return uri_type;
}

/* Sanitize URL, e.g. translate local file names to file:// syntax */
static gchar *
sanitize_url (const gchar *uri)
{
  /* Local contents has preference if no URI scheme is specified.
   *
   * This means that if you set src to 'www.example.com/thing.swf' and
   * you have a www.example.com directory in the current working
   * directory, then the file:// protocol will be prepended. Should
   * you need http://, then this has to be specified explicitly.
   * However, if there is no www.example.com directory, then libcurl
   * will try to resolve this to http:// or whatever.
   */
  if (uri_type_from_url (uri) != URI_TYPE_UNKNOWN)
    return g_strdup (uri);

  if (!g_file_test (uri, G_FILE_TEST_EXISTS))
    return g_strdup (uri);

  GString *new_uri = g_string_new ("file://");
  if (uri[0] != G_DIR_SEPARATOR)
    {
      gchar *cwd = g_get_current_dir ();
      g_string_append (new_uri, cwd);
      g_string_append (new_uri, G_DIR_SEPARATOR_S);
      g_free (cwd);
    }
  g_string_append (new_uri, uri);
  return g_string_free (new_uri, FALSE);
}

static gchar *
get_mime_type_from_content (guchar *bytes, guint size)
{
  gchar *mime_type = NULL;

  /* XXX: poor man's MIME type characterisation */
  static const guchar gif_sig[] = {0x47, 0x49, 0x46, 0x38};
  static const guchar png_sig[] = {0x89, 0x50, 0x4e, 0x47};
  static const guchar jpg_sig[] = {0xff, 0xd8, 0xff};
  static const guchar bmp_sig[] = {0x42, 0x4d};
  static const guchar flv_sig[] = {0x46, 0x4c, 0x56};
  static const guchar xml_sig[] = {0x3c, 0x3f, 0x78, 0x6d, 0x6c};

#define MATCH(TYPE)						\
  (size >= sizeof (TYPE##_sig) &&				\
   memcmp (bytes, TYPE##_sig, sizeof (TYPE##_sig)) == 0)

  if (MATCH(png))
    mime_type = "image/png";
  else if (MATCH(jpg))
    mime_type = "image/jpeg";
  else if (MATCH(gif))
    mime_type = "image/gif";
  else if (MATCH(bmp))
    mime_type = "image/bmp";
  else if (MATCH(flv))
    mime_type = "video/x-flv";
  else if (MATCH(xml))
    mime_type = "text/xml";
  else if (size >= 6 && g_ascii_strncasecmp ((gchar *)bytes, "<html>", 6) == 0)
    mime_type = "text/html";

#undef MATCH

  if (mime_type)
    return g_strdup (mime_type);

  D(bug("Could not determine MIME type for {%02x,%02x,%02x,%02x,%02x,%02x}\n",
	bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5]));
  return NULL;
}

static void
player_app_main_quit (void)
{
  if (g_backend == BACKEND_GTK)
    gtk_main_quit ();
}

#define UNIMPLEMENTED() NP_Unimplemented (__func__, __FILE__, __LINE__)

static void
NP_Unimplemented (const gchar *funcname, const gchar *filename, gint lineno)
{
  npw_printf ("WARNING: unimplemented function %s() at %s:%d\n",
	      funcname, filename, lineno);
}


/* ====================================================================== */
/* === NPRuntime API                                                  === */
/* ====================================================================== */

static NPObject *
g_NPN_CreateObject (NPP instance, NPClass *klass)
{
  if (instance == NULL)
    return NULL;

  if (klass == NULL)
    return NULL;

  NPObject *npobj;
  if (klass->allocate)
    npobj = klass->allocate (instance, klass);
  else
    npobj = g_new (NPObject, 1);

  if (npobj)
    {
      npobj->_class = klass;
      npobj->referenceCount = 1;
    }

  return npobj;
}

static NPObject *
g_NPN_RetainObject (NPObject *npobj)
{
  if (npobj)
    g_atomic_int_add ((volatile gint *)&npobj->referenceCount, 1);

  return npobj;
}

static void
g_NPN_ReleaseObject (NPObject *npobj)
{
  if (npobj == NULL)
    return;

  g_atomic_int_add ((volatile gint *)&npobj->referenceCount, -1);
  if (g_atomic_int_get ((volatile gint *)&npobj->referenceCount) == 0)
    {
      if (npobj->_class && npobj->_class->deallocate)
	npobj->_class->deallocate (npobj);
      else
	g_free (npobj);
    }
}

static bool
g_NPN_Invoke (NPP instance, NPObject *npobj, NPIdentifier methodName,
	      const NPVariant *args, uint32_t argCount, NPVariant *result)
{
  if (!instance || !npobj || !npobj->_class || !npobj->_class->invoke)
    return false;

  return npobj->_class->invoke (npobj, methodName, args, argCount, result);
}

static bool
g_NPN_InvokeDefault (NPP instance, NPObject *npobj,
		     const NPVariant *args, uint32_t argCount, NPVariant *result)
{
  if (!instance || !npobj || !npobj->_class || !npobj->_class->invokeDefault)
    return false;

  return npobj->_class->invokeDefault (npobj, args, argCount, result);
}

static bool
g_NPN_Evaluate (NPP instance, NPObject *npobj, NPString *script, NPVariant *result)
{
  if (!instance || !npobj)
    return false;

  if (result)
    VOID_TO_NPVARIANT (*result);

  if (!script || !script->UTF8Length || !script->UTF8Characters)
    return true; // nothing to evaluate

  UNIMPLEMENTED();
  return false;
}

static bool
g_NPN_GetProperty (NPP instance, NPObject *npobj, NPIdentifier propertyName,
		   NPVariant *result)
{
  if (!instance || !npobj || !npobj->_class || !npobj->_class->getProperty)
    return false;

  return npobj->_class->getProperty (npobj, propertyName, result);
}

static bool
g_NPN_SetProperty (NPP instance, NPObject *npobj, NPIdentifier propertyName,
		   const NPVariant *value)
{
  if (!instance || !npobj || !npobj->_class || !npobj->_class->setProperty)
    return false;

  return npobj->_class->setProperty (npobj, propertyName, value);
}

static bool
g_NPN_RemoveProperty (NPP instance, NPObject *npobj, NPIdentifier propertyName)
{
  if (!instance || !npobj || !npobj->_class || !npobj->_class->removeProperty)
    return false;

  return npobj->_class->removeProperty (npobj, propertyName);
}

static bool
g_NPN_HasProperty (NPP instance, NPObject *npobj, NPIdentifier propertyName)
{
  if (!instance || !npobj || !npobj->_class || !npobj->_class->hasProperty)
    return false;

  return npobj->_class->hasProperty (npobj, propertyName);
}

static bool
g_NPN_HasMethod (NPP instance, NPObject *npobj, NPIdentifier methodName)
{
  if (!instance || !npobj || !npobj->_class || !npobj->_class->hasMethod)
    return false;

  return npobj->_class->hasProperty (npobj, methodName);
}

static void
g_NPN_SetException (NPObject *npobj, const NPUTF8 *message)
{
  UNIMPLEMENTED();
}

static void
g_NPN_ReleaseVariantValue (NPVariant *variant)
{
  /* XXX: suppress double free condition in npw-wrapper.c */
  switch (variant->type)
    {
    case NPVariantType_String:
      {
	NPString *s = &NPVARIANT_TO_STRING (*variant);
	if (s->UTF8Characters)
	  {
	    g_free ((gpointer)s->UTF8Characters);
	    s->UTF8Characters = NULL;
	  }
	break;
      }
    case NPVariantType_Object:
      {
	NPObject *npobj = NPVARIANT_TO_OBJECT (*variant);
	if (npobj)
	  g_NPN_ReleaseObject (npobj);
	break;
      }
    default:
      break;
    }

  VOID_TO_NPVARIANT (*variant);
}

/* NOTE: g_intern_string() returns linear ids */
#define NP_IDENTIFIER_OBJECT	0x0
#define NP_IDENTIFIER_INT	0x1
#define NP_IDENTIFIER_STRING	0x2

static inline gboolean
is_string_identifier (NPIdentifier id)
{
  return (GPOINTER_TO_UINT (id) & 3) == NP_IDENTIFIER_STRING;
}

static inline NPIdentifier
pack_string_identifier (const gchar *str)
{
  guint32 id = g_quark_from_string (str);
  return GUINT_TO_POINTER ((id << 2) | NP_IDENTIFIER_STRING);
}

static inline const gchar *
unpack_string_identifier (NPIdentifier id)
{
  g_return_val_if_fail (is_string_identifier (id), NULL);
  return g_quark_to_string (GPOINTER_TO_UINT (id) >> 2);
}

static inline gboolean
is_int_identifier (NPIdentifier id)
{
  /* NOTE: the integer value must be 31-bit */
  return (GPOINTER_TO_UINT (id) & 1) == NP_IDENTIFIER_INT;
}

static inline NPIdentifier
pack_int_identifier (int32_t value)
{
  
  return GUINT_TO_POINTER ((((uint32_t)value) << 1) | NP_IDENTIFIER_INT);
}

static inline int32_t
unpack_int_identifier (NPIdentifier id)
{
  g_return_val_if_fail (is_int_identifier (id), 0);
  return ((int32_t)(GPOINTER_TO_INT (id))) >> 1;
}

static NPIdentifier
g_NPN_GetStringIdentifier (const NPUTF8 *name)
{
  if (name == NULL)
    return NULL;

  return pack_string_identifier ((gchar *)name);
}

static void
g_NPN_GetStringIdentifiers (const NPUTF8 **names, int32_t nameCount, NPIdentifier *identifiers)
{
  if (names == NULL)
    return;

  if (identifiers == NULL)
    return;

  for (int i = 0; i < nameCount; i++)
    identifiers[i] = pack_string_identifier ((gchar *)names[i]);
}

static NPIdentifier
g_NPN_GetIntIdentifier (int32_t intid)
{
  return pack_int_identifier (intid);
}

static bool
g_NPN_IdentifierIsString (NPIdentifier identifier)
{
  return is_string_identifier (identifier);
}

static NPUTF8 *
g_NPN_UTF8FromIdentifier(NPIdentifier identifier)
{
  return (NPUTF8 *)unpack_string_identifier (identifier);
}

static int32_t
g_NPN_IntFromIdentifier (NPIdentifier identifier)
{
  return unpack_int_identifier (identifier);
}

/* ====================================================================== */
/* === Browser side plug-in API                                       === */
/* ====================================================================== */

static const char *
g_NPN_UserAgent(NPP instance)
{
  D(bug("NPN_UserAgent instance %p\n", instance));

  return "Mozilla/5.0 (X11; U; Linux i386; en-US; rv:1.8.1) Gecko/20061010 Firefox/2.0";
}

static void
g_NPN_Status (NPP instance, const char *message)
{
  D(bug("NPN_Status instance %p\n", instance));

  UNIMPLEMENTED();
}

static NPError
g_NPN_GetValue (NPP instance, NPNVariable variable, void *value)
{
  D(bug("NPN_GetValue instance %p, variable %d [%08x]\n",
	instance, variable & 0xffff, variable));

  switch (variable) {
#if 0
  case NPNVxDisplay:
    break;
  case NPNVxtAppContext:
    break;
#endif
  case NPNVToolkit:
    *(NPNToolkitType *)value = NPNVGtk2;
    break;
  case NPNVSupportsXEmbedBool:
    *(NPBool *)value = USE_XEMBED;
    break;
  default:
    D(bug("WARNING: unhandled variable %d in NPN_GetValue()\n", variable));
    return NPERR_INVALID_PARAM;
  }

  return NPERR_NO_ERROR;
}

static NPError
g_NPN_SetValue (NPP instance, NPPVariable variable, void *value)
{
  D(bug("NPN_SetValue instance %p\n", instance));

  UNIMPLEMENTED();

  return NPERR_GENERIC_ERROR;
}

static NPError
g_NPN_GetURL (NPP instance, const char *url, const char *target)
{
  if (instance == NULL)
    return NPERR_INVALID_INSTANCE_ERROR;

  D(bug("NPN_GetURL instance %p\n", instance));

  UNIMPLEMENTED();

  return NPERR_GENERIC_ERROR;
}

static NPError
g_NPN_GetURLNotify (NPP instance, const char *url, const char *target, void *notifyData)
{
  if (instance == NULL)
    return NPERR_INVALID_INSTANCE_ERROR;

  D(bug("NPN_GetURLNotify instance %p\n", instance));

  if (target == NULL)
    {
      NPError ret = NPERR_GENERIC_ERROR;
      stream_new (instance->ndata, url, NULL, notifyData, &ret);
      return ret;
    }

  UNIMPLEMENTED();

  return NPERR_GENERIC_ERROR;
}

static NPError
g_NPN_PostURL (NPP instance, const char *url, const char *target, uint32_t len, const char *buf, NPBool file)
{
  if (instance == NULL)
    return NPERR_INVALID_INSTANCE_ERROR;

  D(bug("NPN_PostURL instance %p\n", instance));

  UNIMPLEMENTED ();

  return NPERR_GENERIC_ERROR;
}

static NPError
g_NPN_PostURLNotify (NPP instance, const char *url, const char *target, uint32_t len, const char *buf, NPBool file, void *notifyData)
{
  if (instance == NULL)
    return NPERR_INVALID_INSTANCE_ERROR;

  D(bug("NPN_PostURLNotify instance %p\n", instance));

  UNIMPLEMENTED();

  return NPERR_GENERIC_ERROR;
}

static NPError
g_NPN_NewStream (NPP instance, NPMIMEType type, const char *target, NPStream **stream)
{
  if (instance == NULL)
    return NPERR_INVALID_INSTANCE_ERROR;

  if (stream == NULL)
    return NPERR_INVALID_PARAM;
  *stream = NULL;

  D(bug("NPN_NewStream instance %p\n", instance));

  UNIMPLEMENTED();

  return NPERR_GENERIC_ERROR;
}

static NPError
g_NPN_DestroyStream (NPP instance, NPStream *stream, NPError reason)
{
  if (instance == NULL)
    return NPERR_INVALID_INSTANCE_ERROR;

  if (stream == NULL)
    return NPERR_INVALID_PARAM;

  D(bug("NPN_DestroyStream instance %p, stream %p, reason %s\n",
	instance, stream, string_of_NPReason(reason)));

  UNIMPLEMENTED();

  return NPERR_GENERIC_ERROR;
}

static NPError
g_NPN_RequestRead (NPStream *stream, NPByteRange *rangeList)
{
  if (stream == NULL || stream->ndata == NULL || rangeList == NULL)
	return NPERR_INVALID_PARAM;

  D(bug("NPN_RequestRead stream=%p\n", stream));

  UNIMPLEMENTED();

  return NPERR_GENERIC_ERROR;
}

static int32_t
g_NPN_Write (NPP instance, NPStream *stream, int32_t len, void *buf)
{
  if (instance == NULL)
    return -1;

  if (stream == NULL)
    return -1;

  D(bug("NPN_Write instance %p, stream %p, len %d, buf %p\n", instance, stream, len, buf));

  UNIMPLEMENTED();

  return -1;
}

static void *
g_NPN_MemAlloc (uint32_t size)
{
  D(bug("NPN_MemAlloc size %u\n", size));

  return g_malloc (size);
}

static uint32_t
g_NPN_MemFlush (uint32_t size)
{
  D(bug("NPN_MemFlush size %u\n", size));

  return 0;
}

static void
g_NPN_MemFree (void *ptr)
{
  D(bug("NPN_MemFree ptr %p\n", ptr));

  g_free (ptr);
}

static void
g_NPN_InvalidateRect (NPP instance, NPRect *invalidRect)
{
  D(bug("NPN_InvalidateRect instance %p\n", instance));

  UNIMPLEMENTED();
}

static void
g_NPN_InvalidateRegion (NPP instance, NPRegion invalidRegion)
{
  D(bug("NPN_InvalidateRegion instance %p\n", instance));

  UNIMPLEMENTED();
}

static void
g_NPN_ReloadPlugins(NPBool reloadPages)
{
  D(bug("NPN_ReloadPlugins reloadPages %d\n", reloadPages));

  UNIMPLEMENTED();
}

static void
g_NPN_ForceRedraw(NPP instance)
{
  D(bug("NPN_ForceRedraw instance %p\n", instance));

  UNIMPLEMENTED();
}

static void *
g_NPN_GetJavaEnv(void)
{
  D(bug("NPN_GetJavaEnv\n"));

  return NULL;
}

static void *
g_NPN_GetJavaPeer (NPP instance)
{
  D(bug("NPN_GetJavaPeer instance %p\n", instance));

  return NULL;
}

static void
g_NPN_PushPopupsEnabledState (NPP instance, NPBool enabled)
{
  if (instance == NULL)
    return;

  D(bug("NPN_PushPopupsEnabledState instance %p, enabled %d\n", instance, enabled));

  UNIMPLEMENTED();
}

static void
g_NPN_PopPopupsEnabledState (NPP instance)
{
  if (instance == NULL)
    return;

  D(bug("NPN_PopPopupsEnabledState instance %p\n", instance));

  UNIMPLEMENTED();
}

static NPError
g_NP_Initialize (Plugin *plugin)
{
  g_return_val_if_fail (plugin != NULL, NPERR_INVALID_FUNCTABLE_ERROR);

  memset (&plugin->plugin_funcs, 0, sizeof (plugin->plugin_funcs));
  plugin->plugin_funcs.size = sizeof (plugin->plugin_funcs);
  plugin->plugin_funcs.version = 19;

  memset (&plugin->mozilla_funcs, 0, sizeof (plugin->mozilla_funcs));
  plugin->mozilla_funcs.size = sizeof (plugin->mozilla_funcs);
  /* TODO: update this to wrap a more recent NPAPI version  */
  plugin->mozilla_funcs.version = 19; /* (NP_VERSION_MAJOR << 8) | NP_VERSION_MINOR; */
  plugin->mozilla_funcs.geturl = g_NPN_GetURL;
  plugin->mozilla_funcs.posturl = g_NPN_PostURL;
  plugin->mozilla_funcs.requestread = g_NPN_RequestRead;
  plugin->mozilla_funcs.newstream = g_NPN_NewStream;
  plugin->mozilla_funcs.write = g_NPN_Write;
  plugin->mozilla_funcs.destroystream = g_NPN_DestroyStream;
  plugin->mozilla_funcs.status = g_NPN_Status;
  plugin->mozilla_funcs.uagent = g_NPN_UserAgent;
  plugin->mozilla_funcs.memalloc = g_NPN_MemAlloc;
  plugin->mozilla_funcs.memfree = g_NPN_MemFree;
  plugin->mozilla_funcs.memflush = g_NPN_MemFlush;
  plugin->mozilla_funcs.reloadplugins = g_NPN_ReloadPlugins;
  plugin->mozilla_funcs.getJavaEnv = g_NPN_GetJavaEnv;
  plugin->mozilla_funcs.getJavaPeer = g_NPN_GetJavaPeer;
  plugin->mozilla_funcs.geturlnotify = g_NPN_GetURLNotify;
  plugin->mozilla_funcs.posturlnotify = g_NPN_PostURLNotify;
  plugin->mozilla_funcs.getvalue = g_NPN_GetValue;
  plugin->mozilla_funcs.setvalue = g_NPN_SetValue;
  plugin->mozilla_funcs.invalidaterect = g_NPN_InvalidateRect;
  plugin->mozilla_funcs.invalidateregion = g_NPN_InvalidateRegion;
  plugin->mozilla_funcs.forceredraw = g_NPN_ForceRedraw;
  plugin->mozilla_funcs.pushpopupsenabledstate = g_NPN_PushPopupsEnabledState;
  plugin->mozilla_funcs.poppopupsenabledstate = g_NPN_PopPopupsEnabledState;

  if ((plugin->mozilla_funcs.version & 0xff) >= NPVERS_HAS_NPRUNTIME_SCRIPTING)
    {
      D(bug(" browser supports scripting through npruntime\n"));
      plugin->mozilla_funcs.getstringidentifier = g_NPN_GetStringIdentifier;
      plugin->mozilla_funcs.getstringidentifiers = g_NPN_GetStringIdentifiers;
      plugin->mozilla_funcs.getintidentifier = g_NPN_GetIntIdentifier;
      plugin->mozilla_funcs.identifierisstring = g_NPN_IdentifierIsString;
      plugin->mozilla_funcs.utf8fromidentifier = g_NPN_UTF8FromIdentifier;
      plugin->mozilla_funcs.intfromidentifier = g_NPN_IntFromIdentifier;
      plugin->mozilla_funcs.createobject = g_NPN_CreateObject;
      plugin->mozilla_funcs.retainobject = g_NPN_RetainObject;
      plugin->mozilla_funcs.releaseobject = g_NPN_ReleaseObject;
      plugin->mozilla_funcs.invoke = g_NPN_Invoke;
      plugin->mozilla_funcs.invokeDefault = g_NPN_InvokeDefault;
      plugin->mozilla_funcs.evaluate = g_NPN_Evaluate;
      plugin->mozilla_funcs.getproperty = g_NPN_GetProperty;
      plugin->mozilla_funcs.setproperty = g_NPN_SetProperty;
      plugin->mozilla_funcs.removeproperty = g_NPN_RemoveProperty;
      plugin->mozilla_funcs.hasproperty = g_NPN_HasProperty;
      plugin->mozilla_funcs.hasmethod = g_NPN_HasMethod;
      plugin->mozilla_funcs.releasevariantvalue = g_NPN_ReleaseVariantValue;
      plugin->mozilla_funcs.setexception = g_NPN_SetException;
    }

  return plugin->NP_Initialize (&plugin->mozilla_funcs, &plugin->plugin_funcs);
}

static void
g_NP_Shutdown (Plugin *plugin)
{
  if (plugin && plugin->NP_Shutdown)
    plugin->NP_Shutdown ();
}

/* ====================================================================== */
/* === Plug-in side API                                               === */
/* ====================================================================== */

typedef struct _GetAttribsArg GetAttribsArg;

struct _GetAttribsArg
{
  GPtrArray *names;
  GPtrArray *values;
};

static void
get_attribs_cb (gpointer key, gpointer value, gpointer user_data)
{
  GetAttribsArg *arg = (GetAttribsArg *)user_data;
  g_ptr_array_add (arg->names,  g_strdup (key));
  g_ptr_array_add (arg->values, g_strdup (value));
}

static NPError
g_NPP_New (Plugin *plugin, guint16 mode, GHashTable *attrs, guint w, guint h)
{
  if (plugin == NULL)
    return NPERR_INVALID_INSTANCE_ERROR;

  if (attrs == NULL)
    return NPERR_INVALID_PARAM;

  if ((plugin->instance = g_new0 (NPP_t, 1)) == NULL)
    return NPERR_OUT_OF_MEMORY_ERROR;
  plugin->instance->ndata = plugin;

  GetAttribsArg arg;
  gint n_args = g_hash_table_size (attrs);
  arg.names  = g_ptr_array_sized_new (n_args);
  arg.values = g_ptr_array_sized_new (n_args);
  g_hash_table_foreach (attrs, get_attribs_cb, &arg);

  GString *str = g_string_new (NULL);
  g_string_printf (str, "%d", w);
  g_ptr_array_add (arg.names,  g_strdup ("width"));
  g_ptr_array_add (arg.values, g_strdup (str->str));
  ++n_args;
  g_string_printf (str, "%d", h);
  g_ptr_array_add (arg.names,  g_strdup ("height"));
  g_ptr_array_add (arg.values, g_strdup (str->str));
  ++n_args;
  g_string_free (str, TRUE);

  g_return_val_if_fail (arg.names->len  == n_args, NPERR_GENERIC_ERROR);
  g_return_val_if_fail (arg.values->len == n_args, NPERR_GENERIC_ERROR);

  NPError ret = plugin->plugin_funcs.newp(
				 plugin->mime_type,
				 plugin->instance,
				 mode,
				 n_args,
				 (gchar **)arg.names->pdata,
				 (gchar **)arg.values->pdata,
				 NULL);

  g_ptr_array_foreach (arg.names, (GFunc)g_free, NULL);
  g_ptr_array_free (arg.names, TRUE);
  g_ptr_array_foreach (arg.values, (GFunc)g_free, NULL);
  g_ptr_array_free (arg.values, TRUE);

  // check if XEMBED is to be used
  long supports_XEmbed = FALSE;
  if (plugin->mozilla_funcs.getvalue)
    {
      NPError error = plugin->mozilla_funcs.getvalue (NULL, NPNVSupportsXEmbedBool, (void *)&supports_XEmbed);
      if (error == NPERR_NO_ERROR && plugin->plugin_funcs.getvalue)
	{
	  long needs_XEmbed = FALSE;
	  error = plugin->plugin_funcs.getvalue (plugin->instance, NPPVpluginNeedsXEmbed, (void *)&needs_XEmbed);
	  if (error == NPERR_NO_ERROR)
	    plugin->use_xembed = supports_XEmbed && needs_XEmbed;
	}
    }
  return ret;
}

static NPError
g_NPP_Destroy (Plugin *plugin)
{
  if (plugin == NULL)
    return NPERR_INVALID_INSTANCE_ERROR;

  if (plugin->plugin_funcs.destroy == NULL)
    return NPERR_INVALID_FUNCTABLE_ERROR;

  NPSavedData save_data;
  NPSavedData *psave_data = &save_data;
  NPError ret = plugin->plugin_funcs.destroy(plugin->instance, &psave_data);

  if (plugin->instance)
    {
      g_free (plugin->instance);
      plugin->instance = NULL;
    }
  return ret;
}

static NPError
g_NPP_SetWindow (Plugin *plugin, GtkWidget *parent, gpointer display)
{
  if (plugin == NULL || plugin->instance == NULL)
    return NPERR_INVALID_INSTANCE_ERROR;

  guint width  = DISPLAY_WIDTH  (display);
  guint height = DISPLAY_HEIGHT (display);

  if (plugin->window == NULL)
    {
      /* Create the window for the first time */
      Window xid = 0;
      NPSetWindowCallbackStruct *ws_info = NULL;

      if (TRUE)
	{
	  if (plugin->use_xembed)
	    {
	      if ((plugin->window = gtk_socket_new ()) != NULL)
		gtk_container_add (GTK_CONTAINER (parent), plugin->window);
	    }
	  else
	    plugin->window = gtk_xtbin_new (parent->window, NULL);

	  if (plugin->window == NULL)
	    return NPERR_GENERIC_ERROR;
	  gtk_widget_set_size_request (plugin->window, width, height);
	  gtk_widget_show (plugin->window);

	  if ((ws_info = g_new0 (NPSetWindowCallbackStruct, 1)) == NULL)
	    return NPERR_OUT_OF_MEMORY_ERROR;
	  ws_info->type = 0; // should be NP_SETWINDOW but Mozilla sets it to 0

	  if (plugin->use_xembed)
	    {
	      GdkWindow * const win = plugin->window->window;
	      ws_info->display  = GDK_WINDOW_XDISPLAY (win);
	      ws_info->visual   = GDK_VISUAL_XVISUAL (gdk_drawable_get_visual(win));
	      ws_info->colormap = GDK_COLORMAP_XCOLORMAP (gdk_drawable_get_colormap (win));
	      ws_info->depth    = gdk_drawable_get_visual (win)->depth;
	      xid               = GDK_WINDOW_XWINDOW (win);
	    }
	  else
	    {
	      ws_info->display  = GTK_XTBIN (plugin->window)->xtdisplay;
	      ws_info->visual   = GTK_XTBIN (plugin->window)->xtclient.xtvisual;
	      ws_info->colormap = GTK_XTBIN (plugin->window)->xtclient.xtcolormap;
	      ws_info->depth    = GTK_XTBIN (plugin->window)->xtclient.xtdepth;
	      xid               = GTK_XTBIN (plugin->window)->xtwindow;
	    }
	}

      memset (&plugin->np_window, 0, sizeof (plugin->np_window));
      plugin->np_window.type    = NPWindowTypeWindow;
      plugin->np_window.window  = GUINT_TO_POINTER (xid);
      plugin->np_window.x       = 0;
      plugin->np_window.y       = 0;
      plugin->np_window.width   = width;
      plugin->np_window.height  = height;
      plugin->np_window.ws_info = ws_info;
    }
  else
    {
      /* Update window size */
      plugin->np_window.width   = width;
      plugin->np_window.height  = height;
      gtk_widget_set_size_request (plugin->window, width, height);
      // XXX: flash9 has some problems when resized too rapidly

      if (GTK_IS_XTBIN (plugin->window))
	{
	  // XXX: yes, two of these... for acrobat5!
	  // or an extra gtk_widget_size_allocate()
	  gtk_xtbin_resize (plugin->window, width, height);
	  gtk_xtbin_resize (plugin->window, width, height);
	}
    }

  return plugin->plugin_funcs.setwindow(plugin->instance, &plugin->np_window);
}

static NPError
g_NPP_NewStream (StreamInstance *pstream)
{
  if (pstream == NULL)
    return NPERR_INVALID_PARAM;

  Plugin *plugin = pstream->plugin;
  if (plugin == NULL || plugin->instance == NULL)
    return NPERR_INVALID_INSTANCE_ERROR;

  return plugin->plugin_funcs.newstream(
				plugin->instance,
				pstream->mime_type,
				pstream->np_stream,
				pstream->seekable,
				&pstream->stype);
}

static NPError
g_NPP_DestroyStream (StreamInstance *pstream)
{
  if (pstream == NULL)
    return NPERR_INVALID_PARAM;

  Plugin *plugin = pstream->plugin;
  if (plugin == NULL || plugin->instance == NULL)
    return NPERR_INVALID_INSTANCE_ERROR;

  NPReason reason = NPRES_DONE;
  if (pstream->status & STREAM_STATUS_ERROR)
    reason = NPRES_NETWORK_ERR;

  return plugin->plugin_funcs.destroystream(
				    plugin->instance,
				    pstream->np_stream,
				    reason);
}

static int32_t
g_NPP_WriteReady (StreamInstance *pstream)
{
  if (pstream == NULL)
    return -1;

  Plugin *plugin = pstream->plugin;
  if (plugin == NULL || plugin->instance == NULL)
    return -1;

  return plugin->plugin_funcs.writeready(
				 plugin->instance,
				 pstream->np_stream);
}

static int32_t
g_NPP_Write (StreamInstance *pstream, StreamBuffer *buffer, guint32 len)
{
  if (pstream == NULL)
    return -1;

  if (buffer == NULL)
    return -1;

  Plugin *plugin = pstream->plugin;
  if (plugin == NULL || plugin->instance == NULL)
    return -1;

  return plugin->plugin_funcs.write(
			    plugin->instance,
			    pstream->np_stream,
			    pstream->offset,
			    len,
			    buffer->bytes + buffer->offset);
}

static void
g_NPP_StreamAsFile (StreamInstance *pstream, const gchar *filename)
{
  if (pstream == NULL)
    return;

  Plugin *plugin = pstream->plugin;
  if (plugin == NULL || plugin->instance == NULL)
    return;

  if (g_ascii_strncasecmp (filename, "file://", 7) == 0)
    filename += 7;

  plugin->plugin_funcs.asfile(
			    plugin->instance,
			    pstream->np_stream,
			    filename);
}

static void
g_NPP_URLNotify (StreamInstance *pstream)
{
  if (pstream == NULL)
    return;

  Plugin *plugin = pstream->plugin;
  if (plugin == NULL || plugin->instance == NULL)
    return;

  NPReason reason = NPRES_DONE;
  if (pstream->status & STREAM_STATUS_ERROR)
    reason = NPRES_NETWORK_ERR;

  plugin->plugin_funcs.urlnotify(
			 plugin->instance,
			 pstream->np_stream->url,
			 reason,
			 pstream->np_stream->notifyData);
}

/* ====================================================================== */
/* === Stream support functions                                       === */
/* ====================================================================== */

static size_t
on_stream_read_nothing_cb (void *ptr, size_t size, size_t nmemb, void *user_data)
{
  // don't download anything
  return 0;
}

static NPStream *
np_stream_new (const gchar *url, void *notify_data)
{
  /* retrieve (remote) file information */
  /* XXX: should be incrementally updated in wrapper+viewer through NPP_*() */
  CURL *handle = curl_easy_init ();
  if (handle == NULL)
    return NULL;

  curl_easy_setopt (handle, CURLOPT_URL, url);
  curl_easy_setopt (handle, CURLOPT_WRITEFUNCTION, on_stream_read_nothing_cb);
  curl_easy_setopt (handle, CURLOPT_FILETIME, 1);
  curl_easy_setopt (handle, CURLOPT_TIMECONDITION, CURL_TIMECOND_LASTMOD);
  if (uri_type_from_url (url) == URI_TYPE_HTTP)
    curl_easy_setopt (handle, CURLOPT_FOLLOWLOCATION, 1);

  curl_easy_perform (handle);

  double size;
  if (curl_easy_getinfo (handle, CURLINFO_CONTENT_LENGTH_DOWNLOAD, &size) != CURLE_OK)
    size = 0;

  long lastmod;
  if (curl_easy_getinfo (handle, CURLINFO_FILETIME, &lastmod) != CURLE_OK ||
      lastmod == -1)
    lastmod = 0;

  curl_easy_cleanup (handle);

  NPStream *np_stream = g_new0 (NPStream, 1);
  if (np_stream == NULL)
    return NULL;
  np_stream->url          = g_strdup (url);
  np_stream->end          = size;
  np_stream->lastmodified = lastmod;
  np_stream->notifyData   = notify_data;
  return np_stream;
}

static void
np_stream_destroy (NPStream *np_stream)
{
  if (np_stream == NULL)
    return;

  if (np_stream->url)
    {
      g_free ((gpointer)np_stream->url);
      np_stream->url = NULL;
    }

  g_free (np_stream);
}

static StreamInstance *
stream_new (Plugin *plugin,
	    const gchar *src, const gchar *type, void *notify_data,
	    NPError *error)
{
  StreamInstance *pstream   = NULL;
  NPStream       *np_stream = NULL;
  NPError         ret       = NPERR_NO_ERROR;

  if ((pstream = g_new0 (StreamInstance, 1)) == NULL)
    {
      ret = NPERR_OUT_OF_MEMORY_ERROR;
      goto l_error;
    }

  if ((np_stream = np_stream_new (src, notify_data)) == NULL)
    {
      ret = NPERR_OUT_OF_MEMORY_ERROR;
      goto l_error;
    }

  np_stream->ndata      = pstream;
  pstream->plugin       = plugin;
  pstream->np_stream    = np_stream;
  pstream->mime_type    = g_strdup (type);
  pstream->stype        = NP_NORMAL;
  pstream->status       = STREAM_STATUS_IDLE;
  pstream->notify_data  = notify_data;
  pstream->uri_type     = uri_type_from_url (src);
  pstream->seekable     = 0 && (pstream->uri_type == URI_TYPE_FILE);

  if (notify_data == NULL)
    {
      if ((ret = g_NPP_NewStream (pstream)) != NPERR_NO_ERROR)
	goto l_error;
      pstream->status |= STREAM_STATUS_ACTIVE;
    }
  g_idle_add (on_stream_open_cb, pstream);
  goto l_return;

 l_error:
  if (np_stream)
    np_stream_destroy (np_stream);
  if (pstream)
    g_free (pstream);
  pstream = NULL;

 l_return:
  if (error)
    *error = ret;
  return pstream;
}

static void
stream_destroy (StreamInstance *pstream)
{
  if (pstream == NULL)
    return;

  if (pstream->curl_handle)
    {
      glibcurl_remove (pstream->curl_handle);
      curl_easy_cleanup (pstream->curl_handle);
      pstream->curl_handle = NULL;
    }

  if (pstream->buffers)
    {
      g_list_foreach (pstream->buffers, (GFunc)stream_buffer_destroy, NULL);
      g_list_free (pstream->buffers);
      pstream->buffers = NULL;
    }

  if (pstream->temp_file)
    {
      /* Close the file prior to calling NPP_StreamAsFile() [Acrobat5] */
      fclose (pstream->temp_file);
      pstream->temp_file = NULL;
    }

  if (pstream->status & STREAM_STATUS_DESTROY)
    {
      if (pstream->stype == NP_ASFILE || pstream->stype == NP_ASFILEONLY)
	{
	  /* temporary files are used only for remote files */
	  const gchar *fname = pstream->temp_filename;
	  if (fname == NULL)
	    fname = pstream->np_stream->url;
	  g_NPP_StreamAsFile (pstream, fname);
	}

      g_NPP_DestroyStream (pstream);

      if (pstream->notify_data)
	g_NPP_URLNotify (pstream);
    }

  if (pstream->temp_filename)
    {
      unlink (pstream->temp_filename);
      g_free (pstream->temp_filename);
      pstream->temp_filename = NULL;
    }

  if (pstream->mime_type)
    {
      g_free (pstream->mime_type);
      pstream->mime_type = NULL;
    }

  if (pstream->np_stream)
    {
      np_stream_destroy (pstream->np_stream);
      pstream->np_stream = NULL;
    }

  g_free (pstream);
}

static void
stream_commit (StreamInstance *pstream)
{
  if (pstream == NULL)
    return;

  if (pstream->commit_source == 0)
    pstream->commit_source = g_idle_add (on_stream_commit_cb, pstream);
}

static void
stream_schedule_destroy (StreamInstance *pstream, gboolean now)
{
  if (pstream == NULL)
    return;

  if (now)
    pstream->status |= STREAM_STATUS_DESTROY;
  else
    pstream->status |= STREAM_STATUS_FINISH;
  stream_commit (pstream);
}

static inline gboolean
stream_use_npp_write (StreamInstance *pstream)
{
  /* If the stream comes from a local file in NP_ASFILEONLY mode,
   * the NPP_Write() and NPP_WriteReady() functions are not called.
   */
  if (pstream->stype == NP_ASFILEONLY && pstream->uri_type == URI_TYPE_FILE)
    return FALSE;
  return TRUE;
}

static int32_t
stream_write_ready (StreamInstance *pstream)
{
  int32_t write_ready = 0x0fffff;
  if (stream_use_npp_write (pstream))
    write_ready = g_NPP_WriteReady (pstream);
  return write_ready;
}

static int32_t
stream_write (StreamInstance *pstream, StreamBuffer *buffer, guint32 len)
{
  switch (pstream->stype)
    {
    case NP_ASFILE:
    case NP_ASFILEONLY:
      if (pstream->temp_file)
	{
	  if (fwrite (buffer->bytes + buffer->offset, len, 1, pstream->temp_file) != 1)
	    return -1;
	}
      break;
    }
  if (!stream_use_npp_write (pstream))
    return len;
  return g_NPP_Write (pstream, buffer, len);
}

static StreamBuffer *
stream_buffer_new (guchar *bytes, gsize nbytes)
{
  StreamBuffer *buffer = g_new0 (StreamBuffer, 1);

  if (buffer)
    {
      buffer->bytes  = g_memdup (bytes, nbytes);
      buffer->size   = nbytes;
      buffer->offset = 0;
    }

  return buffer;
}

static void
stream_buffer_destroy (StreamBuffer *buffer)
{
  if (buffer == NULL)
    return;

  if (buffer->bytes)
    {
      g_free (buffer->bytes);
      buffer->bytes = NULL;
    }

  g_free (buffer);
}

static gboolean
on_stream_commit_cb (gpointer user_data)
{
  StreamInstance *pstream = (StreamInstance *)user_data;

  if (pstream->buffers)
    {
      StreamBuffer *buffer = pstream->buffers->data;

      if (!(pstream->status & STREAM_STATUS_ACTIVE))
	{
	  /* XXX: determine MIME type (should be done in NPN_GetURL*()...) */
	  if (pstream->mime_type == NULL)
	    pstream->mime_type = get_mime_type_from_content (buffer->bytes,
							     buffer->size);
	  if (pstream->mime_type == NULL)
	    pstream->mime_type = g_strdup (pstream->plugin->mime_type);

	  if (g_NPP_NewStream (pstream) != NPERR_NO_ERROR)
	    goto force_destroy;
	  pstream->status |= STREAM_STATUS_ACTIVE;
	}

      int32_t write_ready = stream_write_ready (pstream);

      if (write_ready < 0)
	{
	  /* The plug-in doesn't want the stream, kill all pending buffers */
	  pstream->status |= STREAM_STATUS_DESTROY;
	}
      else if (write_ready > 0)
	{
	  int32_t len = MIN (buffer->size - buffer->offset, write_ready);

	  len = stream_write (pstream, buffer, len);

	  if (len < 0)
	    pstream->status |= STREAM_STATUS_ERROR | STREAM_STATUS_DESTROY;
	  else if (len > 0)
	    {
	      if ((buffer->offset += len) >= buffer->size)
		{
		  stream_buffer_destroy (buffer);
		  pstream->offset += len;
		  pstream->buffers = g_list_delete_link (pstream->buffers,
							 pstream->buffers);
		}
	    }
	}
    }

  if (pstream->buffers == NULL && (pstream->status & STREAM_STATUS_FINISH))
    pstream->status |= STREAM_STATUS_DESTROY;

  if (pstream->buffers == NULL || (pstream->status & STREAM_STATUS_DESTROY))
    {
    force_destroy:
      g_source_remove (pstream->commit_source);
      pstream->commit_source = 0;
      if (pstream->status & STREAM_STATUS_DESTROY)
	stream_destroy (pstream);
      return FALSE;
    }

  return TRUE;
}

static size_t
on_stream_read_cb (void *ptr, size_t size, size_t nmemb, void *user_data)
{
  StreamInstance *pstream = (StreamInstance *)user_data;
  size_t real_size = size * nmemb;

  StreamBuffer *buffer = stream_buffer_new (ptr, real_size);
  if (buffer == NULL)
    return 0;
  pstream->buffers = g_list_append (pstream->buffers, buffer);
  stream_commit (pstream);
  return real_size;
}

static gboolean
on_stream_open_cb (gpointer user_data)
{
  StreamInstance *pstream = (StreamInstance *)user_data;

  switch (pstream->stype)
    {
    case NP_NORMAL:
      break;
    case NP_ASFILE:
    case NP_ASFILEONLY:
      /* Technically speaking, the Firefox specs say that the file is
       * downloaded to the cache even for local files. However, it
       * turns out the file name passed to NPP_StreamAsFile() is not
       * the path to the cached file but actually that of the original
       * file, if it is a local file.
       *
       * So, we only use temporary files for remote files. The
       * standalone player doesn't handle a persistent cache.
       *
       * This also means that if you remove the original file while
       * viewing it, then you can get unexpected behaviour since the
       * plugin won't be using the cached file.
       *
       * And, in order to be even clearer, the Acrobat Reader plugin expects
       * stream->url == fname passed to NPP_StreamAsFile().
       */
      if (pstream->uri_type != URI_TYPE_FILE)
	{
	  GError *error = NULL;
	  int fd = g_file_open_tmp (NULL, &pstream->temp_filename, &error);
	  if (fd < 0) {
	    npw_printf ("ERROR: Could not open temporary file: %s\n",
			error->message);
	    g_error_free (error);
	    return FALSE;
	  }
	  pstream->temp_file = fdopen (fd, "w");
	  if (pstream->temp_file == NULL) {
	    npw_printf ("ERROR: fdopen failed: %s\n", strerror (errno));
	    close (fd);
	    g_free (pstream->temp_filename);
	    pstream->temp_filename = NULL;
	    return FALSE;
	  }
	}
      break;
    default:
      UNIMPLEMENTED();
      return FALSE;
    }

  if ((pstream->curl_handle = curl_easy_init ()) == NULL)
    npw_printf ("WARNING: could not create CURL stream\n");

  CURL * const handle = pstream->curl_handle;
  curl_easy_setopt (handle, CURLOPT_URL, pstream->np_stream->url);
  curl_easy_setopt (handle, CURLOPT_WRITEFUNCTION, on_stream_read_cb);
  curl_easy_setopt (handle, CURLOPT_WRITEDATA, pstream);
  curl_easy_setopt (handle, CURLOPT_PRIVATE, pstream);
  if (pstream->uri_type == URI_TYPE_HTTP)
    curl_easy_setopt (handle, CURLOPT_FOLLOWLOCATION, 1);
  glibcurl_add (handle);
  return FALSE;
}

static void
on_stream_close_cb (gpointer user_data)
{
  CURLMsg *msg;
  int in_queue;

  while ((msg = curl_multi_info_read (glibcurl_handle (), &in_queue)) != NULL)
    {
      if (msg->msg != CURLMSG_DONE)
	continue;

      StreamInstance *pstream;
      char *pstream_;
      
      CURL *handle = msg->easy_handle;
      /* We actually want to get a void* out of here, but for "for
	 internal reasons", CURL actually returns a char* and gives a
	 compiler warning otherwise. */
      if (curl_easy_getinfo (handle, CURLINFO_PRIVATE, &pstream_) == CURLE_OK)
	{
	  pstream = (StreamInstance *) pstream_;
	  if (!(pstream->status & STREAM_STATUS_ACTIVE))
	    {
	      /* Special case for JavaScript that CURL could not handle */
	      const gchar * const url = pstream->np_stream->url;
	      if (g_ascii_strncasecmp (url, "javascript:", 11) == 0)
		{
		  if (pstream->mime_type)
		    g_free (pstream->mime_type);
		  pstream->mime_type = g_strdup ("text/html");

		  /* XXX: this is just a hack to get top.location */
		  GString *text = NULL;
		  const gchar *jscode = url + 11;
		  if (g_ascii_strncasecmp (jscode, "top.location", 12) == 0)
		    text = g_string_new (pstream->plugin->src);
		  if (text)
		    {
		      bool ok = TRUE;

		      /* XXX: only allow string concatenation */
		      jscode += 12;
		      while (*jscode != '\0')
			{
			  while (g_ascii_isspace (*jscode))
			    ++jscode;
			  if (*jscode == '\0')
			    break;
			  if (*jscode != '+')
			    { ok = FALSE; break; }
			  if (*++jscode == '\0')
			    { ok = FALSE; break; }
			  while (g_ascii_isspace (*jscode))
			    ++jscode;
			  if (*jscode != '"')
			    { ok = FALSE; break; }
			  if (*++jscode == '\0')
			    { ok = FALSE; break; }
			  const gchar *str = jscode;
			  while (*jscode != '"')
			    {
			      if (*jscode == '\0')
				{ ok = FALSE; break; }
			      ++jscode;
			    }
			  g_string_append_len (text, str, jscode - str);
			  ++jscode;
			}
		      if (ok)
			on_stream_read_cb (text->str, text->len, 1, pstream);
		      g_string_free (text, TRUE);
		    }
		}

	      /* XXX: consider the stream in error if it was never started */
	      if (pstream->buffers == NULL)
		pstream->status |= STREAM_STATUS_ERROR;
	    }

	  stream_schedule_destroy (pstream, FALSE);
	}
    }
}

/* ====================================================================== */
/* === Plugin database                                                === */
/* ====================================================================== */

static PluginDataType *
plugin_data_type_new (const gchar *mime, const gchar *ext, const gchar *desc)
{
  PluginDataType *data_type = g_new0 (PluginDataType, 1);
  if (data_type == NULL)
    return NULL;
  data_type->values[PLUGIN_DATA_TYPE_MIME]        = g_strdup (mime);
  data_type->values[PLUGIN_DATA_TYPE_EXTENSION]   = g_strdup (ext);
  data_type->values[PLUGIN_DATA_TYPE_DESCRIPTION] = g_strdup (desc);
  if (g_verbose)
    npw_printf ("%-8s  %-50s  %s\n", ext, mime, desc);
  return data_type;
}

static void
plugin_data_type_destroy (PluginDataType *data_type)
{
  if (data_type == NULL)
    return;

  for (gint i = 0; i < PLUGIN_DATA_TYPE_VALUE_COUNT; i++)
    {
      if (data_type->values[i])
	{
	  g_free (data_type->values[i]);
	  data_type->values[i] = NULL;
	}
    }

  g_free (data_type);
}

static GList *
plugin_get_data_types (Plugin *plugin)
{
  g_return_val_if_fail (plugin != NULL, NULL);

  if (plugin->data_types)
    return plugin->data_types;

  const gchar *mime_desc = plugin->NP_GetMIMEDescription ();
  if (mime_desc == NULL)
    return NULL;

  GList *data_types = NULL;
  gchar **mime_descs = g_strsplit (mime_desc, ";", -1);
  for (gint i = 0; mime_descs[i] != NULL; i++)
    {
      gchar **mime_parts = g_strsplit (mime_descs[i], ":", -1);
      if (mime_parts && mime_parts[0] && mime_parts[1] && mime_parts[2])
	{
	  gchar **extensions = g_strsplit (mime_parts[1], ",", -1);
	  for (gint j = 0; extensions[j] != NULL; j++)
	    {
	      PluginDataType *data_type = plugin_data_type_new (mime_parts[0],
								extensions[j],
								mime_parts[2]);
	      if (data_type)
		data_types = g_list_prepend (data_types, data_type);
	    }
	  g_strfreev (extensions);
	}
      g_strfreev (mime_parts);
    }
  g_strfreev (mime_descs);
  if (data_types == NULL)
    return NULL;
  plugin->data_types = g_list_reverse (data_types);
  return plugin->data_types;
}

static void plugin_destroy (Plugin *plugin);

static Plugin *
plugin_new (const gchar *path)
{
  Plugin *plugin = g_new0 (Plugin, 1);
  if (plugin == NULL)
    return NULL;
  plugin->path = g_strdup (path);

  if ((plugin->module = dlopen (path, RTLD_LAZY|RTLD_LOCAL)) == NULL)
    goto error;

  if (!(plugin->NP_Initialize = dlsym (plugin->module, "NP_Initialize")))
    goto error;
  if (!(plugin->NP_Shutdown = dlsym (plugin->module, "NP_Shutdown")))
    goto error;
  if (!(plugin->NP_GetMIMEDescription =
	dlsym (plugin->module, "NP_GetMIMEDescription")))
    goto error;
  if (!(plugin->NP_GetValue = dlsym (plugin->module, "NP_GetValue")))
    goto error;

  return plugin;

 error:
  if (plugin)
    {
      plugin_destroy (plugin);
      plugin = NULL;
    }
  return NULL;
}

static void
plugin_destroy (Plugin *plugin)
{
  if (plugin == NULL)
    return;

  if (plugin->module)
    {
      /* Intentionally avoid unloading the module. This causes crashes
       * as some libraries break when you unload them. In particular,
       * Qt has a bug where it will blow away pthread_key_t 0 on
       * unload. (In this process, this corresponds to glib's internal
       * thread data. Everything in glib breaks when this happens)
       * Sadly, Kopete installs this utterly pointless
       * "skypebuttons.so" plugin that pulls in Qt while doing almost
       * nothing else.
       *
       * http://bugreports.qt.nokia.com/browse/QTBUG-10861 */

      /* dlclose (plugin->module); */
      plugin->module = NULL;
    }

  if (plugin->path)
    {
      g_free (plugin->path);
      plugin->path = NULL;
    }

  if (plugin->src)
    {
      g_free (plugin->src);
      plugin->src = NULL;
    }

  if (plugin->mime_type)
    {
      g_free (plugin->mime_type);
      plugin->mime_type = NULL;
    }

  if (plugin->data_types)
    {
      g_list_foreach (plugin->data_types, (GFunc)plugin_data_type_destroy, NULL);
      g_list_free (plugin->data_types);
      plugin->data_types = NULL;
    }

  if (plugin->np_window.ws_info)
    {
      g_free (plugin->np_window.ws_info);
      plugin->np_window.ws_info = NULL;
    }

  g_free (plugin);
}

static void
plugin_destroy_if_not (Plugin *plugin, Plugin *plugin_to_keep)
{
  if (plugin != plugin_to_keep)
    plugin_destroy (plugin);
}

static void
plugin_start (Plugin *plugin, PlayerApp *app)
{
  NPError ret;

  D(bug("Execute plugin '%s'\n", plugin->path));

  ret = g_NP_Initialize (plugin);
  if (ret != NPERR_NO_ERROR)
    {
      npw_printf ("ERROR: could not execute: %s\n", string_of_NPError (ret));
      player_app_main_quit ();
      return;
    }

  plugin->src = g_hash_table_lookup (app->attrs, "src");

  /* XXX: determine plugin MIME type supported, move elsewhere? */
  if (plugin->mime_type == NULL)
    {
      const gchar *mime_type;
      if ((mime_type = g_hash_table_lookup (app->attrs, "type")) != NULL)
	plugin->mime_type = g_strdup (mime_type);
    }
  if (plugin->mime_type == NULL)
    {
      npw_printf ("ERROR: no MIME type specified for this plugin\n");
      player_app_main_quit ();
      return;
    }

  guint width  = DISPLAY_WIDTH  (app->display);
  guint height = DISPLAY_HEIGHT (app->display);

  ret = g_NPP_New (plugin, app->mode, app->attrs, width, height);
  if (ret != NPERR_NO_ERROR)
    {
      npw_printf ("ERROR: could not create NPP instance\n");
      player_app_main_quit ();
      return;
    }

  ret = g_NPP_SetWindow (plugin, app->window, app->display);
  if (ret != NPERR_NO_ERROR)
    {
      npw_printf ("ERROR: could not create NPP window\n");
      player_app_main_quit ();
      return;
    }

  if (plugin->src)
    {
      NPError error;
      StreamInstance *pstream;
      pstream = stream_new (plugin, plugin->src, plugin->mime_type, NULL, &error);
      if (pstream == NULL || error != NPERR_NO_ERROR)
	{
	  if (pstream)
	    stream_destroy (pstream);
	  npw_printf ("ERROR: could not create NPP stream from '%s'\n", plugin->src);
	  player_app_main_quit ();
	  return;
	}
    }
}

static void
plugin_stop (Plugin *plugin)
{
  if (plugin == NULL)
    return;

  if (plugin->plugin_funcs.setwindow)
    {
      if (plugin->np_window.ws_info)
	{
	  g_free (plugin->np_window.ws_info);
	  plugin->np_window.ws_info = NULL;
	}

      /* A NULL handle means the plugin must not do any graphics operation further */
      plugin->np_window.window = NULL;

      plugin->plugin_funcs.setwindow(
			     plugin->instance,
			     &plugin->np_window);
    }

  g_NPP_Destroy (plugin);
  g_NP_Shutdown (plugin);
}

static GList *
g_list_prepend_if_path_exists (GList *dirs, const gchar *path)
{
  if (g_file_test (path, G_FILE_TEST_IS_DIR))
    dirs = g_list_prepend (dirs, g_strdup (path));
  return dirs;
}

static GList *
get_plugin_dirs (void)
{
  GList *dirs = NULL;
  gchar *path;

  path = g_build_filename (g_get_home_dir (), ".mozilla", "plugins", NULL);
  dirs = g_list_prepend_if_path_exists (dirs, path);
  g_free (path);

  const gchar *moz_plugin_path = g_getenv ("MOZ_PLUGIN_PATH");
  if (moz_plugin_path)
    {
      gchar **moz_plugin_paths = g_strsplit (moz_plugin_path, ":", -1);
      for (gint i = 0; moz_plugin_paths[i] != NULL; i++)
	dirs = g_list_prepend_if_path_exists (dirs, moz_plugin_paths[i]);
      g_strfreev (moz_plugin_paths);
    }

  path = g_build_filename
    (G_DIR_SEPARATOR_S "usr", "lib", "mozilla", "plugins", NULL);
  dirs = g_list_prepend_if_path_exists (dirs, path);
  g_free (path);

  path = g_build_filename
    (G_DIR_SEPARATOR_S "usr", "lib", "browser", "plugins", NULL);
  dirs = g_list_prepend_if_path_exists (dirs, path);
  g_free (path);

  return dirs;
}

static gboolean
is_npapi_plugin (const gchar *path)
{
  void *module = dlopen (path, RTLD_LAZY|RTLD_LOCAL);
  if (module == NULL)
    {
      if (g_verbose)
	npw_printf ("WARNING: %s\n", dlerror ());
      return FALSE;
    }

  gboolean is_valid = TRUE;
  if (dlsym (module, "NP_Initialize") == NULL)
    is_valid = FALSE;
  if (dlsym (module, "NP_Shutdown") == NULL)
    is_valid = FALSE;
  if (dlsym (module, "NP_GetMIMEDescription") == NULL)
    is_valid = FALSE;

  /* Intentionally avoid unloading the module. This causes crashes as
   * some libraries break when you unload them. In particular, Qt has
   * a bug where it will blow away pthread_key_t 0 on unload. (In this
   * process, this corresponds to glib's internal thread
   * data. Everything in glib breaks when this happens) Sadly, Kopete
   * installs this utterly pointless "skypebuttons.so" plugin that
   * pulls in Qt while doing almost nothing else.
   *
   * http://bugreports.qt.nokia.com/browse/QTBUG-10861 */

  /* dlclose (module); */
  return is_valid;
}

static void
player_app_load_plugin (PlayerApp *app, const gchar *path)
{
  Plugin *plugin;

  if ((plugin = plugin_new (path)) == NULL)
    g_error ("could not initialize plugin '%s'", path);

  app->plugins = g_list_prepend (app->plugins, plugin);
}

static void
player_app_load_plugins (PlayerApp *app, const gchar *dirname)
{
  GDir        *dir;
  gchar       *old_dir = g_get_current_dir ();
  const gchar *name;
  gchar       *path;

  if ((dir = g_dir_open (dirname, 0, NULL)) == NULL)
    return;

  g_chdir (dirname);

  while ((name = g_dir_read_name (dir)) != NULL)
    {
      path = g_build_filename (dirname, name, NULL);
      if (is_npapi_plugin (path))
	player_app_load_plugin (app, path);
      g_free (path);
    }

  g_dir_close (dir);

  g_chdir (old_dir);
  g_free (old_dir);
}

typedef struct _FindPluginCustomArg FindPluginCustomArg;

struct _FindPluginCustomArg
{
  gint id;
  const gchar *value;
  gchar **pmime;
};

static gint
find_plugin_custom_cb (PluginDataType *data_type, FindPluginCustomArg *arg)
{
  int ret = strcmp (data_type->values[arg->id], arg->value);
  if (ret == 0 && arg->pmime)
    *arg->pmime = g_strdup (data_type->values[PLUGIN_DATA_TYPE_MIME]);
  return ret;
}

static Plugin *
find_plugin_custom (PlayerApp *app, gint id, const gchar *value, gchar **pmime)
{
  GList *l;
  for (l = app->plugins; l != NULL; l = l->next)
    {
      Plugin *plugin = (Plugin *)l->data;

      FindPluginCustomArg arg;
      arg.id    = id;
      arg.value = value;
      arg.pmime = pmime;
      if (g_list_find_custom (plugin_get_data_types (plugin),
			      &arg, (GCompareFunc)find_plugin_custom_cb))
	return plugin;
    }
  return NULL;
}

static inline Plugin *
find_plugin_by_mime (PlayerApp *app, const gchar *mime_type)
{
  return find_plugin_custom (app, PLUGIN_DATA_TYPE_MIME, mime_type, NULL);
}

static Plugin *
find_plugin_by_uri (PlayerApp *app, const gchar *uri, gchar **pmime)
{
  Plugin *plugin = NULL;

  gchar *extension = strrchr (uri, '.');
  if (extension)
    ++extension;

  plugin = find_plugin_custom (app, PLUGIN_DATA_TYPE_EXTENSION, extension, pmime);
  if (plugin)
    return plugin;

  /* XXX: load and lookup object MIME type */
  return NULL;
}

static void
player_app_destroy (PlayerApp *app)
{
  if (app == NULL)
    return;

  if (app->plugin)
    {
      plugin_destroy (app->plugin);
      app->plugin = NULL;
    }

  if (app->display)
    {
      g_free (app->display);
      app->display = NULL;
    }

  g_free (app);
}

static gboolean
player_app_run (PlayerApp *app)
{
  GList *plugin_dirs = get_plugin_dirs ();
  for (GList *l = plugin_dirs; l != NULL; l = l->next)
    {
      gchar *plugin_dir = (gchar *)l->data;
      player_app_load_plugins (app, plugin_dir);
      g_free (plugin_dir);
    }
  g_list_free (plugin_dirs);
  plugin_dirs = NULL;

  /* Lookup plugin by MIME type first, then try by object type (from URI) */
  gchar *value;
  if ((value = g_hash_table_lookup (app->attrs, "type")) != NULL)
    app->plugin = find_plugin_by_mime (app, value);
  else if ((value = g_hash_table_lookup (app->attrs, "src")) != NULL)
    {
      gchar *mime_type = NULL;
      if ((app->plugin = find_plugin_by_uri (app, value, &mime_type)) != NULL)
	{
	  if (!g_hash_table_lookup_extended (app->attrs, "type", NULL, NULL))
	    g_hash_table_insert (app->attrs, "type", mime_type);
	}
    }
  else
    app->plugin = NULL;

  if (app->plugin == NULL)
    g_error ("could not find any plugin to use");

  /* Free all other plugins */
  if (app->plugins)
    {
      g_list_foreach (app->plugins, (GFunc)plugin_destroy_if_not, app->plugin);
      g_list_free (app->plugins);
      app->plugins = NULL;
    }

  plugin_start (app->plugin, app);
  return FALSE;
}

static void
player_app_quit (PlayerApp *app)
{
  if (app == NULL)
    return;

  plugin_stop (app->plugin);
}


/* ====================================================================== */
/* === GUI glue                                                       === */
/* ====================================================================== */

static void
print_help (const gchar *program_name)
{
  g_print ("Usage: %s [option]* [--plugin] [name[=value]]*\n", program_name);
  g_print ("\n");

  g_print ("Options:\n");
  g_print ("  -v|--verbose            enable verbose mode\n");
  g_print ("  -f|--fullscreen         start in fullscreen mode\n");
  g_print ("  --xid N                 embed in window with xid N\n");
  g_print ("\n");

  g_print ("Common attributes include:\n");
  g_print ("  embed                   use NP_EMBED mode\n");
  g_print ("  full                    use NP_FULL mode (default)\n");
  g_print ("  src=URI                 location (URL) of the object to load\n");
  g_print ("  type=MIME-TYPE          MIME type of the object\n");
  g_print ("  width=WIDTH             width (in pixels)\n");
  g_print ("  height=HEIGHT           height (in pixels)\n");
  g_print ("\n");

  g_print ("Other attributes will be passed down to the plugin (e.g. flashvars)\n");
  g_print ("\n");
}

static gboolean
on_configure_event_cb (GtkWidget         *widget,
		       GdkEventConfigure *event,
		       PlayerApp         *app)
{
  if (g_backend != BACKEND_GTK)
    return FALSE;

  if (event->width == app->width && event->height == app->height)
    return FALSE;

  // synchronize toplevel window size that actually changed
  app->width  = event->width;
  app->height = event->height;

  // notify plugin of the new dimensions
  GtkDisplay *display = GTK_DISPLAY (app->display);
  display->width  = event->width;
  display->height = event->height;
  g_NPP_SetWindow (app->plugin, app->window, display);
  return FALSE;
}

static gboolean
on_window_state_event_cb (GtkWidget           *widget,
			  GdkEventWindowState *event,
			  PlayerApp           *app)
{
  if (g_backend != BACKEND_GTK)
    return FALSE;

  if (event->changed_mask & GDK_WINDOW_STATE_FULLSCREEN)
    {
      /* Fullscreen mode requested */
      GtkDisplay *display = GTK_DISPLAY (app->display);
      gtk_window_get_size (GTK_WINDOW (widget), &display->width, &display->height);
    }
  return FALSE;
}

static gboolean
on_window_destroy_cb (GtkWidget *widget, gpointer user_data)
{
  if (--g_n_plugins == 0)
    gtk_main_quit ();
  return FALSE;
}

static GdkFilterReturn
on_client_message_cb (GdkXEvent *gdk_xevent, GdkEvent *event, gpointer user_data)
{
  XEvent    *xevent = (XEvent *)gdk_xevent;
  PlayerApp *app    = (PlayerApp *)user_data;

  if (app->plugin == NULL || app->plugin->use_xembed)
    return GDK_FILTER_CONTINUE;

  if (xevent->type != ClientMessage)
    return GDK_FILTER_CONTINUE;

  if (xevent->xclient.message_type != gdk_x11_get_xatom_by_name ("WM_PROTOCOLS"))
    return GDK_FILTER_CONTINUE;

#if 0
  /* XXX: avoid gtk2 from resetting the focus to its focus proxy? */
  if ((Atom)xevent->xclient.data.l[0] == gdk_x11_get_xatom_by_name ("WM_TAKE_FOCUS"))
    return GDK_FILTER_REMOVE;
#endif

  return GDK_FILTER_CONTINUE;
}

static void
set_focus_window (PlayerApp *app)
{
  if (app->plugin == NULL || app->plugin->use_xembed)
    return;

  Window focus_window;
  int focus_state;
  XGetInputFocus (GDK_WINDOW_XDISPLAY (app->window->window), &focus_window, &focus_state);

  /* Don't change anything if the focus moved to another window per plugin's request */
  /* XXX: in that case, is GDK supposed to know about it for sure (non-NULL GDK window)? */
  GdkWindow *gdk_focus_window = gdk_window_lookup (focus_window);
  if (gdk_focus_window && gdk_focus_window != app->window->window)
    return;

  /* XXX: XSetInputFocus() is calling for trouble but I don't know of
   * a better way and Firefox is actually doing this...
   */
  gdk_error_trap_push ();
  XSetInputFocus (GDK_WINDOW_XDISPLAY (app->window->window),
		  GDK_WINDOW_XWINDOW (app->window->window),
		  RevertToNone, CurrentTime);
  gdk_flush ();
  gdk_error_trap_pop ();

  /* Give us a chance to filter out WM_TAKE_FOCUS */
  gdk_window_add_filter (NULL, on_client_message_cb, app);
}

static void
unset_focus_window (PlayerApp *app)
{
  if (app->plugin == NULL || app->plugin->use_xembed)
    return;

  gdk_window_remove_filter (NULL, on_client_message_cb, app);
}

static GdkFilterReturn
on_window_filter_cb (GdkXEvent *gdk_xevent, GdkEvent *event, gpointer user_data)
{
  XEvent    *xevent = (XEvent *)gdk_xevent;
  PlayerApp *app    = (PlayerApp *)user_data;
  GtkWidget *widget;
  GdkWindow *plugin_window;

  GdkFilterReturn ret = GDK_FILTER_CONTINUE;

  switch (xevent->type)
    {
    case CreateNotify:
    case ReparentNotify:
      /* Make sure we have not messed up the plugin->use_xembed logic */
      if (xevent->type == CreateNotify)
	plugin_window = gdk_window_lookup (xevent->xcreatewindow.window);
      else
	{
	  if (xevent->xreparent.event != xevent->xreparent.parent)
	    break;
	  plugin_window = gdk_window_lookup (xevent->xreparent.window);
	}
      if (plugin_window)
	{
	  user_data = NULL;
	  gdk_window_get_user_data (plugin_window, &user_data);
	  widget = GTK_WIDGET (user_data);

	  if (GTK_IS_XTBIN (widget))
	    {
	      g_assert (app->plugin ? !app->plugin->use_xembed : TRUE);
	      /* Ensure focus is set to the newly created (toplevel)
	       * window if the pointer turns out to hang over it already
	       */
	      set_focus_window (app);
	      break;
	    }
	  else if (GTK_IS_SOCKET (widget))
	    {
	      g_assert (app->plugin ? app->plugin->use_xembed : TRUE);
	      break;
	    }
	}
      ret = GDK_FILTER_REMOVE;
      break;
    case EnterNotify:
      set_focus_window (app);
      break;
    case DestroyNotify:
      gdk_window_remove_filter (app->window->window, on_window_filter_cb, app);
      unset_focus_window (app);
      break;
    default:
      break;
    }
  return ret;
}

static gboolean
on_key_press_event_cb (GtkWidget *widget, GdkEventKey *event, gpointer user_data)
{
  if (event->type != GDK_KEY_PRESS)
    return FALSE;

  switch (event->keyval)
    {
    case GDK_Escape:
    case GDK_Q:
    case GDK_q:
      if ((event->state & GDK_CONTROL_MASK) != 0)
	return on_window_destroy_cb (widget, user_data);
      break;
    }
  return FALSE;
}

typedef struct _PluginDescriptor PluginDescriptor;

struct _PluginDescriptor
{
  guint width;
  guint height;
  guint16 mode;
  GHashTable *attrs;
  PlayerApp  *app;
};

static PluginDescriptor *
plugin_descriptor_new (void)
{
  PluginDescriptor *plugin_desc = g_new0 (PluginDescriptor, 1);
  if (plugin_desc == NULL)
    return NULL;
  plugin_desc->width  = DEFAULT_WIDTH;
  plugin_desc->height = DEFAULT_HEIGHT;
  plugin_desc->mode   = NP_FULL;
  plugin_desc->attrs  = g_hash_table_new (g_str_hash, g_str_equal);
  return plugin_desc;
}

int
main (int argc, char *argv[])
{
  GPtrArray        *plugin_descs = g_ptr_array_new ();
  PluginDescriptor *plugin_desc  = NULL;

  const gchar *title     = "nspluginplayer";
  gboolean is_fullscreen = FALSE;
  guint display_width    = DEFAULT_WIDTH;
  guint display_height   = DEFAULT_HEIGHT;
  gint i;

  g_thread_init (NULL);
  gdk_threads_init ();
  glibcurl_init ();
  glibcurl_set_callback (on_stream_close_cb, NULL);
  gtk_init (&argc, &argv);

  for (i = 1; i < argc; i++)
    {
      const gchar *arg = argv[i];
      if (strcmp (arg, "--help") == 0)
	{
	  print_help (argv[0]);
	  return 0;
	}
      else if (strcmp (arg, "--verbose") == 0)
	g_verbose = TRUE;
      else if (strcmp (arg, "--title") == 0)
	{
	  if (++i < argc)
	    title = argv[i];
	}
      else if (strcmp (arg, "--gtk") == 0)
	g_backend = BACKEND_GTK;
      else if (strcmp (arg, "--backend") == 0)
	{
	  if (++i < argc)
	    {
	      const gchar *backend_str = argv[i];
	      if (strcmp (backend_str, "gtk") == 0)
		g_backend = BACKEND_GTK;
	      else
		g_error ("unknown backend '%s'", backend_str);
	    }
	}
      else if (strcmp (arg, "--window") == 0)
	is_fullscreen = FALSE;
      else if (strcmp (arg, "--fullscreen") == 0)
	is_fullscreen = TRUE;
      else if (strcmp (arg, "--xid") == 0)
        {
	  if (++i < argc)
	    {
	      g_xid = atoi(argv[i]);
	      npw_printf ("attaching to xid %d\n", g_xid);
	    }
        }
      else if (strcmp (arg, "--plugin") == 0)
	{
	  if (plugin_desc)
	    g_ptr_array_add (plugin_descs, plugin_desc);
	  plugin_desc = plugin_descriptor_new ();
	}
      else
	{
	  if (plugin_desc == NULL)
	    plugin_desc = plugin_descriptor_new ();

	  gchar **attrs = g_strsplit (arg, "=", 2);
	  if (attrs)
	    {
	      gchar *name  = attrs[0];
	      gchar *value = attrs[1];
	      if (value == NULL)
		{
		  /* Only accept "embed" and "full" attributes */
		  if (g_ascii_strcasecmp (name, "embed") == 0)
		    plugin_desc->mode = NP_EMBED;
		  else if (g_ascii_strcasecmp (name, "full") == 0)
		    plugin_desc->mode = NP_FULL;
		  else
		    npw_printf ("WARNING: skip attribute '%s'\n", name);
		}
	      else if (g_ascii_strcasecmp (name, "width") == 0)
		plugin_desc->width = atoi (value);
	      else if (g_ascii_strcasecmp (name, "height") == 0)
		plugin_desc->height = atoi (value);
	      else
		{
		  /* Build up the attrs hash with names in lowercase */
		  if (g_ascii_strcasecmp (name, "src") == 0)
		    value = sanitize_url (value);
		  else
		    value = g_strdup (value);

		  g_hash_table_insert (plugin_desc->attrs,
				       g_ascii_strdown (name, -1), value);
		}
	    }
	  g_strfreev (attrs);
	}
    }

  if (plugin_desc)
    g_ptr_array_add (plugin_descs, plugin_desc);

  if (plugin_descs->len == 0)
    {
      print_help (argv[0]);
      return 1;
    }

  const gchar *backend_str;
  switch (g_backend)
    {
    case BACKEND_GTK:
      backend_str = "gtk";
      break;
    default:
      g_error ("unknown backend type (%d)", g_backend);
      break;
    }

  g_n_plugins = plugin_descs->len;
  for (i = 0; i < plugin_descs->len; i++)
    {
      PluginDescriptor *plugin_desc = g_ptr_array_index (plugin_descs, i);
      guint width  = plugin_desc->width;
      guint height = plugin_desc->height;

      /* XXX: in Gtk windowed mode, fit window to plugin drawing area size */
      if (g_backend == BACKEND_GTK)
	{
	  display_width  = plugin_desc->width;
	  display_height = plugin_desc->height;
	}

      PlayerApp *app;
      if ((app = g_new0 (PlayerApp, 1)) == NULL)
	g_error ("could not allocate application data");
      app->width  = display_width;
      app->height = display_height;
      app->mode   = plugin_desc->mode;
      app->attrs  = plugin_desc->attrs;
      plugin_desc->app = app;

      g_timeout_add (100, (GSourceFunc)player_app_run, app);

      GtkWidget *window;
      if (g_xid)
	window = gtk_plug_new ((GdkNativeWindow)g_xid);
      else
	window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
      if ((app->window = window) == NULL)
	g_error ("could not create toplevel window");

      gtk_widget_set_size_request (window, display_width, display_height);
      gtk_window_set_title (GTK_WINDOW (window), title);
      if (is_fullscreen && !g_xid)
	gtk_window_fullscreen (GTK_WINDOW (window));
      gtk_widget_show (window);

      /* Ensure focus window is this window not gtk's proxy for non XEMBED case */
      XWindowAttributes xattrs;
      XGetWindowAttributes (GDK_DISPLAY (), GDK_WINDOW_XWINDOW (window->window), &xattrs);
      XSelectInput (GDK_DISPLAY (),
		    GDK_WINDOW_XWINDOW (window->window),
		    xattrs.your_event_mask | SubstructureNotifyMask);
      gdk_window_add_filter (window->window, on_window_filter_cb, app);
      XSync (GDK_DISPLAY (), False);

      g_signal_connect (window, "destroy",
			G_CALLBACK (on_window_destroy_cb), NULL);
      g_signal_connect (window, "key-press-event",
			G_CALLBACK (on_key_press_event_cb), app);
      g_signal_connect (window, "configure-event",
			G_CALLBACK (on_configure_event_cb), app);
      g_signal_connect (window, "window-state-event",
			G_CALLBACK (on_window_state_event_cb), app);

      if (g_backend == BACKEND_GTK)
	{
	  GtkWidget *rwindow = NULL;
	  GtkWidget *canvas  = NULL;

	  GtkDisplay *display = g_new0 (GtkDisplay, 1);
	  display->window  = rwindow;
	  display->canvas  = canvas;
	  display->width   = width;
	  display->height  = height;
	  app->display     = display;
	}
    }

  if (g_backend == BACKEND_GTK)
    gtk_main ();

  for (i = 0; i < plugin_descs->len; i++)
    {
      PluginDescriptor *plugin_desc = g_ptr_array_index (plugin_descs, i);
      if (plugin_desc->app)
	{
	  player_app_quit (plugin_desc->app);
	  player_app_destroy (plugin_desc->app);
	}
      g_free (plugin_desc);
    }
  g_ptr_array_free (plugin_descs, TRUE);
  return 0;
}
