/* -*- Mode: C; tab-width: 2; -*-
 *
 * Netscape Browser Plugin for Squeak on Windows platforms
 * 
 * Author:  Takashi Yamamiya
 *
 * Last edited: 2005-11-21 by tak on metatoys.org
 *
 * History:
 *          Nov 2006 - initial version
 */

#include <windows.h>
#include <windowsx.h>
#include "pluginbase.h"

/* constants */
#define NON_REQUESTED_ID -1
#define SQUEAK_READ  0    /* unused */
#define PLUGIN_WRITE 1    /* plugin *->  vm */
#define PLUGIN_READ  2    /* plugin *<-  vm */
#define SQUEAK_WRITE 3    /* plugin  <-* vm */
#define VM_REGISTRY_KEY "Software\\Squeak"
#define VM_REGISTRY_NAME "InstallDirectory"

DWORD g_WM_QUIT_SESSION = 0; /* Request to quit squeak session */
DWORD g_WM_BWND_SIZE = 0;    /* Browser window size changed */
DWORD g_WM_REQUEST_DATA = 0; /* Request data from browser */
DWORD g_WM_POST_DATA = 0;    /* Post data to browser */
DWORD g_WM_RECEIVE_DATA = 0; /* Receive data from browser */
DWORD g_WM_INVALIDATE = 0;   /* Invalidate portion of window */
DWORD g_WM_BROWSER_PIPE = 0; /* Browser pipe */
DWORD g_WM_CLIENT_PIPE = 0;  /* Client pipe */

class SqueakPlugin : public nsPluginInstanceBase
{
public:
  SqueakPlugin(nsPluginCreateData * aCreateDataStruct);
  ~SqueakPlugin();

  /*  Netscape API Handler */
  NPBool init(NPWindow * aWindow);
  void shut();
  NPBool isInitialized();
  NPError SetWindow(NPWindow * pNPWindow);
  NPError NewStream(NPMIMEType type, NPStream* stream, NPBool seekable, uint16 * stype);
  void StreamAsFile(NPStream * stream, const char * fname);
  void URLNotify(const char * url, NPReason reason, void * notifyData);

  /* Our functions */
  void Run(void);
  void GetClientPipe(WPARAM wParam, LPARAM lParam);
  void GetUrl(int id);
  void PostUrl(int id);

private:
  /* Variables */
  NPP mInstance;              /* plugin instance */
  nsPluginCreateData createDataStruct;
  NPBool mInitialized;        
  HWND mhWnd;                 /* the netscape window */
  HANDLE pipes[4];            /* 4 ends of 2 pipes */
  char vmCommandLine[1024];   /* A command line string for Squeak VM */
  PROCESS_INFORMATION vmProc; /* Squeak VM process information */

  /* Private functions */
  char * Receive(DWORD * psize);
  void Send(const char * buffer);
  void DeliverFile(int id, const char * fname);
  bool BuildCommandLine(int16 argc, char** argn, char** argv, HWND hWnd);
  bool CatStrings(char * destString, char * srcStrings[], size_t maxSize);
  bool GetSqueakDirName(char * dirName, DWORD dirNameSize);

  /* Error handling */
  void handleError(char* message);
  bool isError;
  char lastError[1024];
};

/* URL notify data */
typedef struct SqueakStream {
  int id;                     /* request id (0 if finished)  */
} SqueakStream;

static LRESULT CALLBACK PluginWinProc(HWND, UINT, WPARAM, LPARAM);
static WNDPROC lpOldProc = NULL;
static void DPRINT(char *format, ...);

/*
 * Register the communication events for the plugin.
*/
NPError NS_PluginInitialize()
{ 
  g_WM_QUIT_SESSION = RegisterWindowMessage(TEXT("SqueakQuitSession"));
  g_WM_BWND_SIZE = RegisterWindowMessage(TEXT("SqueakSetBrowserWindowSize"));
  g_WM_REQUEST_DATA = RegisterWindowMessage(TEXT("SqueakRequestData"));
  g_WM_POST_DATA = RegisterWindowMessage(TEXT("SqueakPostData"));
  g_WM_RECEIVE_DATA = RegisterWindowMessage(TEXT("SqueakReceiveData"));
  g_WM_INVALIDATE = RegisterWindowMessage(TEXT("SqueakInvalidateRect"));
  g_WM_BROWSER_PIPE = RegisterWindowMessage(TEXT("SqueakBrowserPipe"));
  g_WM_CLIENT_PIPE = RegisterWindowMessage(TEXT("SqueakClientPipe"));
  return NPERR_NO_ERROR;
}

void NS_PluginShutdown()
{
}

/* construction and destruction of our plugin instance object */

nsPluginInstanceBase * NS_NewPluginInstance(nsPluginCreateData * aCreateDataStruct)
{
  if(!aCreateDataStruct)
    return NULL;

  SqueakPlugin * plugin = new SqueakPlugin(aCreateDataStruct);
  return plugin;
}

void NS_DestroyPluginInstance(nsPluginInstanceBase * aPlugin)
{
  if(aPlugin) delete (SqueakPlugin *)aPlugin;
}

/* SqueakPlugin class implementation */

SqueakPlugin::SqueakPlugin(nsPluginCreateData * aCreateDataStruct) : nsPluginInstanceBase()
{
  mInstance = aCreateDataStruct->instance;
  mInitialized = false;
  createDataStruct = *aCreateDataStruct;
  mhWnd = NULL;
  for (int i = 0; i < 4; i ++) pipes[i] = NULL;
  memset(&vmProc, 0, sizeof(vmProc));
  memset(vmCommandLine, '\0', sizeof(vmCommandLine));
  isError = false;
}

SqueakPlugin::~SqueakPlugin()
{
}

NPBool SqueakPlugin::init(NPWindow* aWindow)
{

  if(aWindow == NULL) return FALSE;
  mhWnd = (HWND)aWindow->window;
  if(mhWnd == NULL) return FALSE;

  lpOldProc = SubclassWindow(mhWnd, (WNDPROC)PluginWinProc);
  SetWindowLong(mhWnd, GWL_USERDATA, (LONG)this);

  mInitialized = TRUE;
  this->Run();

  return TRUE;
}

void SqueakPlugin::shut()
{
if (PostThreadMessage(vmProc.dwThreadId,
    g_WM_QUIT_SESSION, 0, 0) == 0)
      handleError(NULL);

  if(pipes[PLUGIN_READ]) CloseHandle(pipes[PLUGIN_READ]);
  if(pipes[SQUEAK_WRITE]) CloseHandle(pipes[SQUEAK_WRITE]);
  if(pipes[PLUGIN_WRITE]) CloseHandle(pipes[PLUGIN_WRITE]);
  if (vmProc.dwProcessId != 0)
  {
    CloseHandle(vmProc.hProcess);
    CloseHandle(vmProc.hThread);
  }

  // subclass it back
  SubclassWindow(mhWnd, lpOldProc);
  mhWnd = NULL;
  mInitialized = false;
}

NPBool SqueakPlugin::isInitialized()
{
  return mInitialized;
}

/* update plugin window (pNPWindow is not used) */

NPError SqueakPlugin::SetWindow(NPWindow* pNPWindow)
{
  if (!this->isError)
  {
    if (PostThreadMessage(vmProc.dwThreadId, g_WM_INVALIDATE, 0, 0) == 0)
        handleError(NULL);
  }
  else
  {
    PAINTSTRUCT ps;
    char errString[1024];
    _snprintf(errString, sizeof(errString), "\n\n\nSqueak Plugin Error:\n%s", lastError);
    HDC hdc = BeginPaint(mhWnd, &ps);
    RECT rc;
    GetClientRect(mhWnd, &rc);
    DrawText(hdc, errString, -1, &rc, DT_CENTER | DT_VCENTER);
    EndPaint(mhWnd, &ps);
  }
  return nsPluginInstanceBase::SetWindow(pNPWindow);
}

/* handles NPN_GetURLNotify() */

void SqueakPlugin::URLNotify(const char* url, NPReason reason, void* notifyData)
{
  int id = (notifyData ? ((SqueakStream*) notifyData)->id : NON_REQUESTED_ID);
  int ok = (reason == NPRES_DONE);
  DPRINT("NP: NPP_URLNotify(%s, id=%i, ok=%i)\n", url, id, ok);
  if (notifyData) NPN_MemFree(notifyData);
}

NPError SqueakPlugin::NewStream(NPMIMEType type, NPStream* stream, 
                            NPBool seekable, uint16* stype)
{
  int id = (stream->notifyData ? ((SqueakStream*) stream->notifyData)->id : NON_REQUESTED_ID);
  DPRINT("NP: NewStream(%s, id=%i)\n", stream->url, id);
  * stype = NP_ASFILEONLY;
  return nsPluginInstanceBase::NewStream(type, stream, seekable, stype);
}

/* The browser notifies that the file have already downloaded */

void SqueakPlugin::StreamAsFile(NPStream* stream, const char* fname)
{
  int id = stream->notifyData ? ((SqueakStream*) stream->notifyData)->id : NON_REQUESTED_ID;
  DPRINT("NP: StreamAsFile(%s, id=%i)\n", stream->url, id);
  DPRINT("NP:   fname=%s\n", fname ? fname : "<NULL>");
  if (id != NON_REQUESTED_ID) {
    this->DeliverFile(id, fname);
  }
}

/*
 * Run Squeak VM
 */
void SqueakPlugin::Run(void)
{
  if (!this->BuildCommandLine(createDataStruct.argc, createDataStruct.argn, createDataStruct.argv, mhWnd))
    return;

  STARTUPINFO startupInfo;
  SECURITY_ATTRIBUTES securityAttr;
  GetStartupInfo(&startupInfo);
  securityAttr.nLength = sizeof(SECURITY_ATTRIBUTES);
  securityAttr.lpSecurityDescriptor = NULL;
  securityAttr.bInheritHandle = TRUE;

  if(CreatePipe(&pipes[PLUGIN_READ], &pipes[SQUEAK_WRITE], &securityAttr, 4096) == 0)
    handleError(NULL);

  if (CreateProcess(NULL, vmCommandLine, NULL, NULL, TRUE, 0, NULL, NULL, &startupInfo, &vmProc) == 0)
    handleError(NULL);
  DPRINT("NP: CreateProcess(%s)\n", vmCommandLine);

  if (WaitForInputIdle(vmProc.hProcess, INFINITE) != 0)
    handleError(NULL);

  if (PostThreadMessage(vmProc.dwThreadId,
    g_WM_BROWSER_PIPE,
    (WPARAM) GetCurrentProcess(),
    (LPARAM) pipes[SQUEAK_WRITE]) == 0)
      handleError(NULL);
}

/* handles g_WM_CLIENT_PIPE to get pipes
 * WPARAM wParam -- [in] result of GetCurrentProcess() in vm (unused)
 * LPARAM lParam -- [in] a handle for PLUGIN_WRITE (hClientWriteEnd in vm)
 */
void SqueakPlugin::GetClientPipe(WPARAM wParam, LPARAM lParam)
{
  if(pipes[PLUGIN_WRITE]) CloseHandle(pipes[PLUGIN_WRITE]);
  if (DuplicateHandle(vmProc.hProcess,
    (HANDLE) lParam,
    GetCurrentProcess(),
    &pipes[PLUGIN_WRITE], 
    0,
    false,
    DUPLICATE_SAME_ACCESS) == 0)
      handleError(NULL);
  DPRINT("NP: Created pipes (VM read: %d <- %d, NP read: %d <- %d)\n", 
    pipes[SQUEAK_READ],
    pipes[PLUGIN_WRITE],
    pipes[PLUGIN_READ],
    pipes[SQUEAK_WRITE]);
}

/*
 * handles g_WM_REQUEST_DATA to GET URL to the browser.
 * int id -- [in] request id
 */

void SqueakPlugin::GetUrl(int id)
{
  char * url = this->Receive(NULL);
  char * target = this->Receive(NULL);

  SqueakStream* notifyData= (SqueakStream*) NPN_MemAlloc(sizeof(SqueakStream));
  notifyData->id= id;
  NPN_GetURLNotify(this->mInstance, url, target, notifyData);
  DPRINT("NP: NPN_GetURLNotify(%s, id=%i)\n", url, id);

  /* the notify does not return if target is specified */
  if (target) this->DeliverFile(id, "");

  if (url) NPN_MemFree(url);
  if (target) NPN_MemFree(target);
}

/*
 * handles g_WM_POST_DATA to POST URL to the browser.
 * int id -- [in] request id
 */
void SqueakPlugin::PostUrl(int id)
{
  DWORD dataSize;
  char * url = this->Receive(NULL);
  char * target = this->Receive(NULL);
  char * data = this->Receive(&dataSize);

  SqueakStream* notifyData= (SqueakStream*) NPN_MemAlloc(sizeof(SqueakStream));
  DPRINT("NP: NPN_PostURLNotify(%s, id=%i)\n", url, id);
  notifyData->id= id;
  NPN_PostURLNotify(this->mInstance, url, target, dataSize, data, FALSE, notifyData);

  /* the notify does not return if target is specified */
  if (target) this->DeliverFile(id, "");

  if (url) NPN_MemFree(url);
  if (target) NPN_MemFree(target);
  if (data) NPN_MemFree(data);
}

/* Read data from VM. It allocates a memory for pbuffer.
 * DWORD *psize  -- [out] size of the result (except last '\0'), or NULL
 * result char * --       result data with '\0'
 */
char * SqueakPlugin::Receive(DWORD * psize)
{
  DWORD nBytes = 0;
  char * buffer = NULL;
  DWORD bufferSize = 0;
  int result = 0;

  result = ReadFile(pipes[PLUGIN_READ], &bufferSize, 4, &nBytes, NULL);
  if (!result || nBytes != 4)
    handleError(NULL);
  if (bufferSize > 0)
  {
    buffer = (char*) NPN_MemAlloc(sizeof(char) * (bufferSize + 1));
    result = ReadFile(pipes[PLUGIN_READ], buffer, bufferSize, &nBytes, NULL);
    if (!result || nBytes != bufferSize)
      handleError(NULL);
    buffer[bufferSize] = '\0';
  }
  if (psize != NULL) * psize = bufferSize;
  return buffer;
}

/* Send data to VM.
 * char * buffer -- [in] data to send
 */
void SqueakPlugin::Send(const char * buffer)
{
  size_t length = strlen(buffer);
  DWORD nBytes;
  BOOL result = WriteFile(pipes[PLUGIN_WRITE], &length, 4, &nBytes, NULL);
  if (!result || nBytes != 4)
      handleError(NULL);
  if(length > 0) {
    result = WriteFile(pipes[PLUGIN_WRITE], buffer, length, &nBytes, NULL);
    if (!result || nBytes != length)
        handleError(NULL);
  }
}

/* Send g_WM_RECEIVE_DATA to vm with received data
 * int id       -- [in] request id
 * char * fname -- [in] temporary file name
 */
void SqueakPlugin::DeliverFile( int id, const char *fname)
{
  if (!pipes[PLUGIN_WRITE]) return;

  if (PostThreadMessage(vmProc.dwThreadId, g_WM_RECEIVE_DATA, (WPARAM) id, (LPARAM) 1) == 0)
    handleError(NULL);
  DPRINT("NP: g_WM_RECEIVE_DATA(fname=%s, id=%i)\n", fname, id);

  Send(fname);
}

/****************************************************************************/
/* Helper functions                                                         */
/****************************************************************************/

/*
 * Get Squeak plugin's directory name form registory HKCU\Software\Squeak
 * char * dirName -- result directory name
 * answers true if it successes
 */
bool SqueakPlugin::GetSqueakDirName(char * dirName, DWORD dirNameSize)
{
  HKEY hKey;
  DWORD type = 0;
  LONG result = 0;
  result = RegOpenKeyEx(HKEY_CURRENT_USER, VM_REGISTRY_KEY, 0, KEY_QUERY_VALUE, &hKey);
  if (result != ERROR_SUCCESS) {
    handleError("Squeak VM registry key is not found.");
    return false;
  }
  result = RegQueryValueEx(hKey, VM_REGISTRY_NAME, 0, &type, (LPBYTE) dirName, &dirNameSize);
  RegCloseKey(hKey);
  if (type != REG_SZ) {
    handleError("Registry query error.");
    return false;
  }
  if (result != ERROR_SUCCESS) {
    handleError("Squeak VM registry value is not found.");
    return false;
  }
  return true;
}

/* Build a commend line string for running VM
 * int16 argc   -- a number of argument
 * char ** argn -- an array of name
 * char ** argv -- an array of value
 * HWND hWnd    -- a window handle of the plugin
 * anser bool   -- true if successed
 */
bool SqueakPlugin::BuildCommandLine(int16 argc, char** argn, char** argv, HWND hWnd)
{
  int cmdargc = 0;
  char ** cmdargv;
  int n = 0;
  char * vmName = "Squeak.exe";
  char * imageName = "SqueakPlugin.image";
  char * srcName = "\"\"";
  char vmDir[MAX_PATH];
  char vmFullPath[MAX_PATH];
  char imageFullPath[MAX_PATH];

  for (int i= 0; i < argc; i++) {
    if (stricmp(argn[i], "src") == 0) srcName = argv[i];
    if (stricmp(argn[i], "vmname") == 0) vmName = argv[i];
    if (stricmp(argn[i], "imagename") == 0) imageName = argv[i];
  }
  char * result;
  if ((result = strstr(vmName, "..")) != NULL)
  {
    handleError("Malformed vmname.");
    return false;
  }

  GetSqueakDirName(vmDir, MAX_PATH);
  _snprintf(vmFullPath, MAX_PATH, "\"%s\\%s\"", vmDir, vmName);
  _snprintf(imageFullPath, MAX_PATH, "\"%s\\%s\"", vmDir, imageName);

  char browserWindow[100];
  itoa((int) hWnd, browserWindow, 10);

  cmdargc = 5 + (argc * 2) + 1;
  cmdargv = (char**) NPN_MemAlloc(sizeof(char*) * cmdargc);
  cmdargv[0] = vmFullPath;
  cmdargv[1] = "-browserWindow:";
  cmdargv[2] = browserWindow;
  cmdargv[3] = imageFullPath;
  cmdargv[4] = srcName;
  n = 5;
  for (int i= 0; i < argc; i++) {
    cmdargv[n]= argn[i];
    cmdargv[n + 1]= argv[i] ? argv[i] : (char *) "";
    n = n + 2;
  }
  cmdargv[n] = NULL;

  CatStrings (vmCommandLine, cmdargv, sizeof(vmCommandLine));

  NPN_MemFree(cmdargv);
  return true;
}

/* a helper method to concatenate an array of string.
 * destString   -- [out] result string
 * srcString    -- [in]  an array of source string
 * maxSize      -- [in]  max size of result string
 * result BOOL  --       true if successed
 */
bool SqueakPlugin::CatStrings (char * destString, char * srcStrings[], size_t maxSize)
{
  size_t pos = 0;
  while (* srcStrings != NULL) {
    size_t length = strlen(* srcStrings);
    if (pos + length > maxSize)
      return false;
    strncpy(destString + pos, * srcStrings, length);
    * (destString + pos + length) = ' ';
    pos = pos + length + 1;
    srcStrings ++;
  }
  return true;
}

/****************************************************************************/
/* Window procedure                                                         */
/****************************************************************************/

static LRESULT CALLBACK PluginWinProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
  SqueakPlugin *plugin = (SqueakPlugin *)GetWindowLong(hWnd, GWL_USERDATA);

  if (msg == g_WM_CLIENT_PIPE)
  {
    plugin->GetClientPipe(wParam, lParam);
  }
  else if (msg == g_WM_REQUEST_DATA)
  {
    plugin->GetUrl((int) wParam);
  }
  else if (msg == g_WM_POST_DATA)
  {
    plugin->PostUrl((int) wParam);
  }
  else if (msg == WM_PAINT)
  {
    plugin->SetWindow(NULL);
  }
    return DefWindowProc(hWnd, msg, wParam, lParam);
}

/****************************************************************************/
/* Error handling / Debugging support                                       */
/****************************************************************************/

/* Store error message.
 * char * message -- [in] error message, or NULL if GetLastError() is needed.
 */
void SqueakPlugin::handleError(char *message)
{
  if (isError) return;
  isError = true;
  if (message == NULL) {
    DWORD errorNumber = GetLastError();
    FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM, NULL, errorNumber,  0, lastError, sizeof(lastError), NULL);
  } else {
    strncpy(lastError, message, sizeof(lastError));
  }
}

/*
 * printf style error message.
 */
static void DPRINT(char *format, ...)
{
#ifdef DEBUG
  char buffer[1024];
  va_list ap;
  va_start(ap, format);
  _vsnprintf(buffer, 1024, format, ap);
  va_end(ap);
  OutputDebugString(buffer);
#endif
}

