/* -*- Mode: C; tab-width: 8; -*-
 *
 * Netscape Plugin for Squeak on Unix platforms
 * 
 * Author:  Bert Freudenberg <bert@isg.cs.uni-magdeburg.de>
 *
 * History:
 *          Apr 2000 - url requests through browser
 *          Nov 1999 - report attributes to vm
 *          Aug 99   - initial version 
 */

#define XP_UNIX
#include <npapi.h>

#include <stdio.h>
#include <unistd.h>
#include <limits.h>
#include <signal.h>
#include <string.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/stat.h>
#include <X11/Xlib.h>
#include <X11/Intrinsic.h>

#undef DEBUG

#ifdef DEBUG
#  define FPRINTF(X) fprintf X
#else
#  define FPRINTF(X)
#endif

/***********************************************************************
 * Plugin instance data
 ***********************************************************************/

static Atom XA_SET_PIPES;
static Atom XA_BROWSER_WINDOW;
static Atom XA_GET_URL;
static Atom XA_POST_URL;
static Atom XA_RECEIVE_DATA;

#define MAX_STREAMS 128
#define SQUEAK_READ  0
#define PLUGIN_WRITE 1
#define PLUGIN_READ  2
#define SQUEAK_WRITE 3

typedef struct SqueakPlugin {
  NPP instance;                    /* plugin instance */
  pid_t pid;                       /* the child process pid */
  Display *display;
  Window nswindow;                 /* the netscape window */
  Window sqwindow;                 /* the squeak window */
  Widget pagewidget;               /* the widget with our page */
  Bool embedded;                   /* false if we have the whole window */
  char **argv;                     /* the commandline for squeak vm */
  int  argc;
  char vmName[PATH_MAX];
  char imageName[PATH_MAX];
  int pipes[4];                    /* 4 ends of 2 pipes */
  char* srcUrl;                    /* set by browser in first NewStream */
  char* srcFilename;
  int srcId;                       /* if requested */
} SqueakPlugin;

typedef struct SqueakStream {
  int id;                          /* request id (0 if finished)  */
} SqueakStream;

/***********************************************************************
 * Prototypes
 ***********************************************************************/

static int SqueakCheckFile(char *filename);
static int SqueakGetBaseDir(char *basedir);
static void SqueakSendEvent(SqueakPlugin*, Atom message_type, long data0, long data1);
static void SqueakDeliverFile(SqueakPlugin *, int id, const char* fname);
static void SqueakSetWindow(SqueakPlugin*, Window window, int width, int height);
static void SqueakSetUpWindow(SqueakPlugin*);
static void SqueakRun(SqueakPlugin*);
static void SqueakGetUrl(SqueakPlugin *, int id);
static void SqueakPostUrl(SqueakPlugin *, int id);
void SqueakHandleEvent(Widget widget, SqueakPlugin *, XEvent *event);

/***********************************************************************
 * Plugin registration
 ***********************************************************************/

char*
NPP_GetMIMEDescription(void)
{
  return("application/x-squeak-source:sts:Squeak source"
	 ";application/x-squeak-object:sqo:Squeak object");
}

NPError
NPP_GetValue(void *instance, NPPVariable variable, void *value)
{
  switch (variable) {
  case NPPVpluginNameString:
    *((char **)value) = "Squeak Plugin";
    break;
  case NPPVpluginDescriptionString:
    *((char **)value) =
      "The Squeak Plugin handles interactive content"
      " provided by <a href=\"http://squeak.org/\">Squeak</a>"
      " applets.";
    break;
  default:
    return NPERR_GENERIC_ERROR;
  }
  return NPERR_NO_ERROR;
}

/***********************************************************************
 * Plugin loading and termination
 ***********************************************************************/

NPError 
NPP_New(NPMIMEType pluginType, NPP instance, uint16 mode, int16 argc,
		char* argn[], char* argv[], NPSavedData* saved)
{
  SqueakPlugin *squeak;
  char basedir[PATH_MAX];
  if (instance == NULL)
    return NPERR_INVALID_INSTANCE_ERROR;
  squeak = (SqueakPlugin*) NPN_MemAlloc(sizeof(SqueakPlugin));
  if (!squeak)
    return NPERR_OUT_OF_MEMORY_ERROR;
  squeak->argv = (char**) NPN_MemAlloc(sizeof(char*) * (16+2*argc));
  if (!squeak->argv)
    return NPERR_OUT_OF_MEMORY_ERROR;
  /* Default settings */
  if (!SqueakGetBaseDir(basedir)) {
    fprintf(stderr, "Squeak Plugin: You may want to set SQUEAK_PLUGIN_DIR.\n");
    return NPERR_GENERIC_ERROR;
  }
  squeak->instance = instance;
  squeak->pid = 0;
  squeak->nswindow = 0;
  squeak->sqwindow = 0;
  squeak->pagewidget = 0;
  squeak->display = NULL;
  squeak->embedded = mode == NP_EMBED;
  squeak->srcUrl = NULL;
  squeak->srcFilename = NULL;
  squeak->srcId = -1;
  strcpy(squeak->vmName, basedir);
  strcat(squeak->vmName, "squeakvm");
  strcpy(squeak->imageName, basedir);
  strcat(squeak->imageName, "image/plugin.image");
  squeak->argv[0] = squeak->vmName;
  squeak->argv[1] = "-display";
  squeak->argv[2] = ":0";             /* inserted later */
  squeak->argv[3] = "-browserWindow";
  squeak->argv[4] = "0";              /* inserted later */
  squeak->argv[5] = squeak->imageName; 
  squeak->argv[6] = "";               /* no document file on cmdline! */ 
  squeak->argc = 7;
  if (squeak->embedded) {
    int i;
    for (i = 0; i < argc; i++) {
      squeak->argv[squeak->argc++] = argn[i];
      squeak->argv[squeak->argc++] = argv[i] ? argv[i] : "";
      if (strcasecmp("SRC", argn[i]) == 0)
	squeak->srcUrl = strdup(argv[i]);
    }
    if (!squeak->srcUrl)
      squeak->srcUrl = strdup(""); /* we were embedded without a SRC */
  } else {
    /* if not embedded srcUrl will be set in NewStream */
    squeak->srcUrl = NULL;
  }
  squeak->argv[squeak->argc] = 0; 
  if (pipe(&squeak->pipes[SQUEAK_READ])
      || pipe(&squeak->pipes[PLUGIN_READ])) {
    perror("Squeak Plugin: Creating pipes failed");
    return NPERR_GENERIC_ERROR;
  }
  if (!SqueakCheckFile(squeak->vmName) || !SqueakCheckFile(squeak->imageName))
    return NPERR_GENERIC_ERROR;
  instance->pdata = (void*) squeak;
  return NPERR_NO_ERROR;
}

NPError 
NPP_Destroy(NPP instance, NPSavedData** save)
{
  SqueakPlugin *squeak;
  if (!instance)
    return NPERR_INVALID_INSTANCE_ERROR;
  squeak = (SqueakPlugin*) instance->pdata;
  if (squeak) {
    int i;
    if (squeak->pid) {
      kill(squeak->pid, SIGTERM);
      squeak->pid = 0;
    }
    if (squeak->pagewidget) {
      XtRemoveEventHandler(squeak->pagewidget, StructureNotifyMask, False, 
			   (XtEventHandler) SqueakHandleEvent, squeak);
      squeak->pagewidget = 0;
    }
    for (i=0; i<4; i++)
      if (squeak->pipes[i]) {
	close(squeak->pipes[i]);
	squeak->pipes[i] = 0;
      }
    if (squeak->srcUrl) {
      free(squeak->srcUrl);
      squeak->srcUrl = NULL;
    }
    if (squeak->srcFilename) {
      free(squeak->srcFilename);
      squeak->srcFilename = NULL;
    }
    NPN_MemFree(squeak);
  }
  instance->pdata = NULL;
  return NPERR_NO_ERROR;
}

/***********************************************************************
 * Plugin events we need to handle
 ***********************************************************************/

NPError 
NPP_SetWindow(NPP instance, NPWindow *pNPWindow)
{
  SqueakPlugin *squeak;
  if (!instance)
    return NPERR_INVALID_INSTANCE_ERROR;
  squeak = (SqueakPlugin*) instance->pdata;
  if (!squeak)
    return NPERR_GENERIC_ERROR;
  if (pNPWindow == NULL) 
    return NPERR_NO_ERROR;

  if (!squeak->display) {
    /* first time only */
    squeak->display = ((NPSetWindowCallbackStruct *)pNPWindow->ws_info)->display;
    XA_SET_PIPES = XInternAtom(squeak->display, "SQUEAK_SET_PIPES", False);
    XA_BROWSER_WINDOW = XInternAtom(squeak->display, "SQUEAK_BROWSER_WINDOW", False);
    XA_GET_URL = XInternAtom(squeak->display, "SQUEAK_GET_URL", False);
    XA_POST_URL = XInternAtom(squeak->display, "SQUEAK_POST_URL", False);
    XA_RECEIVE_DATA = XInternAtom(squeak->display, "SQUEAK_RECEIVE_DATA", False);
    if (squeak->embedded) {
      /* Need to capture resizes */
      Widget temp = XtWindowToWidget(squeak->display, (Window) pNPWindow->window);
      while ((strcmp(XtName(temp), "pane") != 0))
	temp = XtParent(temp);
      squeak->pagewidget = temp;
      XtAddEventHandler(squeak->pagewidget, StructureNotifyMask, False, 
			(XtEventHandler) SqueakHandleEvent, squeak);
    }
  }
  SqueakSetWindow(squeak, (Window) pNPWindow->window, 
		  pNPWindow->width, pNPWindow->height);
  if (!squeak->pid)
    SqueakRun(squeak);
  return NPERR_NO_ERROR;
}


NPError 
NPP_NewStream(NPP instance, NPMIMEType type, NPStream *stream, NPBool seekable, uint16 *stype)
{
  SqueakPlugin *squeak = (SqueakPlugin*) instance->pdata;
  FPRINTF((stderr, "NewStream(%s, id=%i)\n", stream->url, 
	   stream->notifyData ? ((SqueakStream*) stream->notifyData)->id : -1));
  if (!stream->notifyData && !squeak->srcUrl) {
    /* We did not request this stream, so it is our SRC file. */
    squeak->srcUrl = strdup(stream->url);
    squeak->argv[squeak->argc++] = "SRC";
    squeak->argv[squeak->argc++] = squeak->srcUrl;
    FPRINTF((stderr, "  got srcUrl=%s\n", squeak->srcUrl));
    SqueakRun(squeak);
  }
    
  *stype = NP_ASFILEONLY;          /* We want the file after download */
  return NPERR_NO_ERROR;
}


NPError 
NPP_DestroyStream(NPP instance, NPStream *stream, NPError reason)
{
  /* We'll clean up in URLNotify */
  FPRINTF((stderr, "DestroyStream(%s, id=%i)\n", stream->url, 
	   stream->notifyData ? ((SqueakStream*) stream->notifyData)->id : -1));
  return NPERR_NO_ERROR;
}


void 
NPP_StreamAsFile(NPP instance, NPStream *stream, const char* fname)
{
  int length;
  int id = stream->notifyData ? ((SqueakStream*) stream->notifyData)->id : -1;
  SqueakPlugin *squeak = (SqueakPlugin*) instance->pdata;
  FPRINTF((stderr, "StreamAsFile(%s, id=%i)\n", stream->url, id));
  FPRINTF((stderr, "  fname=%s\n", fname ? fname : "<NULL>"));
  if (!squeak || !fname) return;

  if (!stream->notifyData && !squeak->srcFilename) {
    /* We did not request this stream, so it is our SRC file. */
    squeak->srcFilename = strdup(fname);
    FPRINTF((stderr, "  got srcFilename=%s\n", squeak->srcFilename));
    if (squeak->srcId >= 0) {
      /* plugin wanted it already */
      SqueakDeliverFile(squeak, squeak->srcId, squeak->srcFilename);
      squeak->srcId = -1;
    }
    return;
  }

  SqueakDeliverFile(squeak, id, fname);

  /* signal URLNotify that we're done */
  ((SqueakStream*) stream->notifyData)->id = 0;
}

void
NPP_URLNotify(NPP instance, const char* url, NPReason reason, void* notifyData)
{
  int id = notifyData ? ((SqueakStream*) notifyData)->id : 0;
  int ok = reason == NPRES_DONE;
  SqueakPlugin *squeak = (SqueakPlugin*) instance->pdata;
  FPRINTF((stderr, "URLNotify(%s, id=%i, ok=%i)\n", url, id, ok));
  if (notifyData) NPN_MemFree(notifyData);
  if (!squeak || !id) return;

  SqueakDeliverFile(squeak, id, NULL);
}

/***********************************************************************
 * Plugin stubs
 ***********************************************************************/


NPError
NPP_Initialize(void)
{
  fprintf(stderr, "PLUGIN INIT\n");
  return NPERR_NO_ERROR;
}


void
NPP_Shutdown(void)
{
  fprintf(stderr, "PLUGIN SHUTDOWN\n");
}


/* We don't have an associated java class */

jref
NPP_GetJavaClass()
{
  return NULL;
}

/* We don't really stream */

int32 
NPP_WriteReady(NPP instance, NPStream *stream)
{
  return 0X0FFFFFFF;
}


int32 
NPP_Write(NPP instance, NPStream *stream, int32 offset, int32 len, void *buffer)
{
  return len;
}


/* We don't print */

void 
NPP_Print(NPP instance, NPPrint* printInfo)
{
}


/***********************************************************************
 * Our functions
 ***********************************************************************/

static int
SqueakCheckFile(char *filename)
{
  struct stat statBuf;
  if (stat(filename, &statBuf)) {
    perror("Squeak Plugin");
    fprintf(stderr, "  '%s'\n", filename);
    return 0;
  }
  return 1;
}

static int
SqueakGetBaseDir(char *basedir)
{
  char* ev;
  ev = getenv("SQUEAK_PLUGIN_DIR");
  if (ev) {
    strcpy(basedir, ev);
    if (basedir[strlen(basedir)-1] != '/') 
      strcat(basedir, "/");
  } else {
    ev = getenv("HOME");
    if (ev) {
      strcpy(basedir, ev);
      strcat(basedir, "/.netscape/squeak/");
    } else {
      basedir[0] = '\0';
      fprintf(stderr, "Squeak Plugin: could not find squeak directory.\n");
      return 0;		
    }
  }
  return SqueakCheckFile(basedir);
}


static void
SqueakSendEvent(SqueakPlugin* squeak, Atom message_type, long data0, long data1)
{
  XClientMessageEvent event;
 
  event.type = ClientMessage;
  event.display = squeak->display;
  event.window = squeak->sqwindow;
  event.message_type = message_type;
  event.format = 32;
  event.data.l[0] = data0;
  event.data.l[1] = data1;

  XSendEvent(squeak->display, squeak->sqwindow, True, 
	     0, (XEvent*) &event);
}


static void 
SqueakDeliverFile(SqueakPlugin *squeak, int id, const char* fname)
{
  int ok = fname != NULL;
  FPRINTF((stderr, "  Send RECEIVE_DATA id=%i state=%i\n", id, ok));
  SqueakSendEvent(squeak, XA_RECEIVE_DATA, id, ok);

  if (ok) {
    int length = strlen(fname);
    errno = 0;
    write(squeak->pipes[PLUGIN_WRITE], &length, 4);
    write(squeak->pipes[PLUGIN_WRITE], fname, length);
    if (errno)
      perror("Squeak Plugin (StreamAsFile)");
  }
}


static void 
SqueakRun(SqueakPlugin* squeak)
{
  if (squeak->pid || !squeak->nswindow || !squeak->srcUrl)
    return;
  
  squeak->pid = fork();
  
  if (squeak->pid == -1) {
    perror("Squeak fork() failed");
    squeak->pid = 0;
    return;
  }
  FPRINTF((stderr, "fork() -> %i\n", squeak->pid));
  if (squeak->pid == 0) {
    char browserWindow[16];
    sprintf(browserWindow, "0x%X", squeak->nswindow);
    squeak->argv[2] = DisplayString(squeak->display);
    squeak->argv[4] = browserWindow;
#ifdef DEBUG
    {
      int i;
      for (i = 0; i<squeak->argc; i++)
	fprintf(stderr, "%s\n    ", squeak->argv[i]);
    }
#endif
    /* this is from the XLib manual ... */
    if ((fcntl(ConnectionNumber(squeak->display), F_SETFD, 1)) == -1)
      FPRINTF((stderr, "Cannot disinherit X connection fd\n"));
    execv(squeak->vmName, squeak->argv);
    fprintf(stderr, "Squeak Plugin: running \"%s\"\n", squeak->vmName);
    perror("Squeak execv() failed");
    exit(1);
  }
  /* XXX: Should check whether exec() really was successfull*/
}


static void
SqueakSetWindow(SqueakPlugin *squeak, Window window, int width, int height)
{
  FPRINTF((stderr, "SqueakSetWindow(0x%X, %i@%i)\n", window, width, height));
  if (squeak->nswindow == window) {
    XResizeWindow(squeak->display, squeak->nswindow, width, height);
  } else {
    /* New window */
    squeak->nswindow = window;
    SqueakSetUpWindow(squeak);
    if (squeak->sqwindow) {
      FPRINTF((stderr, "Reparenting to plugin window 0x%X\n", squeak->nswindow));
      XReparentWindow(squeak->display, squeak->sqwindow, squeak->nswindow, 0, 0);
      SqueakSendEvent(squeak, XA_BROWSER_WINDOW, squeak->nswindow, 0);
    }
  }
  if (squeak->sqwindow)
    XResizeWindow(squeak->display, squeak->sqwindow, width, height);
}


static void
SqueakSetUpWindow(SqueakPlugin *squeak)
{
  FPRINTF((stderr, "SqueakSetUpWindow(0x%X)\n", squeak->nswindow));
  XSelectInput(squeak->display, squeak->nswindow, 0);
  /* We need this to capture the Squeak window */
  if (!squeak->sqwindow)
    XtAddEventHandler(XtWindowToWidget(squeak->display, squeak->nswindow),
		      SubstructureNotifyMask, True, 
		      (XtEventHandler) SqueakHandleEvent, squeak);
}

void
SqueakHandleEvent(Widget widget, SqueakPlugin *squeak, XEvent *event)
{
  if (!squeak) return;
  switch (event->type) {
  case CreateNotify: {
    XCreateWindowEvent *ev = (XCreateWindowEvent*) event;
    FPRINTF((stderr, "CreateNotify(0x%X, 0x%X)\n", ev->parent, ev->window));
    if (ev->parent == squeak->nswindow && !squeak->sqwindow) {
			/* squeak just opened its window */
      XWindowAttributes attr;
      squeak->sqwindow = ev->window;
      XGetWindowAttributes(squeak->display, squeak->nswindow, &attr);
      XResizeWindow(squeak->display, squeak->sqwindow, attr.width, attr.height);
      FPRINTF((stderr, "got sqwindow=0x%X\n", squeak->sqwindow));
      SqueakSendEvent(squeak, XA_SET_PIPES, 
		      squeak->pipes[SQUEAK_READ], 
		      squeak->pipes[SQUEAK_WRITE]);
      FPRINTF((stderr, "sent pipes\n"));
    }
    break;
  }
  case ConfigureNotify: {
    XConfigureEvent *ev = (XConfigureEvent*) event;
    if (widget == squeak->pagewidget) {
      /* Save this window from being destroyed by page re-layout */
      /* Move beyond bounds so the jump is invisible */
      FPRINTF((stderr, "Reparenting to page window 0x%X\n", ev->window));
      XReparentWindow(squeak->display, squeak->sqwindow, ev->window, ev->width, 0);
    }
    break;
  }
  case ClientMessage: {
    XClientMessageEvent *ev = (XClientMessageEvent*) event;
    if (ev->window == squeak->nswindow) {
      if (ev->message_type == XA_GET_URL)
	SqueakGetUrl(squeak, ev->data.l[0]);
      else if (ev->message_type == XA_POST_URL)
	SqueakPostUrl(squeak, ev->data.l[0]);
    }
    break;
  }
  }
}


static void
SqueakGetUrl(SqueakPlugin *squeak, int id)
{
  char *url, *target;
  int urlSize, targetSize;

  errno = 0;
  /* Read URL from pipe */
  read(squeak->pipes[PLUGIN_READ], &urlSize, 4);
  if (urlSize > 0) {
    url = malloc(urlSize+1);
    read(squeak->pipes[PLUGIN_READ], url, urlSize);
    url[urlSize] = 0;
  } else url = NULL;
  /* Read target from pipe */
  read(squeak->pipes[PLUGIN_READ], &targetSize, 4);
  if (targetSize > 0) {
    target = malloc(targetSize+1);
    read(squeak->pipes[PLUGIN_READ], target, targetSize);
    target[targetSize] = 0;
  } else target = NULL;

  if (errno) {
    perror("Squeak Plugin (GetUrl)");
  } else {
    if (strcmp(url, squeak->srcUrl)==0) {
      if (squeak->srcFilename)
	SqueakDeliverFile(squeak, id, squeak->srcFilename);
      else
	squeak->srcId = id;
    } else {
      SqueakStream* notifyData = (SqueakStream*) NPN_MemAlloc(sizeof(SqueakStream));
      if (!notifyData) { 
	fprintf(stderr, "Squeak Plugin (GetUrl): alloc failed\n");
      } else {
	FPRINTF((stderr, "GetURLNotify(%s, id=%i)\n", url, id));
	notifyData->id = id;
	NPN_GetURLNotify(squeak->instance, url, target, notifyData);
      }
    }
  }

  if (url) free(url);
  if (target) free(target);
}

static void
SqueakPostUrl(SqueakPlugin *squeak, int id)
{
  char *url, *target, *data;
  int urlSize, targetSize, dataSize;

  errno = 0;
  /* Read URL from pipe */
  read(squeak->pipes[PLUGIN_READ], &urlSize, 4);
  if (urlSize > 0) {
    url = malloc(urlSize+1);
    read(squeak->pipes[PLUGIN_READ], url, urlSize);
    url[urlSize] = 0;
  } else url = NULL;
  /* Read target from pipe */
  read(squeak->pipes[PLUGIN_READ], &targetSize, 4);
  if (targetSize > 0) {
    target = malloc(targetSize+1);
    read(squeak->pipes[PLUGIN_READ], target, targetSize);
    target[targetSize] = 0;
  } else target = NULL;
  /* Read post data from pipe */
  read(squeak->pipes[PLUGIN_READ], &dataSize, 4);
  if (dataSize > 0) {
    data = malloc(dataSize);
    read(squeak->pipes[PLUGIN_READ], data, dataSize);
  } else data = NULL;

  if (errno) {
    perror("Squeak Plugin (PostUrl)");
  } else {
    SqueakStream* notifyData = (SqueakStream*) NPN_MemAlloc(sizeof(SqueakStream));
    if (!notifyData) { 
      fprintf(stderr, "Squeak Plugin (PostUrl): alloc failed\n");
    } else {
      FPRINTF((stderr, "PostURLNotify(%s, id=%i)\n", url, id));
      notifyData->id = id;
      NPN_PostURLNotify(squeak->instance, url, target, 
			dataSize, data, FALSE, notifyData);
    }
  }

  if (url) free(url);
  if (target) free(target);
  if (data) free(data);
}
