/*
 * The MIT License
 *
 * Copyright (c) 2004-2010, Oracle Corporation
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package hudson.util;

import org.apache.commons.beanutils.PropertyUtils;
import org.kohsuke.stapler.ClassDescriptor;

import java.beans.PropertyDescriptor;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.AbstractList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import edu.umd.cs.findbugs.annotations.CheckForNull;

/**
 * Utility code for reflection.
 *
 * @author Kohsuke Kawaguchi
 * @since 1.351
 */
public class ReflectionUtils extends org.springframework.util.ReflectionUtils {
    /**
     * Finds a public method of the given name, regardless of its parameter definitions,
     */
    public static Method getPublicMethodNamed(Class c, String methodName) {
        for( Method m : c.getMethods() )
            if(m.getName().equals(methodName))
                return m;
        return null;
    }

    /**
     * Returns an object-oriented view of parameters of each type.
     */
    public static List<Parameter> getParameters(Method m) {
        return new MethodInfo(m);
    }

    public static Object getPublicProperty(Object o, String p) throws InvocationTargetException, NoSuchMethodException, IllegalAccessException {
        PropertyDescriptor pd = PropertyUtils.getPropertyDescriptor(o, p);
        if(pd==null) {
            // field?
            try {
                Field f = o.getClass().getField(p);
                return f.get(o);
            } catch (NoSuchFieldException e) {
                throw new IllegalArgumentException("No such property "+p+" on "+o.getClass());
            }
        } else {
            return PropertyUtils.getProperty(o, p);
        }
    }

    /**
     * Most reflection operations give us properties of parameters in a batch,
     * so we use this object to store them, then {@link Parameter} will created
     * more object-oriented per-parameter view.
     */
    private static final class MethodInfo extends AbstractList<Parameter> {
        private final Method method;
        private final Class<?>[] types;
        private Type[] genericTypes;
        private Annotation[][] annotations;
        private String[] names;

        private MethodInfo(Method method) {
            this.method = method;
            types = method.getParameterTypes();
        }

        @Override
        public Parameter get(int index) {
            return new Parameter(this,index);
        }

        @Override
        public int size() {
            return types.length;
        }

        public Type[] genericTypes() {
            if (genericTypes==null)
                genericTypes = method.getGenericParameterTypes();
            return genericTypes;
        }

        public Annotation[][] annotations() {
            if (annotations==null)
                annotations = method.getParameterAnnotations();
            return annotations;
        }

        public String[] names() {
            if (names==null)
                names = ClassDescriptor.loadParameterNames(method);
            return names;
        }
    }

    public static final class Parameter implements AnnotatedElement {
        private final MethodInfo parent;
        private final int index;

        public Parameter(MethodInfo parent, int index) {
            this.parent = parent;
            this.index = index;
        }

        /**
         * 0-origin index of this parameter.
         */
        public int index() {
            return index;
        }

        /**
         * Gets the type of this parameter.
         */
        public Class<?> type() {
            return parent.types[index];
        }

        /**
         * Gets the unerased generic type of this parameter.
         */
        public Type genericType() {
            return parent.genericTypes()[index];
        }

        /**
         * Gets all the annotations on this parameter.
         */
        public Annotation[] annotations() {
            return parent.annotations()[index];
        }

        /**
         * Gets the specified annotation on this parameter or null.
         */
        public <A extends Annotation> A annotation(Class<A> type) {
            for (Annotation a : annotations())
                if (a.annotationType()==type)
                    return type.cast(a);
            return null;
        }

        /**
         * Name of this parameter.
         *
         * If unknown, this method returns null.
         */
        public String name() {
            String[] names = parent.names();
            if (index<names.length)
                return names[index];
            return null;
        }

        @Override
        public boolean isAnnotationPresent(Class<? extends Annotation> type) {
            return annotation(type)!=null;
        }

        @Override
        public <T extends Annotation> T getAnnotation(Class<T> type) {
            return annotation(type);
        }

        @Override
        public Annotation[] getAnnotations() {
            return annotations();
        }

        @Override
        public Annotation[] getDeclaredAnnotations() {
            return annotations();
        }
    }

    /**
     * Given the primitive type, returns the VM default value for that type in a boxed form.
     * @return null unless {@link Class#isPrimitive}
     */
    public static @CheckForNull Object getVmDefaultValueForPrimitiveType(Class<?> type) {
        return defaultPrimitiveValue.get(type);
    }

    // TODO the version in org.kohsuke.stapler is incomplete
    private static final Map<Class<?>, Object> defaultPrimitiveValue = new HashMap<>();
    static {
        defaultPrimitiveValue.put(boolean.class, false);
        defaultPrimitiveValue.put(char.class, '\0');
        defaultPrimitiveValue.put(byte.class, (byte) 0);
        defaultPrimitiveValue.put(short.class, (short) 0);
        defaultPrimitiveValue.put(int.class, 0);
        defaultPrimitiveValue.put(long.class, 0L);
        defaultPrimitiveValue.put(float.class, (float) 0);
        defaultPrimitiveValue.put(double.class, (double) 0);
        defaultPrimitiveValue.put(void.class, null); // FWIW
    }
}
