/* -*- Mode: c++; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
 * 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 "NativeJSContainer.h"

#include <jni.h>

#include "Bundle.h"
#include "GeneratedJNINatives.h"
#include "MainThreadUtils.h"
#include "jsapi.h"
#include "nsJSUtils.h"

#include <mozilla/Vector.h>
#include <mozilla/jni/Accessors.h>
#include <mozilla/jni/Refs.h>
#include <mozilla/jni/Utils.h>

/**
 * NativeJSContainer.cpp implements the native methods in both
 * NativeJSContainer and NativeJSObject, using JSAPI to retrieve values from a
 * JSObject and using JNI to return those values to Java code.
 */

namespace mozilla {
namespace widget {

namespace {

bool CheckThread()
{
    if (!NS_IsMainThread()) {
        jni::ThrowException("java/lang/IllegalThreadStateException",
                            "Not on Gecko thread");
        return false;
    }
    return true;
}

template<class C, typename T> bool
CheckJNIArgument(const jni::Ref<C, T>& arg)
{
    if (!arg) {
        jni::ThrowException("java/lang/IllegalArgumentException",
                            "Null argument");
    }
    return !!arg;
}

nsresult
CheckSDKCall(nsresult rv)
{
    if (NS_FAILED(rv)) {
        jni::ThrowException("java/lang/UnsupportedOperationException",
                            "SDK JNI call failed");
    }
    return rv;
}

// Convert a JNI string to a char16_t string that JSAPI expects.
class JSJNIString final
{
    JNIEnv* const mEnv;
    jni::String::Param mJNIString;
    const char16_t* const mJSString;

public:
    JSJNIString(JNIEnv* env, jni::String::Param str)
        : mEnv(env)
        , mJNIString(str)
        , mJSString(!str ? nullptr : reinterpret_cast<const char16_t*>(
                mEnv->GetStringChars(str.Get(), nullptr)))
    {}

    ~JSJNIString() {
        if (mJNIString) {
            mEnv->ReleaseStringChars(mJNIString.Get(),
                reinterpret_cast<const jchar*>(mJSString));
        }
    }

    operator const char16_t*() const {
        return mJSString;
    }

    size_t Length() const {
        return static_cast<size_t>(mEnv->GetStringLength(mJNIString.Get()));
    }
};

} // namepsace

class NativeJSContainerImpl final
    : public NativeJSObject::Natives<NativeJSContainerImpl>
    , public NativeJSContainer::Natives<NativeJSContainerImpl>
{
    typedef NativeJSContainerImpl Self;
    typedef NativeJSContainer::Natives<NativeJSContainerImpl> ContainerBase;
    typedef NativeJSObject::Natives<NativeJSContainerImpl> ObjectBase;

    typedef JS::PersistentRooted<JSObject*> PersistentObject;

    JNIEnv* const mEnv;
    // Context that the object is valid in
    JSContext* const mJSContext;
    // Root JS object
    PersistentObject mJSObject;
    // Children objects
    Vector<NativeJSObject::GlobalRef, 0> mChildren;

    bool CheckObject() const
    {
        if (!mJSObject) {
            jni::ThrowException("java/lang/NullPointerException",
                                "Null JSObject");
        }
        return !!mJSObject;
    }

    bool CheckJSCall(bool result) const
    {
        if (!result) {
            JS_ClearPendingException(mJSContext);
            jni::ThrowException("java/lang/UnsupportedOperationException",
                                "JSAPI call failed");
        }
        return result;
    }

    // Check that a JS Value contains a particular property type as indicaed by
    // the property's InValue method (e.g. StringProperty::InValue).
    bool CheckProperty(bool (Self::*InValue)(JS::HandleValue) const,
                       JS::HandleValue val) const
    {
        if (!(this->*InValue)(val)) {
            // XXX this can happen when converting a double array inside a
            // Bundle, because double arrays can be misidentified as an int
            // array. The workaround is to add a dummy first element to the
            // array that is a floating point value, i.e. [0.5, ...].
            jni::ThrowException(
                "org/mozilla/gecko/util/NativeJSObject$InvalidPropertyException",
                "Property type mismatch");
            return false;
        }
        return true;
    }

    // Primitive properties

    template<bool (JS::Value::*IsType)() const> bool
    PrimitiveInValue(JS::HandleValue val) const
    {
        return (static_cast<const JS::Value&>(val).*IsType)();
    }

    template<typename U, U (JS::Value::*ToType)() const> U
    PrimitiveFromValue(JS::HandleValue val) const
    {
        return (static_cast<const JS::Value&>(val).*ToType)();
    }

    template<class Prop> typename Prop::NativeArray
    PrimitiveNewArray(JS::HandleObject array, size_t length) const
    {
        typedef typename Prop::JNIType JNIType;

        // Fill up a temporary buffer for our array, then use
        // JNIEnv::Set*ArrayRegion to fill out array in one go.

        UniquePtr<JNIType[]> buffer = MakeUnique<JNIType[]>(length);
        for (size_t i = 0; i < length; i++) {
            JS::RootedValue elem(mJSContext);
            if (!CheckJSCall(JS_GetElement(mJSContext, array, i, &elem)) ||
                !CheckProperty(Prop::InValue, elem)) {
                return nullptr;
            }
            buffer[i] = JNIType((this->*Prop::FromValue)(elem));
        }
        auto jarray = Prop::NativeArray::Adopt(
                mEnv, (mEnv->*Prop::NewJNIArray)(length));
        if (!jarray) {
            return nullptr;
        }
        (mEnv->*Prop::SetJNIArrayRegion)(
                jarray.Get(), 0, length, buffer.get());
        if (mEnv->ExceptionCheck()) {
            return nullptr;
        }
        return jarray;
    }

    template<typename U, typename UA, typename V, typename VA,
             bool (JS::Value::*IsType)() const,
             U (JS::Value::*ToType)() const,
             VA (JNIEnv::*NewArray_)(jsize),
             void (JNIEnv::*SetArrayRegion_)(VA, jsize, jsize, const V*)>
    struct PrimitiveProperty
    {
        // C++ type for a primitive property (e.g. bool)
        typedef U NativeType;
        // C++ type for the fallback value used in opt* methods
        typedef U NativeFallback;
        // Type for an array of the primitive type (e.g. BooleanArray::LocalRef)
        typedef typename UA::LocalRef NativeArray;
        // Type for the fallback value used in opt*Array methods
        typedef const typename UA::Ref ArrayFallback;
        // JNI type (e.g. jboolean)
        typedef V JNIType;

        // JNIEnv function to create a new JNI array of the primiive type
        typedef decltype(NewArray_) NewJNIArray_t;
        static constexpr NewJNIArray_t NewJNIArray = NewArray_;

        // JNIEnv function to fill a JNI array of the primiive type
        typedef decltype(SetArrayRegion_) SetJNIArrayRegion_t;
        static constexpr SetJNIArrayRegion_t SetJNIArrayRegion = SetArrayRegion_;

        // Function to determine if a JS Value contains the primitive type
        typedef decltype(&Self::PrimitiveInValue<IsType>) InValue_t;
        static constexpr InValue_t InValue = &Self::PrimitiveInValue<IsType>;

        // Function to convert a JS Value to the primitive type
        typedef decltype(&Self::PrimitiveFromValue<U, ToType>) FromValue_t;
        static constexpr FromValue_t FromValue
                = &Self::PrimitiveFromValue<U, ToType>;

        // Function to convert a JS array to a JNI array
        typedef decltype(&Self::PrimitiveNewArray<PrimitiveProperty>) NewArray_t;
        static constexpr NewArray_t NewArray
                = &Self::PrimitiveNewArray<PrimitiveProperty>;
    };

    // String properties

    bool StringInValue(JS::HandleValue val) const
    {
        return val.isString();
    }

    jni::String::LocalRef
    StringFromValue(const JS::HandleString str) const
    {
        nsAutoJSString autoStr;
        if (!CheckJSCall(autoStr.init(mJSContext, str))) {
            return nullptr;
        }
        // StringParam can automatically convert a nsString to jstring.
        return jni::StringParam(autoStr, mEnv);
    }

    jni::String::LocalRef
    StringFromValue(JS::HandleValue val)
    {
        const JS::RootedString str(mJSContext, val.toString());
        return StringFromValue(str);
    }

    // Bundle properties

    sdk::Bundle::LocalRef
    BundleFromValue(const JS::HandleObject obj)
    {
        JS::Rooted<JS::IdVector> ids(mJSContext, JS::IdVector(mJSContext));
        if (!CheckJSCall(JS_Enumerate(mJSContext, obj, &ids))) {
            return nullptr;
        }

        const size_t length = ids.length();
        sdk::Bundle::LocalRef newBundle(mEnv);
        NS_ENSURE_SUCCESS(CheckSDKCall(
                sdk::Bundle::New(length, &newBundle)), nullptr);

        // Iterate through each property of the JS object. For each property,
        // determine its type from a list of supported types, and convert that
        // proeprty to the supported type.

        for (size_t i = 0; i < ids.length(); i++) {
            const JS::RootedId id(mJSContext, ids[i]);
            JS::RootedValue idVal(mJSContext);
            if (!CheckJSCall(JS_IdToValue(mJSContext, id, &idVal))) {
                return nullptr;
            }

            const JS::RootedString idStr(mJSContext,
                                         JS::ToString(mJSContext, idVal));
            if (!CheckJSCall(!!idStr)) {
                return nullptr;
            }

            jni::String::LocalRef name = StringFromValue(idStr);
            JS::RootedValue val(mJSContext);
            if (!name ||
                !CheckJSCall(JS_GetPropertyById(mJSContext, obj, id, &val))) {
                return nullptr;
            }

#define PUT_IN_BUNDLE_IF_TYPE_IS(TYPE)                                  \
            if ((this->*TYPE##Property::InValue)(val)) {                \
                auto jval = (this->*TYPE##Property::FromValue)(val);    \
                if (mEnv->ExceptionCheck()) {                           \
                    return nullptr;                                     \
                }                                                       \
                NS_ENSURE_SUCCESS(CheckSDKCall(                         \
                        newBundle->Put##TYPE(name, jval)), nullptr);    \
                continue;                                               \
            }                                                           \
            ((void) 0) // Accommodate trailing semicolon.

            // Scalar values are faster to check, so check them first.
            PUT_IN_BUNDLE_IF_TYPE_IS(Boolean);
            // Int can be casted to double, so check int first.
            PUT_IN_BUNDLE_IF_TYPE_IS(Int);
            PUT_IN_BUNDLE_IF_TYPE_IS(Double);
            PUT_IN_BUNDLE_IF_TYPE_IS(String);
            // There's no "putObject", so don't check ObjectProperty

            // Check for array types if scalar checks all failed.
            // XXX empty arrays are treated as boolean arrays. Workaround is
            // to always have a dummy element to create a non-empty array.
            PUT_IN_BUNDLE_IF_TYPE_IS(BooleanArray);
            // XXX because we only check the first element of an array,
            // a double array can potentially be seen as an int array.
            // When that happens, the Bundle conversion will fail.
            PUT_IN_BUNDLE_IF_TYPE_IS(IntArray);
            PUT_IN_BUNDLE_IF_TYPE_IS(DoubleArray);
            PUT_IN_BUNDLE_IF_TYPE_IS(StringArray);
            // There's no "putObjectArray", so don't check ObjectArrayProperty
            // There's no "putBundleArray", so don't check BundleArrayProperty

            // Use Bundle as the default catch-all for objects
            PUT_IN_BUNDLE_IF_TYPE_IS(Bundle);

#undef PUT_IN_BUNDLE_IF_TYPE_IS

            // We tried all supported types; just bail.
            jni::ThrowException("java/lang/UnsupportedOperationException",
                                "Unsupported property type");
            return nullptr;
        }
        return jni::Object::LocalRef::Adopt(newBundle.Env(),
                                            newBundle.Forget());
    }

    sdk::Bundle::LocalRef
    BundleFromValue(JS::HandleValue val)
    {
        if (val.isNull()) {
            return nullptr;
        }
        JS::RootedObject object(mJSContext, &val.toObject());
        return BundleFromValue(object);
    }

    // Object properties

    bool ObjectInValue(JS::HandleValue val) const
    {
        return val.isObjectOrNull();
    }

    NativeJSObject::LocalRef
    ObjectFromValue(JS::HandleValue val)
    {
        if (val.isNull()) {
            return nullptr;
        }
        JS::RootedObject object(mJSContext, &val.toObject());
        return CreateChild(object);
    }

    template<class Prop> typename Prop::NativeArray
    ObjectNewArray(JS::HandleObject array, size_t length)
    {
        auto jarray = Prop::NativeArray::Adopt(mEnv, mEnv->NewObjectArray(
                length, typename Prop::ClassType::Context().ClassRef(),
                nullptr));
        if (!jarray) {
            return nullptr;
        }

        // For object arrays, we have to set each element separately.
        for (size_t i = 0; i < length; i++) {
            JS::RootedValue elem(mJSContext);
            if (!CheckJSCall(JS_GetElement(mJSContext, array, i, &elem)) ||
                !CheckProperty(Prop::InValue, elem)) {
                return nullptr;
            }
            mEnv->SetObjectArrayElement(
                    jarray.Get(), i, (this->*Prop::FromValue)(elem).Get());
            if (mEnv->ExceptionCheck()) {
                return nullptr;
            }
        }
        return jarray;
    }

    template<class Class,
             bool (Self::*InValue_)(JS::HandleValue) const,
             typename Class::LocalRef (Self::*FromValue_)(JS::HandleValue)>
    struct BaseObjectProperty
    {
        // JNI class for the object type (e.g. jni::String)
        typedef Class ClassType;

        // See comments in PrimitiveProperty.
        typedef typename ClassType::LocalRef NativeType;
        typedef const typename ClassType::Ref NativeFallback;
        typedef typename jni::ObjectArray::LocalRef NativeArray;
        typedef const jni::ObjectArray::Ref ArrayFallback;

        typedef decltype(InValue_) InValue_t;
        static constexpr InValue_t InValue = InValue_;

        typedef decltype(FromValue_) FromValue_t;
        static constexpr FromValue_t FromValue = FromValue_;

        typedef decltype(&Self::ObjectNewArray<BaseObjectProperty>) NewArray_t;
        static constexpr NewArray_t NewArray
                = &Self::ObjectNewArray<BaseObjectProperty>;
    };

    // Array properties

    template<class Prop> bool
    ArrayInValue(JS::HandleValue val) const
    {
        if (!val.isObject()) {
            return false;
        }
        JS::RootedObject obj(mJSContext, &val.toObject());
        bool isArray;
        uint32_t length = 0;
        if (!JS_IsArrayObject(mJSContext, obj, &isArray) ||
            !isArray ||
            !JS_GetArrayLength(mJSContext, obj, &length)) {
            JS_ClearPendingException(mJSContext);
            return false;
        }
        if (!length) {
            // Empty arrays are always okay.
            return true;
        }
        // We only check to see the first element is the target type. If the
        // array has mixed types, we'll throw an error during actual conversion.
        JS::RootedValue element(mJSContext);
        if (!JS_GetElement(mJSContext, obj, 0, &element)) {
            JS_ClearPendingException(mJSContext);
            return false;
        }
        return (this->*Prop::InValue)(element);
    }

    template<class Prop> typename Prop::NativeArray
    ArrayFromValue(JS::HandleValue val)
    {
        JS::RootedObject obj(mJSContext, &val.toObject());
        uint32_t length = 0;
        if (!CheckJSCall(JS_GetArrayLength(mJSContext, obj, &length))) {
            return nullptr;
        }
        return (this->*Prop::NewArray)(obj, length);
    }

    template<class Prop>
    struct ArrayProperty
    {
        // See comments in PrimitiveProperty.
        typedef typename Prop::NativeArray NativeType;
        typedef typename Prop::ArrayFallback NativeFallback;

        typedef decltype(&Self::ArrayInValue<Prop>) InValue_t;
        static constexpr InValue_t InValue
                = &Self::ArrayInValue<Prop>;

        typedef decltype(&Self::ArrayFromValue<Prop>) FromValue_t;
        static constexpr FromValue_t FromValue
                = &Self::ArrayFromValue<Prop>;
    };

    // "Has" property is a special property type that is used to implement
    // NativeJSObject.has, by returning true from InValue and FromValue for
    // every existing property, and having false as the fallback value for
    // when a property doesn't exist.

    bool HasValue(JS::HandleValue val) const
    {
        return true;
    }

    struct HasProperty
    {
        // See comments in PrimitiveProperty.
        typedef bool NativeType;
        typedef bool NativeFallback;

        typedef decltype(&Self::HasValue) HasValue_t;
        static constexpr HasValue_t InValue = &Self::HasValue;
        static constexpr HasValue_t FromValue = &Self::HasValue;
    };

    // Statically cast from bool to jboolean (unsigned char); it works
    // since false and JNI_FALSE have the same value (0), and true and
    // JNI_TRUE have the same value (1).
    typedef PrimitiveProperty<
            bool, jni::BooleanArray, jboolean, jbooleanArray,
            &JS::Value::isBoolean, &JS::Value::toBoolean,
            &JNIEnv::NewBooleanArray, &JNIEnv::SetBooleanArrayRegion>
        BooleanProperty;

    typedef PrimitiveProperty<
            double, jni::DoubleArray, jdouble, jdoubleArray,
            &JS::Value::isNumber, &JS::Value::toNumber,
            &JNIEnv::NewDoubleArray, &JNIEnv::SetDoubleArrayRegion>
        DoubleProperty;

    typedef PrimitiveProperty<
            int32_t, jni::IntArray, jint, jintArray,
            &JS::Value::isInt32, &JS::Value::toInt32,
            &JNIEnv::NewIntArray, &JNIEnv::SetIntArrayRegion>
        IntProperty;

    typedef BaseObjectProperty<
            jni::String, &Self::StringInValue, &Self::StringFromValue>
        StringProperty;

    typedef BaseObjectProperty<
            sdk::Bundle, &Self::ObjectInValue, &Self::BundleFromValue>
        BundleProperty;

    typedef BaseObjectProperty<
            NativeJSObject, &Self::ObjectInValue, &Self::ObjectFromValue>
        ObjectProperty;

    typedef ArrayProperty<BooleanProperty> BooleanArrayProperty;
    typedef ArrayProperty<DoubleProperty> DoubleArrayProperty;
    typedef ArrayProperty<IntProperty> IntArrayProperty;
    typedef ArrayProperty<StringProperty> StringArrayProperty;
    typedef ArrayProperty<BundleProperty> BundleArrayProperty;
    typedef ArrayProperty<ObjectProperty> ObjectArrayProperty;

    template<class Prop>
    typename Prop::NativeType
    GetProperty(jni::String::Param name,
                typename Prop::NativeFallback* fallback = nullptr)
    {
        if (!CheckThread() || !CheckObject()) {
            return typename Prop::NativeType();
        }

        const JSJNIString nameStr(mEnv, name);
        JS::RootedValue val(mJSContext);

        if (!CheckJNIArgument(name) ||
            !CheckJSCall(JS_GetUCProperty(
                    mJSContext, mJSObject, nameStr, nameStr.Length(), &val))) {
            return typename Prop::NativeType();
        }

        // Strictly, null is different from undefined in JS. However, in
        // practice, null is often used to indicate a property doesn't exist in
        // the same manner as undefined. Therefore, we treat null in the same
        // way as undefined when checking property existence (bug 1014965).
        if (val.isUndefined() || val.isNull()) {
            if (fallback) {
                return mozilla::Move(*fallback);
            }
            jni::ThrowException(
                "org/mozilla/gecko/util/NativeJSObject$InvalidPropertyException",
                "Property does not exist");
            return typename Prop::NativeType();
        }

        if (!CheckProperty(Prop::InValue, val)) {
            return typename Prop::NativeType();
        }
        return (this->*Prop::FromValue)(val);
    }

    NativeJSObject::LocalRef CreateChild(JS::HandleObject object)
    {
        auto instance = NativeJSObject::New();
        mozilla::UniquePtr<NativeJSContainerImpl> impl(
                new NativeJSContainerImpl(instance.Env(), mJSContext, object));

        ObjectBase::AttachNative(instance, mozilla::Move(impl));
        if (!mChildren.append(NativeJSObject::GlobalRef(instance))) {
            MOZ_CRASH();
        }
        return instance;
    }

    NativeJSContainerImpl(JNIEnv* env, JSContext* cx, JS::HandleObject object)
        : mEnv(env)
        , mJSContext(cx)
        , mJSObject(cx, object)
    {}

public:
    ~NativeJSContainerImpl()
    {
        // Dispose of all children on destruction. The children will in turn
        // dispose any of their children (i.e. our grandchildren) and so on.
        NativeJSObject::LocalRef child(mEnv);
        for (size_t i = 0; i < mChildren.length(); i++) {
            child = mChildren[i];
            ObjectBase::GetNative(child)->ObjectBase::DisposeNative(child);
        }
    }

    static NativeJSContainer::LocalRef
    CreateInstance(JSContext* cx, JS::HandleObject object)
    {
        auto instance = NativeJSContainer::New();
        mozilla::UniquePtr<NativeJSContainerImpl> impl(
                new NativeJSContainerImpl(instance.Env(), cx, object));

        ContainerBase::AttachNative(instance, mozilla::Move(impl));
        return instance;
    }

    // NativeJSContainer methods

    void DisposeNative(const NativeJSContainer::LocalRef& instance)
    {
        if (!CheckThread()) {
            return;
        }
        ContainerBase::DisposeNative(instance);
    }

    NativeJSContainer::LocalRef Clone()
    {
        if (!CheckThread()) {
            return nullptr;
        }
        return CreateInstance(mJSContext, mJSObject);
    }

    // NativeJSObject methods

    bool GetBoolean(jni::String::Param name)
    {
        return GetProperty<BooleanProperty>(name);
    }

    bool OptBoolean(jni::String::Param name, bool fallback)
    {
        return GetProperty<BooleanProperty>(name, &fallback);
    }

    jni::BooleanArray::LocalRef
    GetBooleanArray(jni::String::Param name)
    {
        return GetProperty<BooleanArrayProperty>(name);
    }

    jni::BooleanArray::LocalRef
    OptBooleanArray(jni::String::Param name, jni::BooleanArray::Param fallback)
    {
        return GetProperty<BooleanArrayProperty>(name, &fallback);
    }

    jni::Object::LocalRef
    GetBundle(jni::String::Param name)
    {
        return GetProperty<BundleProperty>(name);
    }

    jni::Object::LocalRef
    OptBundle(jni::String::Param name, jni::Object::Param fallback)
    {
        // Because the GetProperty expects a sdk::Bundle::Param,
        // we have to do conversions here from jni::Object::Param.
        const auto& fb = sdk::Bundle::Ref::From(fallback.Get());
        return GetProperty<BundleProperty>(name, &fb);
    }

    jni::ObjectArray::LocalRef
    GetBundleArray(jni::String::Param name)
    {
        return GetProperty<BundleArrayProperty>(name);
    }

    jni::ObjectArray::LocalRef
    OptBundleArray(jni::String::Param name, jni::ObjectArray::Param fallback)
    {
        return GetProperty<BundleArrayProperty>(name, &fallback);
    }

    double GetDouble(jni::String::Param name)
    {
        return GetProperty<DoubleProperty>(name);
    }

    double OptDouble(jni::String::Param name, double fallback)
    {
        return GetProperty<DoubleProperty>(name, &fallback);
    }

    jni::DoubleArray::LocalRef
    GetDoubleArray(jni::String::Param name)
    {
        return GetProperty<DoubleArrayProperty>(name);
    }

    jni::DoubleArray::LocalRef
    OptDoubleArray(jni::String::Param name, jni::DoubleArray::Param fallback)
    {
        jni::DoubleArray::LocalRef fb(fallback);
        return GetProperty<DoubleArrayProperty>(name, &fb);
    }

    int GetInt(jni::String::Param name)
    {
        return GetProperty<IntProperty>(name);
    }

    int OptInt(jni::String::Param name, int fallback)
    {
        return GetProperty<IntProperty>(name, &fallback);
    }

    jni::IntArray::LocalRef
    GetIntArray(jni::String::Param name)
    {
        return GetProperty<IntArrayProperty>(name);
    }

    jni::IntArray::LocalRef
    OptIntArray(jni::String::Param name, jni::IntArray::Param fallback)
    {
        jni::IntArray::LocalRef fb(fallback);
        return GetProperty<IntArrayProperty>(name, &fb);
    }

    NativeJSObject::LocalRef
    GetObject(jni::String::Param name)
    {
        return GetProperty<ObjectProperty>(name);
    }

    NativeJSObject::LocalRef
    OptObject(jni::String::Param name, NativeJSObject::Param fallback)
    {
        return GetProperty<ObjectProperty>(name, &fallback);
    }

    jni::ObjectArray::LocalRef
    GetObjectArray(jni::String::Param name)
    {
        return GetProperty<ObjectArrayProperty>(name);
    }

    jni::ObjectArray::LocalRef
    OptObjectArray(jni::String::Param name, jni::ObjectArray::Param fallback)
    {
        return GetProperty<ObjectArrayProperty>(name, &fallback);
    }

    jni::String::LocalRef
    GetString(jni::String::Param name)
    {
        return GetProperty<StringProperty>(name);
    }

    jni::String::LocalRef
    OptString(jni::String::Param name, jni::String::Param fallback)
    {
        return GetProperty<StringProperty>(name, &fallback);
    }

    jni::ObjectArray::LocalRef
    GetStringArray(jni::String::Param name)
    {
        return GetProperty<StringArrayProperty>(name);
    }

    jni::ObjectArray::LocalRef
    OptStringArray(jni::String::Param name, jni::ObjectArray::Param fallback)
    {
        return GetProperty<StringArrayProperty>(name, &fallback);
    }

    bool Has(jni::String::Param name)
    {
        bool no = false;
        // Fallback to false indicating no such property.
        return GetProperty<HasProperty>(name, &no);
    }

    jni::Object::LocalRef ToBundle()
    {
        if (!CheckThread() || !CheckObject()) {
            return nullptr;
        }
        return BundleFromValue(mJSObject);
    }

private:
    static bool AppendJSON(const char16_t* buf, uint32_t len, void* data)
    {
        static_cast<nsAutoString*>(data)->Append(buf, len);
        return true;
    }

public:
    jni::String::LocalRef ToString()
    {
        if (!CheckThread() || !CheckObject()) {
            return nullptr;
        }

        JS::RootedValue value(mJSContext, JS::ObjectValue(*mJSObject));
        nsAutoString json;
        if (!CheckJSCall(JS_Stringify(mJSContext, &value, nullptr,
                                      JS::NullHandleValue, AppendJSON, &json))) {
            return nullptr;
        }
        return jni::StringParam(json, mEnv);
    }
};


// Define the "static constexpr" members of our property types (e.g.
// PrimitiveProperty<>::InValue). This is tricky because there are a lot of
// template parameters, so we use macros to make it simpler.

#define DEFINE_PRIMITIVE_PROPERTY_MEMBER(Name) \
    template<typename U, typename UA, typename V, typename VA, \
             bool (JS::Value::*I)() const, \
             U (JS::Value::*T)() const, \
             VA (JNIEnv::*N)(jsize), \
             void (JNIEnv::*S)(VA, jsize, jsize, const V*)> \
    constexpr typename NativeJSContainerImpl \
        ::PrimitiveProperty<U, UA, V, VA, I, T, N, S>::Name##_t \
    NativeJSContainerImpl::PrimitiveProperty<U, UA, V, VA, I, T, N, S>::Name

DEFINE_PRIMITIVE_PROPERTY_MEMBER(NewJNIArray);
DEFINE_PRIMITIVE_PROPERTY_MEMBER(SetJNIArrayRegion);
DEFINE_PRIMITIVE_PROPERTY_MEMBER(InValue);
DEFINE_PRIMITIVE_PROPERTY_MEMBER(FromValue);
DEFINE_PRIMITIVE_PROPERTY_MEMBER(NewArray);

#undef DEFINE_PRIMITIVE_PROPERTY_MEMBER

#define DEFINE_OBJECT_PROPERTY_MEMBER(Name) \
    template<class C, \
             bool (NativeJSContainerImpl::*I)(JS::HandleValue) const, \
             typename C::LocalRef (NativeJSContainerImpl::*F)(JS::HandleValue)> \
    constexpr typename NativeJSContainerImpl \
        ::BaseObjectProperty<C, I, F>::Name##_t \
    NativeJSContainerImpl::BaseObjectProperty<C, I, F>::Name

DEFINE_OBJECT_PROPERTY_MEMBER(InValue);
DEFINE_OBJECT_PROPERTY_MEMBER(FromValue);
DEFINE_OBJECT_PROPERTY_MEMBER(NewArray);

#undef DEFINE_OBJECT_PROPERTY_MEMBER

template<class P> constexpr typename NativeJSContainerImpl::ArrayProperty<P>
        ::InValue_t NativeJSContainerImpl::ArrayProperty<P>::InValue;
template<class P> constexpr typename NativeJSContainerImpl::ArrayProperty<P>
        ::FromValue_t NativeJSContainerImpl::ArrayProperty<P>::FromValue;

constexpr NativeJSContainerImpl::HasProperty::HasValue_t
        NativeJSContainerImpl::HasProperty::InValue;
constexpr NativeJSContainerImpl::HasProperty::HasValue_t
        NativeJSContainerImpl::HasProperty::FromValue;


NativeJSContainer::LocalRef
CreateNativeJSContainer(JSContext* cx, JS::HandleObject object)
{
    return NativeJSContainerImpl::CreateInstance(cx, object);
}

} // namespace widget
} // namespace mozilla

