#include "EXTERN.h"
#include "perl.h"
#include "XSUB.h"

#include "ppport.h"

#define XP_UNIX
#define INCLUDES_IN_MOZJS
#define JS_THREADSAFE

#include <jsapi.h>

jsval build_value(JSContext* , SV*);

/* Global class, does nothing */
static JSClass global_class = {
    "global", 0,
    JS_PropertyStub,  JS_PropertyStub,  JS_PropertyStub,  JS_PropertyStub,
    JS_EnumerateStub, JS_ResolveStub,   JS_ConvertStub,   JS_FinalizeStub,
    JSCLASS_NO_OPTIONAL_MEMBERS
};

typedef struct xsjs_err {
  char* file;
  char* message;
  int32 line;
} xsjs_err;

typedef struct xsjs_appdata {
  SV*               branch_callback;
  unsigned long     branch_interval;
  unsigned long     branch_counter;
} xsjs_appdata;

const xsjs_err unknown_err = { "(null)", "(null)", 0 };

typedef struct xsjs_rv {
  JSBool succ;
  jsval  rv;
} xsjs_rv;

xsjs_err get_error(JSContext* cx) {
  xsjs_err rv = unknown_err;
  jsval val;
  JSObject* eobj;
  JSString* sobj;

  if(JS_IsExceptionPending(cx) == JS_FALSE) {
    rv.message = "(no exception)";
    return rv;
  }
  
  JS_GetPendingException(cx, &val);

  if(JSVAL_IS_OBJECT(val)) {
    eobj = JSVAL_TO_OBJECT(val);
    if(JS_GetProperty(cx, eobj, "message", &val) == JS_TRUE) {
      if((sobj = JS_ValueToString(cx, val))) {
        rv.message = JS_GetStringBytes(sobj);
      } else {
        rv.message = "(failed to retrieve exception)";
      }
    } else {
      rv.message = "(unknown exception)";
    }

    if(JS_GetProperty(cx, eobj, "fileName", &val) == JS_TRUE) {
      if((sobj = JS_ValueToString(cx, val))) {
        rv.file = JS_GetStringBytes(sobj);
      } else {
        rv.file = "(failed to retrieve filename)";
      }
    } else {
      rv.file = "(unknown)";
    }

    if(JS_GetProperty(cx, eobj, "lineNumber", &val) == JS_TRUE) {
      if(JS_ValueToInt32(cx, val, &(rv.line)) == JS_FALSE) rv.line = 0;
    }
  } else {
    rv.message = "(invalid exception)";
  }

  return rv;
}

xsjs_rv run_eval(JSContext* cx, const char* str, const char* fname) {
  JSObject* g = JS_GetGlobalObject(cx);
  JSScript* scr = JS_CompileScript(cx, g, str, strlen(str), fname, 1);
  xsjs_rv rv;

  if(scr) {
    rv.succ = JS_ExecuteScript(cx, g, scr, &(rv.rv));
  } else {
    rv.succ = JS_FALSE;
    rv.rv = JSVAL_VOID;
  }

  return rv;
}

char* get_key_name(HE* hashent) {
  SV* keysv;
  char* keyname;
  keysv = HeSVKEY(hashent);
    
  if (keysv) {
    keyname = SvPVbyte(keysv, SvLEN( keysv ) );
  } else {
    keyname = HeKEY(hashent);
  }
    
  return keyname;
}

jsval build_string(JSContext* cx, SV* v) {
  jsdouble js_nv;
  jsval js_v;
  STRLEN len;
  char* c_sv;
  JSBool succ;
  JSString *js_s;

  if(SvOK(v)) {
    if(SvIOK(v) || SvNOK(v)) {
      js_nv = SvNV(v);
      succ = JS_NewDoubleValue(cx, js_nv, &js_v);
      if(succ == JS_FALSE) {
        croak("Failed to create a new number value!");
        return JSVAL_VOID;
      }
    } else {
      c_sv = SvPV(v, len);
      if((js_s = JS_NewStringCopyN(cx, c_sv, len))) {
        js_v = STRING_TO_JSVAL(js_s);
      } else {
        croak("Failed to create a new string value!");
        return JSVAL_VOID;
      }
    }
  } else {
    js_v = JSVAL_NULL;
  }

  return js_v;
}

jsval
build_hash(cx,hv)
  JSContext *cx;
  HV *hv;
{
  HE* i;
  char* k;
  SV* v;
  jsval jval;
  jsval rv;
  JSBool succ;

  JSObject* rvo = JS_NewObject(cx, NULL, NULL, NULL);

  if(!rvo) {
    croak("Failed to instantiate a JavaScript object!");
    return JSVAL_VOID;
  }

  hv_iterinit(hv);

  while((i = hv_iternext(hv)) != NULL) {
    k = get_key_name(i);
    v = (SV *)hv_iterval(hv, i);
    jval = build_value(cx, v);
    succ = JS_SetProperty(cx, rvo, k, &jval);
    if(succ == JS_FALSE) {
      croak("Failed to assign property '%s'!", k);
      return JSVAL_VOID;
    }
  }

  rv = OBJECT_TO_JSVAL(rvo);
  return rv;
}

jsval
build_array(cx,av)
  JSContext *cx;
  AV *av;
{
  JSObject* rvo = JS_NewArrayObject(cx, 0, NULL);
  jsval rv;
  SV** entry;
  jsval jsentry;
  int i;
  JSBool succ;

  for(i=0;i<=av_len(av);i++) {
    entry = av_fetch(av, i, 0);
    if(*entry) {
      jsentry = build_value(cx, *entry);
      succ = JS_SetElement(cx, rvo, i, &jsentry);
      if(succ == JS_FALSE) {
        croak("Failed to assign array entry #%d!", i);
        return JSVAL_VOID;
      }
    } else {
      croak("Entry #%d of array not found!", i);
      return JSVAL_VOID;
    }
  }

  rv = OBJECT_TO_JSVAL(rvo);
  return rv;  
}

jsval
build_value(cx,v)
  JSContext *cx;
  SV *v;
{
  jsval jval;

  if(SvOK(v)) {
    if(SvROK(v)) {
      switch(SvTYPE(SvRV(v))) {
        case SVt_PVAV:
          jval = build_array(cx, (AV*)(SvRV(v)));
          break;
        case SVt_PVHV:
          jval = build_hash(cx, (HV*)(SvRV(v)));
          break;
        default:
          croak("Cannot dereference %p!", v);
          jval = JSVAL_VOID;
      }
    } else {
      jval = build_string(cx, v);
    }
  } else {
    jval = JSVAL_NULL;
  }

  return jval;  
}

JSBool run_branch_callback(cx, script)
  JSContext *cx;
  JSScript *script;
{
  xsjs_appdata* appdata = JS_GetRuntimePrivate(JS_GetRuntime(cx));

  appdata->branch_counter ++;

  if(appdata->branch_counter >= appdata->branch_interval) {
    SV* rv;
    bool rv_b;
    int count;

    appdata->branch_counter = 0;

    dSP;

    ENTER;
    SAVETMPS;

    PUSHMARK(SP);

    count = call_sv(appdata->branch_callback, G_SCALAR | G_EVAL);

    SPAGAIN;

    if(SvTRUE(ERRSV)) {
      STRLEN n_a;
      JS_ReportError(cx, "Branch Callback failed: %s", SvPV(ERRSV, n_a));
      rv_b = 0;
    } else {
      if(count == 0) {
        rv_b = 0;
      } else if(count != 1) {
        croak("Bad arguments!");
      } else {
        rv = POPs;
        rv_b = SvTRUE(rv);
        if(!rv_b) {
          JS_ReportError(cx, "Branch Callback aborted script");
        }
      }
    }

    PUTBACK;
    FREETMPS;
    LEAVE;

    if(rv_b) {
      return JS_TRUE;
    } else {
      return JS_FALSE;
    }
  } else {
    return JS_TRUE;
  }
}

void
set_branch_callback(cx,cb,interval)
  JSContext *cx;
  unsigned long interval;
  SV *cb;
{
  xsjs_appdata *appdata = JS_GetRuntimePrivate(JS_GetRuntime(cx));

  if(appdata->branch_callback) {
    croak("branch callback has already been set");
  }

  SvREFCNT_inc(cb);
  appdata->branch_callback = cb;
  appdata->branch_interval = interval;
  appdata->branch_counter = 0;

  JS_SetBranchCallback(cx, run_branch_callback);
}

void
clear_branch_callback(cx)
  JSContext *cx;
{
  xsjs_appdata* appdata = JS_GetRuntimePrivate(JS_GetRuntime(cx));

  if(appdata->branch_callback) {
    SvREFCNT_dec(appdata->branch_callback);
    appdata->branch_callback = NULL;
    appdata->branch_interval = 0;
    appdata->branch_counter = 0;
    JS_SetBranchCallback(cx, NULL);
  }
}

void
assign_property(cx,obj,k,v)
  JSContext *cx;
  JSObject *obj;
  const char *k;
  SV *v;
{
  JSBool succ;
  jsval jval = build_value(cx, v);
  succ = JS_SetProperty(cx, obj, k, &jval);
  if(succ == JS_FALSE) {
    croak("Failed to assign property '%s'!", k);
  }
}

MODULE = JavaScript::Lite       PACKAGE = JavaScript::Lite

void
assign(cx,k,v)
    JSContext *cx;
    const char *k;
    SV *v;
  PREINIT:
    JSObject* jobj;
  CODE:
    if((jobj = JS_GetGlobalObject(cx))) {
      assign_property(cx, jobj, k, v);
    } else {
      croak("Failed to get JavaScript global object!");
    }

void
assign_property(cx,n,k,v)
    JSContext *cx;
    const char *n;
    const char *k;
    SV *v;
  PREINIT:
    JSObject* gobj;
    JSObject* jobj;
    jsval jobjv;
    JSBool rv;
  CODE:
    if((gobj = JS_GetGlobalObject(cx))) {
      rv = JS_GetProperty(cx, gobj, n, &jobjv);
      if(rv == JS_FALSE)
        croak("Failed to find an object named '%s'!", n);
      if(!JSVAL_IS_OBJECT(jobjv))
        croak("Global property '%s' is not an object!", n);
      jobj = JSVAL_TO_OBJECT(jobjv);
      assign_property(cx,jobj,k,v);
    } else {
      croak("Failed to get JavaScript global object!");
    }

void
clear_error(cx)
    JSContext *cx;
  CODE:
    JS_ClearPendingException(cx);

void
eval_void(cx, str, fname)
    JSContext *cx;
    const char *str;
    const char *fname;
  PREINIT:
    xsjs_rv rv;
    xsjs_err err;
  CODE:
    rv = run_eval(cx, str, fname);
    if(rv.succ == JS_FALSE) {
      err = get_error(cx);
      croak("JavaScript: %s at %s line %d", err.message, err.file, err.line);
    }

SV*
eval_js(cx, str, fname)
    JSContext* cx;
    const char* str;
    const char* fname;
  PREINIT:
    char* rv_str;
    JSString* s_rval;
    xsjs_rv rv;
    xsjs_err err;
  CODE:
    rv = run_eval(cx, str, fname);
    if(rv.succ == JS_FALSE) {
      err = get_error(cx);
      croak("JavaScript: %s at %s line %d", err.message, err.file, err.line);
    }
    if(JSVAL_IS_NULL(rv.rv) || JSVAL_IS_VOID(rv.rv)) {
      XSRETURN_UNDEF;
    }
    if((s_rval = JS_ValueToString(cx, rv.rv))) {
      if((rv_str = JS_GetStringBytes(s_rval))) {
        RETVAL = newSVpv(rv_str, 0);
      } else {
        croak("Failed to convert return value of JavaScript function '%s'!", fname);
      }
    } else {
      croak("Failed to obtain return value of JavaScript function '%s'!", fname);
    }
  OUTPUT:
    RETVAL

JSContext *
create(const char* class_name, long maxmem)
  PREINIT:
    JSRuntime* rt;
    JSContext* cx;
    JSObject* gobj;
    xsjs_appdata* appdata;
  CODE:
    rt = JS_NewRuntime(maxmem);
    if(rt == NULL)
      croak("%s: Failed to create JavaScript runtime!", class_name);
    cx = JS_NewContext(rt, 8192);
    if(cx == NULL)
      croak("%s: Failed to create JavaScript context!", class_name);
#ifdef JSOPTION_DONT_REPORT_UNCAUGHT
    JS_SetOptions(cx, JSOPTION_DONT_REPORT_UNCAUGHT);
#endif
    gobj = JS_NewObject(cx, &global_class, NULL, NULL);
    if(!gobj)
      croak("%s: Failed to create the global object", class_name);
    if (JS_InitStandardClasses(cx, gobj) == JS_FALSE)
      croak("%s: Standard classes not loaded properly.", class_name);
    JS_SetGlobalObject(cx, gobj);
    appdata = malloc(sizeof(xsjs_appdata));
    memset(appdata, 0, sizeof(xsjs_appdata));
    JS_SetRuntimePrivate(rt, appdata);

    RETVAL = cx;
  OUTPUT:
    RETVAL

void branch_callback(cx, callback, interval=0)
    JSContext*  cx;
    SV* callback;
    unsigned long interval;
  CODE:
    if(SvTRUE(callback)) {
      set_branch_callback(cx, callback, interval);
    } else {
      clear_branch_callback(cx);
    }

void clear_branch_counter(cx)
    JSContext* cx
  CODE:
    xsjs_appdata* appdata = JS_GetRuntimePrivate(JS_GetRuntime(cx));
    appdata->branch_counter = 0;

char* invoke(cx, name)
    JSContext*  cx;
    const char* name;
  PREINIT:
    jsval       fval;
    jsval       rval;
    JSObject*   global;
    JSBool      rv;  
    JSString*   s_rval;
    xsjs_err    err;
  CODE:
    if((global = JS_GetGlobalObject(cx))) {
      rv = JS_GetProperty(cx, global, name, &fval);
      if(rv == JS_FALSE)
        croak("Failed to find global javascript function '%s'!", name);
      if(JSVAL_IS_OBJECT(fval) && JS_ObjectIsFunction(cx, JSVAL_TO_OBJECT(fval))) {
        rv = JS_CallFunctionValue(cx, global, fval, 0, NULL, &rval);
        if(rv == JS_FALSE) {
          err = get_error(cx);
          croak("JavaScript: %s at %s line %d", err.message, err.file, err.line);
        }
        if(JSVAL_IS_NULL(rval) || JSVAL_IS_VOID(rval))
          XSRETURN_UNDEF;
        if((s_rval = JS_ValueToString(cx, rval))) {
          RETVAL = JS_GetStringBytes(s_rval);
        } else {
          croak("Failed to obtain return value from '%s'!", name);
        }
      } else {
        croak("Failed to convert '%s' into a function!", name);
      }
    } else {
      croak("Failed to find JavaScript global object!");
    }
  OUTPUT:
    RETVAL

void collect(cx)
    JSContext* cx;
  CODE:
    JS_MaybeGC(cx);

void DESTROY(cx)
    JSContext* cx;
  PREINIT:
    JSRuntime* rt;
    xsjs_appdata* appdata;
  CODE:
    rt = JS_GetRuntime(cx);
    clear_branch_callback(cx);
    appdata = (xsjs_appdata*) JS_GetRuntimePrivate(rt);
    JS_SetRuntimePrivate(rt, NULL);
    if(appdata) free(appdata);
    JS_DestroyContext(cx);
    JS_DestroyRuntime(rt);