/* -*- Mode: C++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 8 -*- */
/* ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is Mozilla Firefox.
 *
 * The Initial Developer of the Original Code is
 * the Mozilla Foundation <http://www.mozilla.org>.
 * Portions created by the Initial Developer are Copyright (C) 2010
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 *
 * ***** END LICENSE BLOCK ***** */

#include "base/basictypes.h"
#include "jscntxt.h"
#include "jswrapper.h"
#include "nsXULAppAPI.h"
#include "nsNativeCharsetUtils.h"

#include "mozilla/jetpack/JetpackChild.h"
#include "mozilla/jetpack/Handle.h"
#include "mozilla/IntentionalCrash.h"

#include <stdio.h>

namespace mozilla {
namespace jetpack {

JetpackChild::JetpackChild()
{
}

JetpackChild::~JetpackChild()
{
}

#define IMPL_METHOD_FLAGS (JSPROP_ENUMERATE | \
                           JSPROP_READONLY | \
                           JSPROP_PERMANENT)
const JSFunctionSpec
JetpackChild::sImplMethods[] = {
  JS_FN("sendMessage", SendMessage, 3, IMPL_METHOD_FLAGS),
  JS_FN("callMessage", CallMessage, 2, IMPL_METHOD_FLAGS),
  JS_FN("registerReceiver", RegisterReceiver, 2, IMPL_METHOD_FLAGS),
  JS_FN("unregisterReceiver", UnregisterReceiver, 2, IMPL_METHOD_FLAGS),
  JS_FN("unregisterReceivers", UnregisterReceivers, 1, IMPL_METHOD_FLAGS),
  JS_FN("createHandle", CreateHandle, 0, IMPL_METHOD_FLAGS),
  JS_FN("createSandbox", CreateSandbox, 0, IMPL_METHOD_FLAGS),
  JS_FN("evalInSandbox", EvalInSandbox, 2, IMPL_METHOD_FLAGS),
  JS_FN("gc", GC, 0, IMPL_METHOD_FLAGS),
#ifdef JS_GC_ZEAL
  JS_FN("gczeal", GCZeal, 1, IMPL_METHOD_FLAGS),
#endif
  JS_FN("_noteIntentionalCrash", NoteIntentionalCrash, 0,
        IMPL_METHOD_FLAGS),
  JS_FS_END
};

#undef IMPL_METHOD_FLAGS

const JSClass
JetpackChild::sGlobalClass = {
  "JetpackChild::sGlobalClass", JSCLASS_GLOBAL_FLAGS,
  JS_PropertyStub,  JS_PropertyStub, JS_PropertyStub, JS_StrictPropertyStub,
  JS_EnumerateStub, JS_ResolveStub,  JS_ConvertStub,  JS_FinalizeStub,
  JSCLASS_NO_OPTIONAL_MEMBERS
};

#ifdef BUILD_CTYPES
static char*
UnicodeToNative(JSContext *cx, const jschar *source, size_t slen)
{
  nsCAutoString native;
  nsDependentString unicode(source, slen);
  nsresult rv = NS_CopyUnicodeToNative(unicode, native);
  if (NS_FAILED(rv)) {
    JS_ReportError(cx, "could not convert string to native charset");
    return NULL;
  }

  char* result = static_cast<char*>(JS_malloc(cx, native.Length() + 1));
  if (!result)
    return NULL;

  memcpy(result, native.get(), native.Length() + 1);
  return result;
}

static JSCTypesCallbacks sCallbacks = {
  UnicodeToNative
};
#endif

bool
JetpackChild::Init(base::ProcessHandle aParentProcessHandle,
                   MessageLoop* aIOLoop,
                   IPC::Channel* aChannel)
{
  if (!Open(aChannel, aParentProcessHandle, aIOLoop))
    return false;

  if (!(mRuntime = JS_NewRuntime(32L * 1024L * 1024L)) ||
      !(mCx = JS_NewContext(mRuntime, 8192)))
    return false;

  JS_SetVersion(mCx, JSVERSION_LATEST);
  JS_SetOptions(mCx, JS_GetOptions(mCx) |
                JSOPTION_DONT_REPORT_UNCAUGHT |
                JSOPTION_ATLINE |
                JSOPTION_JIT);
  JS_SetErrorReporter(mCx, ReportError);

  {
    JSAutoRequest request(mCx);
    JS_SetContextPrivate(mCx, this);
    JSObject* implGlobal =
      JS_NewCompartmentAndGlobalObject(mCx, const_cast<JSClass*>(&sGlobalClass), NULL);
    if (!implGlobal)
        return false;

    JSAutoEnterCompartment ac;
    if (!ac.enter(mCx, implGlobal))
        return false;

    jsval ctypes;
    if (!JS_InitStandardClasses(mCx, implGlobal) ||
#ifdef BUILD_CTYPES
        !JS_InitCTypesClass(mCx, implGlobal) ||
        !JS_GetProperty(mCx, implGlobal, "ctypes", &ctypes) ||
        !JS_SetCTypesCallbacks(mCx, JSVAL_TO_OBJECT(ctypes), &sCallbacks) ||
#endif
        !JS_DefineFunctions(mCx, implGlobal,
                            const_cast<JSFunctionSpec*>(sImplMethods)))
      return false;
  }

  return true;
}

void
JetpackChild::CleanUp()
{
  ClearReceivers();
  JS_DestroyContext(mCx);
  JS_DestroyRuntime(mRuntime);
  JS_ShutDown();
}

void
JetpackChild::ActorDestroy(ActorDestroyReason why)
{
  XRE_ShutdownChildProcess();
}

bool
JetpackChild::RecvSendMessage(const nsString& messageName,
                              const InfallibleTArray<Variant>& data)
{
  JSAutoRequest request(mCx);

  JSObject *global = JS_GetGlobalObject(mCx);
  JSAutoEnterCompartment ac;
  if (!ac.enter(mCx, global))
    return false;

  return JetpackActorCommon::RecvMessage(mCx, messageName, data, NULL);
}

bool
JetpackChild::RecvEvalScript(const nsString& code)
{
  JSAutoRequest request(mCx);

  JSObject *global = JS_GetGlobalObject(mCx);
  JSAutoEnterCompartment ac;
  if (!ac.enter(mCx, global))
    return false;

  jsval ignored;
  (void) JS_EvaluateUCScript(mCx, global, code.get(),
                             code.Length(), "", 1, &ignored);
  return true;
}

PHandleChild*
JetpackChild::AllocPHandle()
{
  return new HandleChild();
}

bool
JetpackChild::DeallocPHandle(PHandleChild* actor)
{
  delete actor;
  return true;
}

JetpackChild*
JetpackChild::GetThis(JSContext* cx)
{
  JetpackChild* self =
    static_cast<JetpackChild*>(JS_GetContextPrivate(cx));
  JS_ASSERT(cx == self->mCx);
  return self;
}

struct MessageResult {
  nsString msgName;
  InfallibleTArray<Variant> data;
};

static JSBool
MessageCommon(JSContext* cx, uintN argc, jsval* vp,
              MessageResult* result)
{
  if (argc < 1) {
    JS_ReportError(cx, "Message requires a name, at least");
    return JS_FALSE;
  }

  jsval* argv = JS_ARGV(cx, vp);

  JSString* msgNameStr = JS_ValueToString(cx, argv[0]);
  if (!msgNameStr) {
    JS_ReportError(cx, "Could not convert value to string");
    return JS_FALSE;
  }

  size_t length;
  const jschar* chars = JS_GetStringCharsAndLength(cx, msgNameStr, &length);
  if (!chars)
      return JS_FALSE;

  result->msgName.Assign(chars, length);

  result->data.Clear();

  if (!result->data.SetCapacity(argc)) {
    JS_ReportOutOfMemory(cx);
    return JS_FALSE;
  }

  for (uintN i = 1; i < argc; ++i) {
    Variant* vp = result->data.AppendElement();
    if (!JetpackActorCommon::jsval_to_Variant(cx, argv[i], vp)) {
      JS_ReportError(cx, "Invalid message argument at position %d", i);
      return JS_FALSE;
    }
  }

  return JS_TRUE;
}

JSBool
JetpackChild::SendMessage(JSContext* cx, uintN argc, jsval* vp)
{
  MessageResult smr;
  if (!MessageCommon(cx, argc, vp, &smr))
    return JS_FALSE;

  if (!GetThis(cx)->SendSendMessage(smr.msgName, smr.data)) {
    JS_ReportError(cx, "Failed to sendMessage");
    return JS_FALSE;
  }

  return JS_TRUE;
}

JSBool
JetpackChild::CallMessage(JSContext* cx, uintN argc, jsval* vp)
{
  MessageResult smr;
  if (!MessageCommon(cx, argc, vp, &smr))
    return JS_FALSE;

  InfallibleTArray<Variant> results;
  if (!GetThis(cx)->CallCallMessage(smr.msgName, smr.data, &results)) {
    JS_ReportError(cx, "Failed to callMessage");
    return JS_FALSE;
  }

  nsAutoTArray<jsval, 4> jsvals;
  jsval* rvals = jsvals.AppendElements(results.Length());
  if (!rvals) {
    JS_ReportOutOfMemory(cx);
    return JS_FALSE;
  }
  for (PRUint32 i = 0; i < results.Length(); ++i)
    rvals[i] = JSVAL_VOID;
  js::AutoArrayRooter root(cx, results.Length(), rvals);

  for (PRUint32 i = 0; i < results.Length(); ++i)
    if (!jsval_from_Variant(cx, results.ElementAt(i), rvals + i)) {
      JS_ReportError(cx, "Invalid result from handler %d", i);
      return JS_FALSE;
    }

  JSObject* arrObj = JS_NewArrayObject(cx, results.Length(), rvals);
  if (!arrObj) {
    JS_ReportOutOfMemory(cx);
    return JS_FALSE;
  }
  JS_SET_RVAL(cx, vp, OBJECT_TO_JSVAL(arrObj));

  return JS_TRUE;
}

struct ReceiverResult
{
  nsString msgName;
  jsval receiver;
};

static JSBool
ReceiverCommon(JSContext* cx, uintN argc, jsval* vp,
               const char* methodName, uintN arity,
               ReceiverResult* result)
{
  if (argc != arity) {
    JS_ReportError(cx, "%s requires exactly %d arguments", methodName, arity);
    return JS_FALSE;
  }

  // Not currently possible, but think of the future.
  if (arity < 1)
    return JS_TRUE;

  jsval* argv = JS_ARGV(cx, vp);

  JSString* str = JS_ValueToString(cx, argv[0]);
  if (!str) {
    JS_ReportError(cx, "%s expects a stringifiable value as its first argument",
                   methodName);
    return JS_FALSE;
  }

  size_t length;
  const jschar* chars = JS_GetStringCharsAndLength(cx, str, &length);
  if (!chars)
      return JS_FALSE;

  result->msgName.Assign(chars, length);

  if (arity < 2)
    return JS_TRUE;

  if (JS_TypeOfValue(cx, argv[1]) != JSTYPE_FUNCTION) {
    JS_ReportError(cx, "%s expects a function as its second argument",
                   methodName);
    return JS_FALSE;
  }

  // GC-safe because argv is rooted.
  result->receiver = argv[1];

  return JS_TRUE;
}

JSBool
JetpackChild::RegisterReceiver(JSContext* cx, uintN argc, jsval* vp)
{
  ReceiverResult rr;
  if (!ReceiverCommon(cx, argc, vp, "registerReceiver", 2, &rr))
    return JS_FALSE;

  JetpackActorCommon* actor = GetThis(cx);
  nsresult rv = actor->RegisterReceiver(cx, rr.msgName, rr.receiver);
  if (NS_FAILED(rv)) {
    JS_ReportOutOfMemory(cx);
    return JS_FALSE;
  }

  return JS_TRUE;
}

JSBool
JetpackChild::UnregisterReceiver(JSContext* cx, uintN argc, jsval* vp)
{
  ReceiverResult rr;
  if (!ReceiverCommon(cx, argc, vp, "unregisterReceiver", 2, &rr))
    return JS_FALSE;

  JetpackActorCommon* actor = GetThis(cx);
  actor->UnregisterReceiver(rr.msgName, rr.receiver);
  return JS_TRUE;
}

JSBool
JetpackChild::UnregisterReceivers(JSContext* cx, uintN argc, jsval* vp)
{
  ReceiverResult rr;
  if (!ReceiverCommon(cx, argc, vp, "unregisterReceivers", 1, &rr))
    return JS_FALSE;

  JetpackActorCommon* actor = GetThis(cx);
  actor->UnregisterReceivers(rr.msgName);
  return JS_TRUE;
}

JSBool
JetpackChild::CreateHandle(JSContext* cx, uintN argc, jsval* vp)
{
  if (argc > 0) {
    JS_ReportError(cx, "createHandle takes zero arguments");
    return JS_FALSE;
  }

  HandleChild* handle;
  JSObject* hobj;

  PHandleChild* phc = GetThis(cx)->SendPHandleConstructor();
  if (!(handle = static_cast<HandleChild*>(phc)) ||
      !(hobj = handle->ToJSObject(cx))) {
    JS_ReportError(cx, "Failed to construct Handle");
    return JS_FALSE;
  }

  JS_SET_RVAL(cx, vp, OBJECT_TO_JSVAL(hobj));

  return JS_TRUE;
}

JSBool
JetpackChild::CreateSandbox(JSContext* cx, uintN argc, jsval* vp)
{
  if (argc > 0) {
    JS_ReportError(cx, "createSandbox takes zero arguments");
    return JS_FALSE;
  }

  JSObject* obj = JS_NewCompartmentAndGlobalObject(cx, const_cast<JSClass*>(&sGlobalClass), NULL);
  if (!obj)
    return JS_FALSE;

  jsval rval = OBJECT_TO_JSVAL(obj);
  if (!JS_WrapValue(cx, &rval))
    return JS_FALSE;

  JSAutoEnterCompartment ac;
  if (!ac.enter(cx, obj))
    return JS_FALSE;

  JS_SET_RVAL(cx, vp, rval);
  return JS_InitStandardClasses(cx, obj);
}

JSBool
JetpackChild::EvalInSandbox(JSContext* cx, uintN argc, jsval* vp)
{
  if (argc != 2) {
    JS_ReportError(cx, "evalInSandbox takes two arguments");
    return JS_FALSE;
  }

  jsval* argv = JS_ARGV(cx, vp);

  JSObject* obj;
  if (!JSVAL_IS_OBJECT(argv[0]) ||
      !(obj = JSVAL_TO_OBJECT(argv[0]))) {
    JS_ReportError(cx, "The first argument to evalInSandbox must be a global object created using createSandbox.");
    JS_ASSERT(JS_FALSE);
    return JS_FALSE;
  }

  // Unwrap, and switch compartments
  obj = js::UnwrapObject(obj);

  JSAutoEnterCompartment ac;
  if (!ac.enter(cx, obj))
    return JS_FALSE;

  if (&sGlobalClass != JS_GetClass(cx, obj) ||
      obj == JS_GetGlobalObject(cx)) {
    JS_ReportError(cx, "The first argument to evalInSandbox must be a global object created using createSandbox.");
    JS_ASSERT(JS_FALSE);
    return JS_FALSE;
  }

  if (!JS_WrapValue(cx, &argv[1]))
    return JS_FALSE;

  JSString* str = JS_ValueToString(cx, argv[1]);
  if (!str)
    return JS_FALSE;

  size_t length;
  const jschar* chars = JS_GetStringCharsAndLength(cx, str, &length);
  if (!chars)
      return JS_FALSE;

  js::AutoValueRooter ignored(cx);
  return JS_EvaluateUCScript(cx, obj, chars, length, "", 1, ignored.jsval_addr());
}

bool JetpackChild::sReportingError;

/* static */ void
JetpackChild::ReportError(JSContext* cx, const char* message,
                          JSErrorReport* report)
{
  if (sReportingError) {
    NS_WARNING("Recursive error reported.");
    return;
  }

  sReportingError = true;

  js::AutoObjectRooter obj(cx, JS_NewObject(cx, NULL, NULL, NULL));

  if (report && report->filename) {
    jsval filename = STRING_TO_JSVAL(JS_NewStringCopyZ(cx, report->filename));
    JS_SetProperty(cx, obj.object(), "fileName", &filename);
  }

  if (report) {
    jsval lineno = INT_TO_JSVAL(report->lineno);
    JS_SetProperty(cx, obj.object(), "lineNumber", &lineno);
  }

  jsval msgstr = JSVAL_NULL;
  if (report && report->ucmessage)
    msgstr = STRING_TO_JSVAL(JS_NewUCStringCopyZ(cx, report->ucmessage));
  else
    msgstr = STRING_TO_JSVAL(JS_NewStringCopyZ(cx, message));
  JS_SetProperty(cx, obj.object(), "message", &msgstr);

  MessageResult smr;
  Variant* vp = smr.data.AppendElement();
  JetpackActorCommon::jsval_to_Variant(cx, OBJECT_TO_JSVAL(obj.object()), vp);
  GetThis(cx)->SendSendMessage(NS_LITERAL_STRING("core:exception"), smr.data);

  sReportingError = false;
}

JSBool
JetpackChild::GC(JSContext* cx, uintN argc, jsval *vp)
{
  JS_GC(cx);
  return JS_TRUE;
}

#ifdef JS_GC_ZEAL
JSBool
JetpackChild::GCZeal(JSContext* cx, uintN argc, jsval *vp)
{
  jsval* argv = JS_ARGV(cx, vp);

  uint32 zeal;
  if (!JS_ValueToECMAUint32(cx, argv[0], &zeal))
    return JS_FALSE;

  JS_SetGCZeal(cx, PRUint8(zeal), JS_DEFAULT_ZEAL_FREQ, JS_FALSE);
  return JS_TRUE;
}
#endif

JSBool
JetpackChild::NoteIntentionalCrash(JSContext* cx, uintN argc, jsval *vp)
{
  mozilla::NoteIntentionalCrash("jetpack");
  return JS_TRUE;
}

} // namespace jetpack
} // namespace mozilla
