/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
 *
 * The contents of this file are subject to the Netscape Public License
 * Version 1.0 (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/NPL/
 *
 * 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 Communicator client code.
 *
 * The Initial Developer of the Original Code is Netscape Communications
 * Corporation.  Portions created by Netscape are Copyright (C) 1998
 * Netscape Communications Corporation.  All Rights Reserved.
 */

/* This file is part of the Java-vendor-neutral implementation of LiveConnect
 *
 * It contains the native code implementation of JS's JavaClass class.
 *
 * A JavaClass is JavaScript's representation of a Java class.
 * Its parent JS object is always a JavaPackage object.  A JavaClass is not an
 * exact reflection of Java's corresponding java.lang.Class object.  Rather,
 * the properties of a JavaClass are the static methods and properties of the
 * corresponding Java class.
 *
 * Note that there is no runtime equivalent to the JavaClass class in Java.
 * (Although there are instances of java.lang.String and there are static
 * methods of java.lang.String that can be invoked, there's no such thing as
 * a first-class object that can be referenced simply as "java.lang.String".)
 */

#include <stdlib.h>
#include <string.h>

#include "prtypes.h"
#include "prprf.h"
#include "prlog.h"

#include "jsj_private.h"        /* LiveConnect internals */

static JSBool
JavaClass_convert(JSContext *cx, JSObject *obj, JSType type, jsval *vp)
{
    char *name;
    JSString *str;

    JavaClassDescriptor *class_descriptor;
    
    class_descriptor = JS_GetPrivate(cx, obj);
    if (!class_descriptor)
        return JS_FALSE;

    switch(type) {


    case JSTYPE_STRING:
        /* Convert '/' to '.' so that it looks like Java language syntax. */
        if (!class_descriptor->name)
            break;
        name = PR_smprintf("[JavaClass %s]", class_descriptor->name);
        if (!name) {
            JS_ReportOutOfMemory(cx);
            return JS_FALSE;
        }

        str = JS_NewString(cx, name, strlen(name));
        if (!str) {
            free(name);
            /* It's not necessary to call JS_ReportOutOfMemory(), as
               JS_NewString() will do so on failure. */
            return JS_FALSE;
        }

        *vp = STRING_TO_JSVAL(str);
        return JS_TRUE;

    default:
      break;
    }
    return JS_TRUE;
}

static JSBool
lookup_static_member_by_id(JSContext *cx, JNIEnv *jEnv, JSObject *obj,
                           JavaClassDescriptor **class_descriptorp,
                           jsid id, JavaMemberDescriptor **memberp)
{
    jsval idval;
    JavaMemberDescriptor *member_descriptor;
    const char *member_name;
    JavaClassDescriptor *class_descriptor;

    class_descriptor = JS_GetPrivate(cx, obj);
    if (!class_descriptor) {
        *class_descriptorp = NULL;
        *memberp = NULL;
        return JS_TRUE;
    }
    
    if (class_descriptorp)
        *class_descriptorp = class_descriptor;
    
    member_descriptor = jsj_LookupJavaStaticMemberDescriptorById(cx, jEnv, class_descriptor, id);
    if (!member_descriptor) {
        JS_IdToValue(cx, id, &idval);
        if (!JSVAL_IS_STRING(idval)) {
            JS_ReportError(cx, "invalid JavaClass property expression. "
                "(methods and fields of a JavaClass object can only be identified by their name)");
            return JS_FALSE;
        }

        member_name = JS_GetStringBytes(JSVAL_TO_STRING(idval));

        /* Why do we have to do this ? */
        if (!strcmp(member_name, "prototype")) {
            *memberp = NULL;
            return JS_TRUE;
        }

        JS_ReportError(cx, "Java class %s has no public static field or method named \"%s\"",
                       class_descriptor->name, member_name);
        return JS_FALSE;
    }
    if (memberp)
        *memberp = member_descriptor;
    return JS_TRUE;
}

PR_STATIC_CALLBACK(JSBool)
JavaClass_getPropertyById(JSContext *cx, JSObject *obj, jsid id, jsval *vp)
{
    jsval idval;
    jclass java_class;
    const char *member_name;
    JavaClassDescriptor *class_descriptor;
    JavaMemberDescriptor *member_descriptor;
    JNIEnv *jEnv;

    /* printf("In JavaClass_getProperty\n"); */
    
    /* Get the Java per-thread environment pointer for this JSContext */
    jsj_MapJSContextToJSJThread(cx, &jEnv);
    if (!jEnv)
        return JS_FALSE;

    if (!lookup_static_member_by_id(cx, jEnv, obj, &class_descriptor, id, &member_descriptor))
        return JS_FALSE;
    if (!member_descriptor) {
        *vp = JSVAL_VOID;
        return JS_TRUE;
    }

    java_class = class_descriptor->java_class;

    if (member_descriptor->field) {
        if (!member_descriptor->methods) {
            return jsj_GetJavaFieldValue(cx, jEnv, member_descriptor->field, java_class, vp);
        } else {
            PR_ASSERT(0);
            return JS_FALSE;
        }
    } else {
        JSFunction *function;
        
        /* TODO - eliminate JSFUN_BOUND_METHOD */
        JS_IdToValue(cx, id, &idval);
        member_name = JS_GetStringBytes(JSVAL_TO_STRING(idval));
        function = JS_NewFunction(cx, jsj_JavaStaticMethodWrapper, 0,
                                  JSFUN_BOUND_METHOD, obj, member_name);
        if (!function)
            return JS_FALSE;

        *vp = OBJECT_TO_JSVAL(JS_GetFunctionObject(function));
        return JS_TRUE;
    }
}

PR_STATIC_CALLBACK(JSBool)
JavaClass_setPropertyById(JSContext *cx, JSObject *obj, jsid id, jsval *vp)
{
    jclass java_class;
    const char *member_name;
    JavaClassDescriptor *class_descriptor;
    JavaMemberDescriptor *member_descriptor;
    jsval idval;
    JNIEnv *jEnv;

    /* printf("In JavaClass_setProperty\n"); */

    /* Get the Java per-thread environment pointer for this JSContext */
    jsj_MapJSContextToJSJThread(cx, &jEnv);
    if (!jEnv)
        return JS_FALSE;
    
    if (!lookup_static_member_by_id(cx, jEnv, obj, &class_descriptor, id, &member_descriptor))
        return JS_FALSE;

    /* Check for the case where there is a method with the given name, but no field
       with that name */
    if (!member_descriptor->field)
        goto no_such_field;

    /* Silently fail if field value is final (immutable), as required by ECMA spec */
    if (member_descriptor->field->modifiers & ACC_FINAL)
        return JS_TRUE;

    java_class = class_descriptor->java_class;
    return jsj_SetJavaFieldValue(cx, jEnv, member_descriptor->field, java_class, *vp);

no_such_field:
    JS_IdToValue(cx, id, &idval);
    member_name = JS_GetStringBytes(JSVAL_TO_STRING(idval));
    JS_ReportError(cx, "No static field named \"%s\" in Java class %s",
                   member_name, class_descriptor->name);
    return JS_FALSE;
}

/*
 * Free the private native data associated with the JavaPackage object.
 */
PR_STATIC_CALLBACK(void)
JavaClass_finalize(JSContext *cx, JSObject *obj)
{
    JNIEnv *jEnv;

    JavaClassDescriptor *class_descriptor = JS_GetPrivate(cx, obj);
    if (!class_descriptor)
        return;
    
    /* Get the Java per-thread environment pointer for this JSContext */
    jsj_MapJSContextToJSJThread(cx, &jEnv);
    if (!jEnv)
        return;

    printf("Finalizing %s\n", class_descriptor->name);
    jsj_ReleaseJavaClassDescriptor(cx, jEnv, class_descriptor);
}


static JSBool
JavaClass_lookupProperty(JSContext *cx, JSObject *obj, jsid id,
                         JSObject **objp, JSProperty **propp
#if defined JS_THREADSAFE && defined DEBUG
                            , const char *file, uintN line
#endif
                            )
{
    JNIEnv *jEnv;

    /* printf("In JavaClass_lookupProperty()\n"); */

    /* Get the Java per-thread environment pointer for this JSContext */
    jsj_MapJSContextToJSJThread(cx, &jEnv);
    if (!jEnv)
        return JS_FALSE;

    if (!lookup_static_member_by_id(cx, jEnv, obj, NULL, id, NULL))
        return JS_FALSE;
    *objp = obj;
    *propp = (JSProperty*)1;
    return JS_TRUE;
}

static JSBool
JavaClass_defineProperty(JSContext *cx, JSObject *obj, jsid id, jsval value,
                         JSPropertyOp getter, JSPropertyOp setter,
                         uintN attrs, JSProperty **propp)
{
    JS_ReportError(cx, "Cannot define a new property in a JavaClass");
    return JS_FALSE;
}

static JSBool
JavaClass_getAttributes(JSContext *cx, JSObject *obj, jsid id,
                        JSProperty *prop, uintN *attrsp)
{
    /* We don't maintain JS property attributes for Java class members */
    *attrsp = JSPROP_PERMANENT|JSPROP_ENUMERATE;
    return JS_FALSE;
}

static JSBool
JavaClass_setAttributes(JSContext *cx, JSObject *obj, jsid id,
                        JSProperty *prop, uintN *attrsp)
{
    /* We don't maintain JS property attributes for Java class members */
    if (*attrsp != JSPROP_PERMANENT|JSPROP_ENUMERATE) {
        PR_ASSERT(0);
        return JS_FALSE;
    }

    /* Silently ignore all setAttribute attempts */
    return JS_TRUE;
}

static JSBool
JavaClass_deleteProperty(JSContext *cx, JSObject *obj, jsid id, jsval *vp)
{
    JSVersion version = JS_GetVersion(cx);
    
    *vp = JSVAL_FALSE;

    if (!JSVERSION_IS_ECMA(version)) {
        JS_ReportError(cx, "Properties of JavaClass objects may not be deleted");
        return JS_FALSE;
    } else {
        /* Attempts to delete permanent properties are silently ignored
           by ECMAScript. */
        return JS_TRUE;
    }
}

static JSBool
JavaClass_defaultValue(JSContext *cx, JSObject *obj, JSType type, jsval *vp)
{
    /* printf("In JavaClass_defaultValue()\n"); */
    return JavaClass_convert(cx, obj, JSTYPE_STRING, vp);
}

static JSBool
JavaClass_newEnumerate(JSContext *cx, JSObject *obj, JSIterateOp enum_op,
                       jsval *statep, jsid *idp)
{
    JavaMemberDescriptor *member_descriptor;
    JavaClassDescriptor *class_descriptor;
    JNIEnv *jEnv;
    
    class_descriptor = JS_GetPrivate(cx, obj);

    /* Check for prototype JavaClass object */
    if (!class_descriptor) {
        *statep = JSVAL_NULL;
        if (idp)
            *idp = INT_TO_JSVAL(0);
        return JS_TRUE;
    }
    
    switch(enum_op) {
    case JSENUMERATE_INIT:
        /* Get the Java per-thread environment pointer for this JSContext */
        jsj_MapJSContextToJSJThread(cx, &jEnv);
        if (!jEnv)
            return JS_FALSE;
        member_descriptor = jsj_GetClassStaticMembers(cx, jEnv, class_descriptor);
        *statep = PRIVATE_TO_JSVAL(member_descriptor);
        if (idp)
            *idp = INT_TO_JSVAL(class_descriptor->num_instance_members);
        return JS_TRUE;
        
    case JSENUMERATE_NEXT:
        member_descriptor = JSVAL_TO_PRIVATE(*statep);
        if (member_descriptor) {
            *idp = member_descriptor->id;
            *statep = PRIVATE_TO_JSVAL(member_descriptor->next);
            return JS_TRUE;
        }
        /* Fall through ... */

    case JSENUMERATE_DESTROY:
        *statep = JSVAL_NULL;
        return JS_TRUE;

    default:
        PR_ASSERT(0);
        return JS_FALSE;
    }
}

static JSBool
JavaClass_checkAccess(JSContext *cx, JSObject *obj, jsid id,
                      JSAccessMode mode, jsval *vp, uintN *attrsp)
{
    switch (mode) {
    case JSACC_WATCH:
        JS_ReportError(cx, "Cannot place watchpoints on JavaClass object properties");
        return JS_FALSE;

    case JSACC_IMPORT:
        JS_ReportError(cx, "Cannot export a JavaClass object's properties");
        return JS_FALSE;

    default:
        return JS_TRUE;
    }
}

/*
 * Implement the JavaScript instanceof operator for JavaClass objects by using
 * the equivalent Java instanceof operation.
 */
static JSBool
JavaClass_hasInstance(JSContext *cx, JSObject *obj, jsval candidate_jsval,
                      JSBool *has_instancep)
{
    JavaClassDescriptor *class_descriptor;
    JavaObjectWrapper *java_wrapper;
    JSClass *js_class;
    JSBool has_instance;
    JSObject *candidate_obj;
    jclass java_class;
    jobject java_obj;
    JNIEnv *jEnv;
    
    has_instance = JS_FALSE;
    class_descriptor = JS_GetPrivate(cx, obj);
    if (!class_descriptor) {
        JS_ReportError(cx, "illegal operation on JavaClass prototype object");
        return JS_FALSE;
    }

    /*
     * Make sure that the thing to the left of the instanceof operator is a
     * Java object.
     */
    if (!JSVAL_IS_OBJECT(candidate_jsval))
        goto done;
    candidate_obj = JSVAL_TO_OBJECT(candidate_jsval);
    js_class = JS_GetClass(candidate_obj);
    if ((js_class != &JavaObject_class) && (js_class != &JavaArray_class))
        goto done;

    java_class = class_descriptor->java_class;
    java_wrapper = JS_GetPrivate(cx, candidate_obj);
    if (!java_wrapper) {
        JS_ReportError(cx, "illegal operation on prototype object");
        return JS_FALSE;
    }
    java_obj = java_wrapper->java_obj;
    /* Get JNI pointer */
    jsj_MapJSContextToJSJThread(cx, &jEnv);
    has_instance = (*jEnv)->IsInstanceOf(jEnv, java_obj, java_class);

done:
    *has_instancep = has_instance;
    return JS_TRUE;
}

JSObjectOps JavaClass_ops = {
    /* Mandatory non-null function pointer members. */
    NULL,                       /* newObjectMap */
    NULL,                       /* destroyObjectMap */
    JavaClass_lookupProperty,
    JavaClass_defineProperty,
    JavaClass_getPropertyById,  /* getProperty */
    JavaClass_setPropertyById,  /* setProperty */
    JavaClass_getAttributes,
    JavaClass_setAttributes,
    JavaClass_deleteProperty,
    JavaClass_defaultValue,
    JavaClass_newEnumerate,
    JavaClass_checkAccess,

    /* Optionally non-null members start here. */
    NULL,                       /* thisObject */
    NULL,                       /* dropProperty */
    jsj_JavaConstructorWrapper, /* call */
    jsj_JavaConstructorWrapper, /* construct */
    NULL,                       /* xdrObject */
    JavaClass_hasInstance,      /* hasInstance */
};

static JSObjectOps *
JavaClass_getObjectOps(JSContext *cx, JSClass *clazz)
{
    return &JavaClass_ops;
}

JSClass JavaClass_class = {
    "JavaClass", JSCLASS_HAS_PRIVATE,
    NULL, NULL, NULL, NULL,
    NULL, NULL, JavaClass_convert, JavaClass_finalize,
    JavaClass_getObjectOps,
};

static JSObject *
jsj_new_JavaClass(JSContext *cx, JNIEnv *jEnv, JSObject* parent_obj,
                  JavaClassDescriptor *class_descriptor)
{
    JSObject *JavaClass_obj;

    JavaClass_obj = JS_NewObject(cx, &JavaClass_class, 0, parent_obj);
    if (!JavaClass_obj)
        return NULL;

    JS_SetPrivate(cx, JavaClass_obj, (void *)class_descriptor);

#ifdef DEBUG
    /*    printf("JavaClass \'%s\' created\n", class_descriptor->name); */
#endif

    return JavaClass_obj;
}

JSObject *
jsj_define_JavaClass(JSContext *cx, JNIEnv *jEnv, JSObject* parent_obj,
                     const char *simple_class_name,
                     jclass java_class)
{
    JavaClassDescriptor *class_descriptor;
    JSObject *JavaClass_obj;

    class_descriptor = jsj_GetJavaClassDescriptor(cx, jEnv, java_class);
    if (!class_descriptor)
        return NULL;

    JavaClass_obj = jsj_new_JavaClass(cx, jEnv, parent_obj, class_descriptor);
    if (!JavaClass_obj)
        return NULL;

    if (!JS_DefineProperty(cx, parent_obj, simple_class_name,
                           OBJECT_TO_JSVAL(JavaClass_obj), 0, 0,
                           JSPROP_PERMANENT|JSPROP_READONLY|JSPROP_ENUMERATE))
        return NULL;
    return JavaClass_obj;
}


/*
 * The getClass() native JS method is defined as a property of the global
 * object.  Given a JavaObject it returns the corresponding JavaClass.  This
 * is useful for accessing static methods and fields.
 *
 *    js> getClass(new java.lang.String("foo"))
 *    [JavaClass java.lang.String]
 */
static JSBool
getClass(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval)
{
    JSObject *obj_arg, *JavaClass_obj;
    JavaObjectWrapper *java_wrapper;
    JavaClassDescriptor *class_descriptor;
    JNIEnv *jEnv;

    jsj_MapJSContextToJSJThread(cx, &jEnv);
    if (!jEnv)
        return JS_FALSE;

    if (argc != 1 ||
	!JSVAL_IS_OBJECT(argv[0]) ||
	!(obj_arg = JSVAL_TO_OBJECT(argv[0])) ||
	(!JS_InstanceOf(cx, obj_arg, &JavaObject_class, 0) &&
         !JS_InstanceOf(cx, obj_arg, &JavaArray_class, 0))) {
        JS_ReportError(cx, "getClass expects a Java object argument");
	return JS_FALSE;
    }

    java_wrapper = JS_GetPrivate(cx, obj_arg);
    if (!java_wrapper) {
        JS_ReportError(cx, "getClass called on prototype object");
        return JS_FALSE;
    }

    class_descriptor = java_wrapper->class_descriptor;

    JavaClass_obj = jsj_new_JavaClass(cx, jEnv, NULL, class_descriptor);
    if (!JavaClass_obj)
        return JS_FALSE;
    *rval = OBJECT_TO_JSVAL(JavaClass_obj);
    return JS_TRUE;
}

extern PR_IMPORT_DATA(JSObjectOps) js_ObjectOps;

JSBool
jsj_init_JavaClass(JSContext *cx, JSObject *global_obj)
{
    JavaClass_ops.newObjectMap = js_ObjectOps.newObjectMap;
    JavaClass_ops.destroyObjectMap = js_ObjectOps.destroyObjectMap;
    
    /* Define JavaClass class */
    if (!JS_InitClass(cx, global_obj, 0, &JavaClass_class, 0, 0, 0, 0, 0, 0))
        return JS_FALSE;

    if (!JS_DefineFunction(cx, global_obj, "getClass", getClass, 0,
                           JSPROP_READONLY))
        return JS_FALSE;

    return jsj_InitJavaClassReflectionsTable();
}

