/* -*- Mode: java; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
 *
 * ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0
 *
 * 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 Rhino code, released
 * May 6, 1999.
 *
 * The Initial Developer of the Original Code is
 * Netscape Communications Corporation.
 * Portions created by the Initial Developer are Copyright (C) 1997-1999
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *   Norris Boyd
 *   Frank Mitchell
 *   Mike Shaver
 *   Kurt Westerfeld
 *   Kemal Bayram
 *   Ulrike Mueller <umueller@demandware.com>
 *
 * Alternatively, the contents of this file may be used under the terms of
 * the GNU General Public License Version 2 or later (the "GPL"), in which
 * case the provisions of the GPL are applicable instead of those above. If
 * you wish to allow use of your version of this file only under the terms of
 * the GPL and not to allow others to use your version of this file under the
 * MPL, indicate your decision by deleting the provisions above and replacing
 * them with the notice and other provisions required by the GPL. If you do
 * not delete the provisions above, a recipient may use your version of this
 * file under either the MPL or the GPL.
 *
 * ***** END LICENSE BLOCK ***** */

package org.mozilla.javascript;

import java.lang.reflect.*;
import java.util.Map;

/**
 * This class reflects Java classes into the JavaScript environment, mainly
 * for constructors and static members.  We lazily reflect properties,
 * and currently do not guarantee that a single j.l.Class is only
 * reflected once into the JS environment, although we should.
 * The only known case where multiple reflections
 * are possible occurs when a j.l.Class is wrapped as part of a
 * method return or property access, rather than by walking the
 * Packages/java tree.
 *
 * @author Mike Shaver
 * @see NativeJavaArray
 * @see NativeJavaObject
 * @see NativeJavaPackage
 */

public class NativeJavaClass extends NativeJavaObject implements Function
{
    static final long serialVersionUID = -6460763940409461664L;

    // Special property for getting the underlying Java class object.
    static final String javaClassPropertyName = "__javaObject__";

    public NativeJavaClass() {
    }

    public NativeJavaClass(Scriptable scope, Class<?> cl) {
        this.parent = scope;
        this.javaObject = cl;
        initMembers();
    }

    @Override
    protected void initMembers() {
        Class<?> cl = (Class<?>)javaObject;
        members = JavaMembers.lookupClass(parent, cl, cl, false);
        staticFieldAndMethods
            = members.getFieldAndMethodsObjects(this, cl, true);
    }

    @Override
    public String getClassName() {
        return "JavaClass";
    }

    @Override
    public boolean has(String name, Scriptable start) {
        return members.has(name, true) || javaClassPropertyName.equals(name);
    }

    @Override
    public Object get(String name, Scriptable start) {
        // When used as a constructor, ScriptRuntime.newObject() asks
        // for our prototype to create an object of the correct type.
        // We don't really care what the object is, since we're returning
        // one constructed out of whole cloth, so we return null.
        if (name.equals("prototype"))
            return null;

         if (staticFieldAndMethods != null) {
            Object result = staticFieldAndMethods.get(name);
            if (result != null)
                return result;
        }

        if (members.has(name, true)) {
            return members.get(this, name, javaObject, true);
        }
        
        if (javaClassPropertyName.equals(name)) {
            Context cx = Context.getContext();
            Scriptable scope = ScriptableObject.getTopLevelScope(start);
            return cx.getWrapFactory().wrap(cx, scope, javaObject, 
                                            ScriptRuntime.ClassClass);
        }
        
        // experimental:  look for nested classes by appending $name to
        // current class' name.
        Class<?> nestedClass = findNestedClass(getClassObject(), name);
        if (nestedClass != null) {
            NativeJavaClass nestedValue = new NativeJavaClass
                (ScriptableObject.getTopLevelScope(this), nestedClass);
            nestedValue.setParentScope(this);
            return nestedValue;
        }
        
        throw members.reportMemberNotFound(name);
    }

    @Override
    public void put(String name, Scriptable start, Object value) {
        members.put(this, name, javaObject, value, true);
    }

    @Override
    public Object[] getIds() {
        return members.getIds(true);
    }

    public Class<?> getClassObject() {
        return (Class<?>) super.unwrap();
    }

    @Override
    public Object getDefaultValue(Class<?> hint) {
        if (hint == null || hint == ScriptRuntime.StringClass)
            return this.toString();
        if (hint == ScriptRuntime.BooleanClass)
            return Boolean.TRUE;
        if (hint == ScriptRuntime.NumberClass)
            return ScriptRuntime.NaNobj;
        return this;
    }

    public Object call(Context cx, Scriptable scope, Scriptable thisObj,
                       Object[] args)
    {
        // If it looks like a "cast" of an object to this class type,
        // walk the prototype chain to see if there's a wrapper of a
        // object that's an instanceof this class.
        if (args.length == 1 && args[0] instanceof Scriptable) {
            Class<?> c = getClassObject();
            Scriptable p = (Scriptable) args[0];
            do {
                if (p instanceof Wrapper) {
                    Object o = ((Wrapper) p).unwrap();
                    if (c.isInstance(o))
                        return p;
                }
                p = p.getPrototype();
            } while (p != null);
        }
        return construct(cx, scope, args);
    }

    public Scriptable construct(Context cx, Scriptable scope, Object[] args)
    {
        Class<?> classObject = getClassObject();
        int modifiers = classObject.getModifiers();
        if (! (Modifier.isInterface(modifiers) ||
               Modifier.isAbstract(modifiers)))
        {
            MemberBox[] ctors = members.ctors;
            int index = NativeJavaMethod.findFunction(cx, ctors, args);
            if (index < 0) {
                String sig = NativeJavaMethod.scriptSignature(args);
                throw Context.reportRuntimeError2(
                    "msg.no.java.ctor", classObject.getName(), sig);
            }

            // Found the constructor, so try invoking it.
            return constructSpecific(cx, scope, args, ctors[index]);
        } else {
            Scriptable topLevel = ScriptableObject.getTopLevelScope(this);
            String msg = "";
            try {
                // trying to construct an interface; use JavaAdapter to
                // construct a new class on the fly that implements this
                // interface.
                Object v = topLevel.get("JavaAdapter", topLevel);
                if (v != NOT_FOUND) {
                    Function f = (Function) v;
                    Object[] adapterArgs = { this, args[0] };
                    return f.construct(cx, topLevel,adapterArgs);
                }
            } catch (Exception ex) {
                // fall through to error
                String m = ex.getMessage();
                if (m != null)
                    msg = m;
            }
            throw Context.reportRuntimeError2(
                "msg.cant.instantiate", msg, classObject.getName());
        }
    }

    static Scriptable constructSpecific(Context cx, Scriptable scope,
                                        Object[] args, MemberBox ctor)
    {
        Scriptable topLevel = ScriptableObject.getTopLevelScope(scope);
        Class<?>[] argTypes = ctor.argTypes;
      
        if (ctor.vararg) {
            // marshall the explicit parameter
            Object[] newArgs = new Object[argTypes.length];
            for (int i = 0; i < argTypes.length-1; i++) {
                newArgs[i] = Context.jsToJava(args[i], argTypes[i]);
            }
            
            Object varArgs;
            
            // Handle special situation where a single variable parameter
            // is given and it is a Java or ECMA array.
            if (args.length == argTypes.length &&
                (args[args.length-1] == null ||
                 args[args.length-1] instanceof NativeArray ||
                 args[args.length-1] instanceof NativeJavaArray))
            {
                // convert the ECMA array into a native array
                varArgs = Context.jsToJava(args[args.length-1], 
                                           argTypes[argTypes.length - 1]);
            } else {            
                // marshall the variable parameter
                Class<?> componentType = argTypes[argTypes.length - 1].
                                        getComponentType();
                varArgs = Array.newInstance(componentType, 
                                            args.length - argTypes.length + 1);            
                for (int i=0; i < Array.getLength(varArgs); i++) {
                    Object value = Context.jsToJava(args[argTypes.length-1 + i],
                                                    componentType);
                    Array.set(varArgs, i, value);
                }
            }
            
            // add varargs
            newArgs[argTypes.length-1] = varArgs;
            // replace the original args with the new one
            args = newArgs;
        } else {
            Object[] origArgs = args;
            for (int i = 0; i < args.length; i++) {
                Object arg = args[i];
                Object x = Context.jsToJava(arg, argTypes[i]);
                if (x != arg) {
                    if (args == origArgs) {
                        args = origArgs.clone();
                    }
                    args[i] = x;
                }
            }
        }
        
        Object instance = ctor.newInstance(args);
        // we need to force this to be wrapped, because construct _has_
        // to return a scriptable
        return cx.getWrapFactory().wrapNewObject(cx, topLevel, instance);
    }

    @Override
    public String toString() {
        return "[JavaClass " + getClassObject().getName() + "]";
    }

    /**
     * Determines if prototype is a wrapped Java object and performs
     * a Java "instanceof".
     * Exception: if value is an instance of NativeJavaClass, it isn't
     * considered an instance of the Java class; this forestalls any
     * name conflicts between java.lang.Class's methods and the
     * static methods exposed by a JavaNativeClass.
     */
    @Override
    public boolean hasInstance(Scriptable value) {

        if (value instanceof Wrapper &&
            !(value instanceof NativeJavaClass)) {
            Object instance = ((Wrapper)value).unwrap();

            return getClassObject().isInstance(instance);
        }

        // value wasn't something we understand
        return false;
    }

    private static Class<?> findNestedClass(Class<?> parentClass, String name) {
        String nestedClassName = parentClass.getName() + '$' + name;
        ClassLoader loader = parentClass.getClassLoader();
        if (loader == null) {
            // ALERT: if loader is null, nested class should be loaded
            // via system class loader which can be different from the
            // loader that brought Rhino classes that Class.forName() would
            // use, but ClassLoader.getSystemClassLoader() is Java 2 only
            return Kit.classOrNull(nestedClassName);
        } else {
            return Kit.classOrNull(loader, nestedClassName);
        }
    }

    private Map<String,FieldAndMethods> staticFieldAndMethods;
}
