
/*
 * Janino - An embedded Java[TM] compiler
 *
 * Copyright (c) 2001-2010, Arno Unkrig
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
 * following conditions are met:
 *
 *    1. Redistributions of source code must retain the above copyright notice, this list of conditions and the
 *       following disclaimer.
 *    2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
 *       following disclaimer in the documentation and/or other materials provided with the distribution.
 *    3. The name of the author may not be used to endorse or promote products derived from this software without
 *       specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
 * THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

package org.codehaus.janino;

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

import org.codehaus.commons.compiler.CompileException;
import org.codehaus.commons.compiler.Location;

/**
 * Wraps a {@link java.lang.Class} in an {@link org.codehaus.janino.IClass}.
 */
@SuppressWarnings({ "rawtypes", "unchecked" })
class ReflectionIClass extends IClass {
    private final Class        clazz;
    private final IClassLoader iClassLoader;

    /**
     * @param iClassLoader required to load other {@link IClass}es on <code>get...()</code>.
     */
    public
    ReflectionIClass(Class clazz, IClassLoader iClassLoader) {
        this.clazz        = clazz;
        this.iClassLoader = iClassLoader;
    }

    @Override protected IConstructor[]
    getDeclaredIConstructors2() {
        Constructor[]  constructors = this.clazz.getDeclaredConstructors();
        IConstructor[] result       = new IConstructor[constructors.length];
        for (int i = 0; i < constructors.length; ++i) {
            result[i] = new ReflectionIConstructor(constructors[i]);
        }
        return result;
    }

    @Override protected IMethod[]
    getDeclaredIMethods2() {
        Method[] methods  = this.clazz.getDeclaredMethods();
        List     iMethods = new ArrayList();
        for (int i = 0; i < methods.length; ++i) {
            Method m = methods[i];

            // Formerly, the Java 5 synthetic methods were skipped here, because they are not "declared", i.e. hand-
            // written. However that turned out to be a bad idea, because with parameterized types the check that
            // all abstract methods are implemented fails.
//            if ((m.getModifiers() & Mod.SYNTHETIC) != 0) continue;

            // Wrap java.reflection.Method in an IMethod.
            iMethods.add(new ReflectionIMethod(m));
        }
        if (methods.length == 0 && this.clazz.isArray()) {
            iMethods.add(new IMethod() {

                // CHECKSTYLE LineLength:OFF
                @Override public String   getName()             { return "clone"; }
                @Override public IClass   getReturnType()       { return ReflectionIClass.this.iClassLoader.JAVA_LANG_OBJECT; }
                @Override public boolean  isAbstract()          { return false; }
                @Override public boolean  isStatic()            { return false; }
                @Override public Access   getAccess()           { return Access.PUBLIC; }
                @Override public IClass[] getParameterTypes()   { return new IClass[0]; }
                @Override public IClass[] getThrownExceptions() { return new IClass[0]; }
                // CHECKSTYLE LineLength:ON

            });
        }
        return (IMethod[]) iMethods.toArray(new IMethod[iMethods.size()]);
    }

    @Override protected IField[]
    getDeclaredIFields2() {
        Field[]  fields = this.clazz.getDeclaredFields();
        IField[] result = new IField[fields.length];
        for (int i = 0; i < fields.length; ++i) {
            result[i] = new ReflectionIField(fields[i]);
        }
        return result;
    }

    @Override protected IClass[]
    getDeclaredIClasses2() { return this.classesToIClasses(this.clazz.getDeclaredClasses()); }

    @Override protected IClass
    getDeclaringIClass2() {
        Class declaringClass = this.clazz.getDeclaringClass();
        if (declaringClass == null) return null;
        return this.classToIClass(declaringClass);
    }

    @Override protected IClass
    getOuterIClass2() throws CompileException {
        if (Modifier.isStatic(this.clazz.getModifiers())) return null;
        return this.getDeclaringIClass();
    }

    @Override protected IClass
    getSuperclass2() {
        Class superclass = this.clazz.getSuperclass();
        return superclass == null ? null : this.classToIClass(superclass);
    }

    @Override protected IClass[]
    getInterfaces2() {
        return this.classesToIClasses(this.clazz.getInterfaces());
    }

    @Override protected String
    getDescriptor2() {
        return Descriptor.fromClassName(this.clazz.getName());
    }

    @Override public Access  getAccess()   { return ReflectionIClass.modifiers2Access(this.clazz.getModifiers()); }
    @Override public boolean isFinal()     { return Modifier.isFinal(this.clazz.getModifiers()); }
    @Override public boolean isInterface() { return this.clazz.isInterface(); }
    @Override public boolean isAbstract()  { return Modifier.isAbstract(this.clazz.getModifiers()); }
    @Override public boolean isArray()     { return this.clazz.isArray(); }

    @Override protected IClass
    getComponentType2() {
        Class componentType = this.clazz.getComponentType();
        return componentType == null ? null : this.classToIClass(componentType);
    }

    @Override public boolean
    isPrimitive() { return this.clazz.isPrimitive(); }

    @Override public boolean
    isPrimitiveNumeric() {
        return (
            this.clazz == byte.class
            || this.clazz == short.class
            || this.clazz == int.class
            || this.clazz == long.class
            || this.clazz == char.class
            || this.clazz == float.class
            || this.clazz == double.class
        );
    }

    public Class
    getClazz() { return this.clazz; }

    /**
     * @return E.g. "int", "int[][]", "pkg1.pkg2.Outer$Inner[]"
     */
    @Override public String
    toString() {
        int   brackets = 0;
        Class c        = this.clazz;
        while (c.isArray()) {
            ++brackets;
            c = c.getComponentType();
        }
        String s = c.getName();
        while (brackets-- > 0) s += "[]";
        return s;
    }

    private
    class ReflectionIConstructor extends IConstructor {

        public ReflectionIConstructor(Constructor constructor) { this.constructor = constructor; }

        // Implement IMember.
        @Override public Access
        getAccess() {
            int mod = this.constructor.getModifiers();
            return ReflectionIClass.modifiers2Access(mod);
        }

        // Implement "IConstructor".
        @Override public IClass[]
        getParameterTypes() throws CompileException {
            IClass[] parameterTypes = ReflectionIClass.this.classesToIClasses(this.constructor.getParameterTypes());

            // The JAVADOC of java.lang.reflect.Constructor does not document it, but
            // "getParameterTypes()" includes the synthetic "enclosing instance" parameter.
            IClass outerClass = ReflectionIClass.this.getOuterIClass();
            if (outerClass != null) {
                if (parameterTypes.length < 1) {
                    throw new CompileException(
                        "Constructor \"" + this.constructor + "\" lacks synthetic enclosing instance parameter",
                        null
                    );
                }
                if (parameterTypes[0] != outerClass) {
                    throw new CompileException((
                        "Enclosing instance parameter of constructor \""
                        + this.constructor
                        + "\" has wrong type -- \""
                        + parameterTypes[0]
                        + "\" vs. \""
                        + outerClass
                        + "\""
                    ), null);
                }
                IClass[] tmp = new IClass[parameterTypes.length - 1];
                System.arraycopy(parameterTypes, 1, tmp, 0, tmp.length);
                parameterTypes = tmp;
            }

            return parameterTypes;
        }

        @Override public String
        getDescriptor() {
            Class[]  parameterTypes       = this.constructor.getParameterTypes();
            String[] parameterDescriptors = new String[parameterTypes.length];
            for (int i = 0; i < parameterDescriptors.length; ++i) {
                parameterDescriptors[i] = Descriptor.fromClassName(parameterTypes[i].getName());
            }
            return new MethodDescriptor(parameterDescriptors, Descriptor.VOID).toString();
        }

        @Override public IClass[]
        getThrownExceptions() {
            return ReflectionIClass.this.classesToIClasses(this.constructor.getExceptionTypes());
        }

        final Constructor constructor;
    }
    private
    class ReflectionIMethod extends IMethod {

        public ReflectionIMethod(Method method) { this.method = method; }

        // Implement IMember.
        @Override public Access
        getAccess() { return ReflectionIClass.modifiers2Access(this.method.getModifiers()); }

        // Implement "IMethod".
        @Override public String
        getName() { return this.method.getName(); }

        @Override public IClass[]
        getParameterTypes() { return ReflectionIClass.this.classesToIClasses(this.method.getParameterTypes()); }

        @Override public boolean
        isStatic() { return Modifier.isStatic(this.method.getModifiers()); }

        @Override public boolean
        isAbstract() { return Modifier.isAbstract(this.method.getModifiers()); }

        @Override public IClass
        getReturnType() { return ReflectionIClass.this.classToIClass(this.method.getReturnType()); }

        @Override public IClass[]
        getThrownExceptions() { return ReflectionIClass.this.classesToIClasses(this.method.getExceptionTypes()); }

        final Method method;
    }

    private
    class ReflectionIField extends IField {

        public ReflectionIField(Field field) { this.field = field; }

        // Implement IMember.
        @Override public Access
        getAccess() { return ReflectionIClass.modifiers2Access(this.field.getModifiers()); }

        // Implement "IField".

        @Override public String
        getName() { return this.field.getName(); }

        @Override public boolean
        isStatic() { return Modifier.isStatic(this.field.getModifiers()); }

        @Override public IClass
        getType() { return ReflectionIClass.this.classToIClass(this.field.getType()); }

        @Override public String
        toString() {
            return Descriptor.toString(this.getDeclaringIClass().getDescriptor()) + "." + this.getName();
        }

        /**
         * This implementation of {@link IClass.IField#getConstantValue()} is
         * not completely correct:
         * <ul>
         *   <li>
         *   It treats non-static fields as non-constant
         *   <li>
         *   Even fields with a <i>non-constant</i> initializer are identified
         *   as constant. (The value of that field may be different in a
         *   different JVM instance -- the classical example is
         *   {@link java.io.File#separator}.)
         * </ul>
         */
        @Override public Object
        getConstantValue() throws CompileException {
            int   mod   = this.field.getModifiers();
            Class clazz = this.field.getType();
            if (
                Modifier.isStatic(mod)
                && Modifier.isFinal(mod)
                && (clazz.isPrimitive() || clazz == String.class)
            ) {
                try {
                    return this.field.get(null);
                } catch (IllegalAccessException ex) {
                    throw new CompileException( // SUPPRESS CHECKSTYLE AvoidHidingCause
                        "Field \"" + this.field.getName() + "\" is not accessible",
                        (Location) null
                    );
                }
            }
            return NOT_CONSTANT;
        }

        final Field field;
    }

    /**
     * Load {@link Class} through {@link IClassLoader} to
     * ensure unique {@link IClass}es.
     */
    private IClass
    classToIClass(Class c) {
        IClass iClass;
        try {
            iClass = this.iClassLoader.loadIClass(Descriptor.fromClassName(c.getName()));
        } catch (ClassNotFoundException ex) {
            throw new JaninoRuntimeException("Loading IClass \"" + c.getName() + "\": " + ex);
        }
        if (iClass == null) {
            throw new JaninoRuntimeException("Cannot load class \"" + c.getName() + "\" through the given ClassLoader");
        }
        return iClass;
    }

    /**
     * @see #classToIClass(Class)
     */
    private IClass[]
    classesToIClasses(Class[] cs) {
        IClass[] result = new IClass[cs.length];
        for (int i = 0; i < cs.length; ++i) result[i] = this.classToIClass(cs[i]);
        return result;
    }

    private static Access
    modifiers2Access(int modifiers) {
        return (
            Modifier.isPrivate(modifiers)   ? Access.PRIVATE   :
            Modifier.isProtected(modifiers) ? Access.PROTECTED :
            Modifier.isPublic(modifiers)    ? Access.PUBLIC    :
            Access.DEFAULT
        );
    }
}
