/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
 * vim: set ts=4 sw=4 et tw=80:
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "JavaScriptParent.h"
#include "mozilla/dom/ContentParent.h"
#include "nsJSUtils.h"
#include "jsfriendapi.h"
#include "jsproxy.h"
#include "HeapAPI.h"
#include "xpcprivate.h"
#include "mozilla/Casting.h"

using namespace js;
using namespace JS;
using namespace mozilla;
using namespace mozilla::jsipc;

JavaScriptParent::JavaScriptParent()
  : refcount_(1),
    inactive_(false)
{
}

static inline JavaScriptParent *
ParentOf(JSObject *obj)
{
    MOZ_ASSERT(JavaScriptParent::IsCPOW(obj));
    return reinterpret_cast<JavaScriptParent *>(GetProxyExtra(obj, 0).toPrivate());
}

ObjectId
JavaScriptParent::idOf(JSObject *obj)
{
    MOZ_ASSERT(JavaScriptParent::IsCPOW(obj));

    Value v = GetProxyExtra(obj, 1);
    MOZ_ASSERT(v.isDouble());

    ObjectId objId = BitwiseCast<uint64_t>(v.toDouble());
    MOZ_ASSERT(findObject(objId) == obj);
    MOZ_ASSERT(objId);

    return objId;
}

int sCPOWProxyHandler;

class CPOWProxyHandler : public BaseProxyHandler
{
  public:
    CPOWProxyHandler()
      : BaseProxyHandler(&sCPOWProxyHandler) {}
    virtual ~CPOWProxyHandler() {}

    virtual bool finalizeInBackground(Value priv) MOZ_OVERRIDE {
        return false;
    }

    virtual bool getPropertyDescriptor(JSContext *cx, HandleObject proxy, HandleId id,
                                       PropertyDescriptor *desc, unsigned flags) MOZ_OVERRIDE;
    virtual bool getOwnPropertyDescriptor(JSContext *cx, HandleObject proxy,
                                          HandleId id, PropertyDescriptor *desc, unsigned flags) MOZ_OVERRIDE;
    virtual bool defineProperty(JSContext *cx, HandleObject proxy, HandleId id,
                                PropertyDescriptor *desc) MOZ_OVERRIDE;
    virtual bool getOwnPropertyNames(JSContext *cx, HandleObject proxy,
                                     AutoIdVector &props) MOZ_OVERRIDE;
    virtual bool delete_(JSContext *cx, HandleObject proxy, HandleId id, bool *bp) MOZ_OVERRIDE;
    virtual bool enumerate(JSContext *cx, HandleObject proxy, AutoIdVector &props) MOZ_OVERRIDE;

    virtual bool has(JSContext *cx, HandleObject proxy, HandleId id, bool *bp) MOZ_OVERRIDE;
    virtual bool hasOwn(JSContext *cx, HandleObject proxy, HandleId id, bool *bp) MOZ_OVERRIDE;
    virtual bool get(JSContext *cx, HandleObject proxy, HandleObject receiver,
                     HandleId id, MutableHandleValue vp) MOZ_OVERRIDE;
    virtual bool set(JSContext *cx, JS::HandleObject proxy, JS::HandleObject receiver,
                     JS::HandleId id, bool strict, JS::MutableHandleValue vp) MOZ_OVERRIDE;
    virtual bool keys(JSContext *cx, HandleObject proxy, AutoIdVector &props) MOZ_OVERRIDE;
    virtual bool iterate(JSContext *cx, HandleObject proxy, unsigned flags,
                         MutableHandleValue vp) MOZ_OVERRIDE;

    virtual bool call(JSContext *cx, HandleObject proxy, const CallArgs &args) MOZ_OVERRIDE;
    virtual void finalize(JSFreeOp *fop, JSObject *proxy) MOZ_OVERRIDE;
    virtual bool objectClassIs(HandleObject obj, js::ESClassValue classValue, JSContext *cx) MOZ_OVERRIDE;
    virtual const char* className(JSContext *cx, HandleObject proxy) MOZ_OVERRIDE;
    virtual bool preventExtensions(JSContext *cx, HandleObject proxy) MOZ_OVERRIDE;
    virtual bool isExtensible(JSObject *proxy) MOZ_OVERRIDE;

    static CPOWProxyHandler singleton;
};

CPOWProxyHandler CPOWProxyHandler::singleton;

bool
CPOWProxyHandler::getPropertyDescriptor(JSContext *cx, HandleObject proxy, HandleId id,
                                        PropertyDescriptor *desc, unsigned flags)
{
    return ParentOf(proxy)->getPropertyDescriptor(cx, proxy, id, desc, flags);
}

bool
JavaScriptParent::getPropertyDescriptor(JSContext *cx, HandleObject proxy, HandleId id,
                                        PropertyDescriptor *desc, unsigned flags)
{
    ObjectId objId = idOf(proxy);

    nsString idstr;
    if (!convertIdToGeckoString(cx, id, &idstr))
        return false;

    ReturnStatus status;
    PPropertyDescriptor result;
    if (!CallGetPropertyDescriptor(objId, idstr, flags, &status, &result))
        return ipcfail(cx);
    if (!ok(cx, status))
        return false;

    return toDescriptor(cx, result, desc);
}

bool
CPOWProxyHandler::getOwnPropertyDescriptor(JSContext *cx, HandleObject proxy,
                                           HandleId id, PropertyDescriptor *desc, unsigned flags)
{
    return ParentOf(proxy)->getOwnPropertyDescriptor(cx, proxy, id, desc, flags);
}

bool
JavaScriptParent::getOwnPropertyDescriptor(JSContext *cx, HandleObject proxy, HandleId id,
                                           PropertyDescriptor *desc, unsigned flags)
{
    ObjectId objId = idOf(proxy);

    nsString idstr;
    if (!convertIdToGeckoString(cx, id, &idstr))
        return false;

    ReturnStatus status;
    PPropertyDescriptor result;
    if (!CallGetOwnPropertyDescriptor(objId, idstr, flags, &status, &result))
        return ipcfail(cx);
    if (!ok(cx, status))
        return false;

    return toDescriptor(cx, result, desc);
}

bool
CPOWProxyHandler::defineProperty(JSContext *cx, HandleObject proxy, HandleId id,
                                 PropertyDescriptor *desc)
{
    MOZ_CRASH();
    return true;
}

bool
CPOWProxyHandler::getOwnPropertyNames(JSContext *cx, HandleObject proxy, AutoIdVector &props)
{
    return ParentOf(proxy)->getOwnPropertyNames(cx, proxy, props);
}

bool
JavaScriptParent::getOwnPropertyNames(JSContext *cx, HandleObject proxy, AutoIdVector &props)
{
    ObjectId objId = idOf(proxy);

    ReturnStatus status;
    InfallibleTArray<nsString> names;
    if (!CallGetOwnPropertyNames(objId, &status, &names))
        return ipcfail(cx);
    if (!ok(cx, status))
        return false;

    RootedId name(cx);
    for (size_t i = 0; i < names.Length(); i++) {
        if (!convertGeckoStringToId(cx, names[i], &name))
            return false;
        if (!props.append(name))
            return false;
    }

    return true;
}

bool
CPOWProxyHandler::keys(JSContext *cx, HandleObject proxy, AutoIdVector &props)
{
    return ParentOf(proxy)->keys(cx, proxy, props);
}

bool
JavaScriptParent::keys(JSContext *cx, HandleObject proxy, AutoIdVector &props)
{
    ObjectId objId = idOf(proxy);

    ReturnStatus status;
    InfallibleTArray<nsString> names;
    if (!CallKeys(objId, &status, &names))
        return ipcfail(cx);
    if (!ok(cx, status))
        return false;

    RootedId name(cx);
    for (size_t i = 0; i < names.Length(); i++) {
        if (!convertGeckoStringToId(cx, names[i], &name))
            return false;
        if (!props.append(name))
            return false;
    }

    return true;
}

bool
CPOWProxyHandler::delete_(JSContext *cx, HandleObject proxy, HandleId id, bool *bp)
{
    MOZ_CRASH();
    return true;
}

bool
CPOWProxyHandler::enumerate(JSContext *cx, HandleObject proxy, AutoIdVector &props)
{
    MOZ_CRASH();
    return true;
}

bool
CPOWProxyHandler::preventExtensions(JSContext *cx, HandleObject proxy)
{
    return ParentOf(proxy)->preventExtensions(cx, proxy);
}

bool
JavaScriptParent::preventExtensions(JSContext *cx, HandleObject proxy)
{
    ObjectId objId = idOf(proxy);

    ReturnStatus status;
    if (!CallPreventExtensions(objId, &status))
        return ipcfail(cx);

    return ok(cx, status);
}

bool
CPOWProxyHandler::isExtensible(JSObject *proxy)
{
    return ParentOf(proxy)->isExtensible(proxy);
}

bool
JavaScriptParent::isExtensible(JSObject *proxy)
{
    ObjectId objId = idOf(proxy);

    // This function is assumed infallible, so we just return false if the IPC
    // channel fails.
    bool extensible;
    if (!CallIsExtensible(objId, &extensible))
        return false;
    return extensible;
}

bool
CPOWProxyHandler::has(JSContext *cx, HandleObject proxy, HandleId id, bool *bp)
{
    return ParentOf(proxy)->has(cx, proxy, id, bp);
}

bool
JavaScriptParent::has(JSContext *cx, HandleObject proxy, HandleId id, bool *bp)
{
    ObjectId objId = idOf(proxy);

    nsString idstr;
    if (!convertIdToGeckoString(cx, id, &idstr))
        return false;

    ReturnStatus status;
    if (!CallHas(objId, idstr, &status, bp))
        return ipcfail(cx);

    return ok(cx, status);
}

bool
CPOWProxyHandler::hasOwn(JSContext *cx, HandleObject proxy, HandleId id, bool *bp)
{
    return ParentOf(proxy)->hasOwn(cx, proxy, id, bp);
}

bool
JavaScriptParent::hasOwn(JSContext *cx, HandleObject proxy, HandleId id, bool *bp)
{
    ObjectId objId = idOf(proxy);

    nsString idstr;
    if (!convertIdToGeckoString(cx, id, &idstr))
        return false;

    ReturnStatus status;
    if (!CallHasOwn(objId, idstr, &status, bp))
        return ipcfail(cx);

    return !!ok(cx, status);
}

bool
CPOWProxyHandler::get(JSContext *cx, HandleObject proxy, HandleObject receiver,
                      HandleId id, MutableHandleValue vp)
{
    return ParentOf(proxy)->get(cx, proxy, receiver, id, vp);
}

bool
JavaScriptParent::get(JSContext *cx, HandleObject proxy, HandleObject receiver,
                      HandleId id, MutableHandleValue vp)
{
    ObjectId objId = idOf(proxy);
    ObjectId receiverId = idOf(receiver);

    nsString idstr;
    if (!convertIdToGeckoString(cx, id, &idstr))
        return false;

    JSVariant val;
    ReturnStatus status;
    if (!CallGet(objId, receiverId, idstr, &status, &val))
        return ipcfail(cx);

    if (!ok(cx, status))
        return false;

    return toValue(cx, val, vp);
}

bool
CPOWProxyHandler::set(JSContext *cx, JS::HandleObject proxy, JS::HandleObject receiver,
                      JS::HandleId id, bool strict, JS::MutableHandleValue vp)
{
    return ParentOf(proxy)->set(cx, proxy, receiver, id, strict, vp);
}

bool
JavaScriptParent::set(JSContext *cx, JS::HandleObject proxy, JS::HandleObject receiver,
                      JS::HandleId id, bool strict, JS::MutableHandleValue vp)
{
    ObjectId objId = idOf(proxy);
    ObjectId receiverId = idOf(receiver);

    nsString idstr;
    if (!convertIdToGeckoString(cx, id, &idstr))
        return false;

    JSVariant val;
    if (!toVariant(cx, vp, &val))
        return false;

    ReturnStatus status;
    JSVariant result;
    if (!CallSet(objId, receiverId, idstr, strict, val, &status, &result))
        return ipcfail(cx);

    if (!ok(cx, status))
        return false;

    return toValue(cx, result, vp);
}

bool
CPOWProxyHandler::iterate(JSContext *cx, HandleObject proxy, unsigned flags,
                          MutableHandleValue vp)
{
    MOZ_CRASH();
    return true;
}

bool
CPOWProxyHandler::call(JSContext *cx, HandleObject proxy, const CallArgs &args)
{
    return ParentOf(proxy)->call(cx, proxy, args);
}

bool
JavaScriptParent::call(JSContext *cx, HandleObject proxy, const CallArgs &args)
{
    ObjectId objId = idOf(proxy);

    InfallibleTArray<JSParam> vals;
    AutoValueVector outobjects(cx);

    RootedValue v(cx);
    for (size_t i = 0; i < args.length() + 2; i++) {
        v = args.base()[i];
        if (v.isObject()) {
            JSObject *obj = &v.toObject();
            if (xpc::IsOutObject(cx, obj)) {
                // Make sure it is not an in-out object.
                JSBool found;
                if (!JS_HasProperty(cx, obj, "value", &found))
                    return false;
                if (found) {
                    JS_ReportError(cx, "in-out objects cannot be sent via CPOWs yet");
                    return false;
                }

                vals.AppendElement(JSParam(void_t()));
                if (!outobjects.append(ObjectValue(*obj)))
                    return false;
                continue;
            }
        }
        JSVariant val;
        if (!toVariant(cx, v, &val))
            return false;
        vals.AppendElement(JSParam(val));
    }

    JSVariant result;
    ReturnStatus status;
    InfallibleTArray<JSParam> outparams;
    if (!CallCall(objId, vals, &status, &result, &outparams))
        return ipcfail(cx);
    if (!ok(cx, status))
        return false;

    if (outparams.Length() != outobjects.length())
        return ipcfail(cx);

    for (size_t i = 0; i < outparams.Length(); i++) {
        // Don't bother doing anything for outparams that weren't set.
        if (outparams[i].type() == JSParam::Tvoid_t)
            continue;

        // Take the value the child process returned, and set it on the XPC
        // object.
        if (!toValue(cx, outparams[i], &v))
            return false;

        JSObject *obj = &outobjects[i].toObject();
        if (!JS_SetProperty(cx, obj, "value", v.address()))
            return false;
    }

    if (!toValue(cx, result, args.rval()))
        return false;

    return true;
}

void
CPOWProxyHandler::finalize(JSFreeOp *fop, JSObject *proxy)
{
    ParentOf(proxy)->drop(proxy);
}

void
JavaScriptParent::drop(JSObject *obj)
{
    if (inactive_)
        return;

    ObjectId objId = idOf(obj);

    objects_.remove(objId);
    if (!SendDropObject(objId))
        MOZ_CRASH();
    decref();
}

bool
CPOWProxyHandler::objectClassIs(HandleObject proxy, js::ESClassValue classValue, JSContext *cx)
{
    return ParentOf(proxy)->objectClassIs(cx, proxy, classValue);
}

bool
JavaScriptParent::objectClassIs(JSContext *cx, HandleObject proxy, js::ESClassValue classValue)
{
    ObjectId objId = idOf(proxy);

    // This function is assumed infallible, so we just return false if the IPC
    // channel fails.
    bool result;
    if (!CallObjectClassIs(objId, classValue, &result))
        return false;

    return result;
}

const char *
CPOWProxyHandler::className(JSContext *cx, HandleObject proxy)
{
    return ParentOf(proxy)->className(cx, proxy);
}

const char *
JavaScriptParent::className(JSContext *cx, HandleObject proxy)
{
    ObjectId objId = idOf(proxy);

    nsString name;
    if (!CallClassName(objId, &name))
        return NULL;

    return ToNewCString(name);
}

bool
JavaScriptParent::init()
{
    if (!JavaScriptShared::init())
        return false;

    return true;
}

bool
JavaScriptParent::makeId(JSContext *cx, JSObject *obj, ObjectId *idp)
{
    if (!IsProxy(obj) || GetProxyHandler(obj) != &CPOWProxyHandler::singleton) {
        JS_ReportError(cx, "cannot ipc non-cpow object");
        return false;
    }

    *idp = idOf(obj);
    return true;
}

JSObject *
JavaScriptParent::unwrap(JSContext *cx, ObjectId objId)
{
    if (JSObject *obj = findObject(objId)) {
        if (!JS_WrapObject(cx, &obj))
            return NULL;
        return obj;
    }

    if (objId > MAX_CPOW_IDS) {
        JS_ReportError(cx, "unusable CPOW id");
        return NULL;
    }

    bool callable = !!(objId & OBJECT_IS_CALLABLE);

    RootedValue v(cx, UndefinedValue());
    JSObject *obj = NewProxyObject(cx,
                                   &CPOWProxyHandler::singleton,
                                   v,
                                   NULL,
                                   NULL,
                                   callable ? ProxyIsCallable : ProxyNotCallable);
    if (!obj)
        return NULL;

    if (!objects_.add(objId, obj))
        return NULL;

    // Incref once we know the decref will be called.
    incref();

    SetProxyExtra(obj, 0, PrivateValue(this));
    SetProxyExtra(obj, 1, DoubleValue(BitwiseCast<double>(objId)));
    return obj;
}

bool
JavaScriptParent::ipcfail(JSContext *cx)
{
    JS_ReportError(cx, "catastrophic IPC failure");
    return false;
}

bool
JavaScriptParent::ok(JSContext *cx, const ReturnStatus &status)
{
    if (status.ok())
        return true;

    RootedValue exn(cx);
    if (!toValue(cx, status.exn(), &exn))
        return false;

    JS_SetPendingException(cx, exn);
    return false;
}

void
JavaScriptParent::decref()
{
    refcount_--;
    if (!refcount_)
        delete this;
}

void
JavaScriptParent::incref()
{
    refcount_++;
}

void
JavaScriptParent::destroyFromContent()
{
    inactive_ = true;
    decref();
}

/* static */ bool
JavaScriptParent::IsCPOW(JSObject *obj)
{
    return IsProxy(obj) && GetProxyHandler(obj) == &CPOWProxyHandler::singleton;
}

/* static */ nsresult
JavaScriptParent::InstanceOf(JSObject *obj, const nsID *id, bool *bp)
{
    return ParentOf(obj)->instanceOf(obj, id, bp);
}

nsresult
JavaScriptParent::instanceOf(JSObject *obj, const nsID *id, bool *bp)
{
    ObjectId objId = idOf(obj);

    JSIID iid;
    ConvertID(*id, &iid);

    ReturnStatus status;
    if (!CallInstanceOf(objId, iid, &status, bp))
        return NS_ERROR_UNEXPECTED;

    if (!status.ok())
        return NS_ERROR_UNEXPECTED;

    return NS_OK;
}
