/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.felix.scr.impl.inject.field;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.security.AccessController;
import java.security.PrivilegedAction;

import org.apache.felix.scr.impl.inject.internal.ClassUtils;
import org.apache.felix.scr.impl.logger.ComponentLogger;
import org.apache.felix.scr.impl.logger.InternalLogger.Level;

/**
 * Utility methods for handling field injection.
 */
public class FieldUtils {

	/**
	 * Return type for {@link FieldUtils#searchField(Class, String, ComponentLogger)}
	 */
    public static final class FieldSearchResult
	{
		public final Field field;
		public final boolean usable;

		public FieldSearchResult(final Field f, final boolean usable)
		{
			this.field = f;
			this.usable = usable;
		}
	}

    /**
     * Searches the field named {@code fieldName} in the given
     * {@code targetClass}. If the target class has no acceptable field
     * the class hierarchy is traversed until a field is found or the root
     * of the class hierarchy is reached without finding a field.
     * <p>
     * If an unexpected error occurs while searching, {@code null} is
     * returned. In all other cases a {@code FieldSearchResult} is
     * returned. If no field is found, {@code FieldSearchResult#field}
     * is set to {@code null}. If the field is found, but not usable
     * (e.g. due to visibility restrictions), {@code FieldSearchResult#usable}
     * is set to {@code false}.
     *
     * @param targetClass The class of the component
     * @param fieldName The name of the field
     * @param logger A logger to log errors / problems
     * @return A field search result or {@code null} if an unexpected
     *         error occurred.
     */
    public static FieldSearchResult searchField( final Class<?> componentClass,
    		final String fieldName,
    		final ComponentLogger logger )
    {
        final ClassLoader targetClasslLoader = componentClass.getClassLoader();
        final String targetPackage = ClassUtils.getPackageName( componentClass );
        Class<?> theClass = componentClass;
        boolean acceptPrivate = true;
        boolean acceptPackage = true;
        while (true)
        {

            logger.log(Level.DEBUG,
                "Locating field {0} in class {1}", null, fieldName, theClass.getName() );

            try
            {
	            final FieldSearchResult result = getField( componentClass, theClass, fieldName, acceptPrivate, acceptPackage, logger );
	            if ( result != null )
	            {
	                return result;
	            }
            }
            catch ( final InvocationTargetException ex )
            {
                logger.log(Level.ERROR,
                    "Field {0} cannot be found in component class {1}. The field will be ignored.",
                        ex.getTargetException(),
                        fieldName, componentClass.getName()  );
                return null;
            }

            // if we get here, we have no field, so check the super class
            theClass = theClass.getSuperclass();
            if ( theClass == null )
            {
                break;
            }

            // super class field check ignores private fields and accepts
            // package fields only if in the same package and package
            // fields are (still) allowed
            acceptPackage &= targetClasslLoader == theClass.getClassLoader()
                && targetPackage.equals( ClassUtils.getPackageName( theClass ) );

            // private fields will not be accepted any more in super classes
            acceptPrivate = false;
        }

        // nothing found
        logger.log(Level.ERROR,
            "Field {0} cannot be found in component class {1}. The field will be ignored.",
            null,
                      fieldName, componentClass.getName() );
        return new FieldSearchResult(null, false);
    }

    /**
     * Finds the field named {@code fieldName} field in the given
     * {@code targetClass}. If the target class has no acceptable field
     * the class hierarchy is traversed until a field is found or the root
     * of the class hierarchy is reached without finding a field.
     *
     * @param componentClass The class of the component (for logging)
     * @param targetClass The class in which to look for the method
     * @param fieldName The name of the field
     * @param acceptPrivate {@code true} if private fields should be
     *      considered.
     * @param acceptPackage {@code true} if package private fields should
     *      be considered.
     * @param logger For error logging
     * @return If the field is found a {@code FieldSearchResult} is returned.
     *         If the field is not found, {@code null} is returned.
     * @throws InvocationTargetException If an unexpected Throwable is caught
     *      trying to find the requested field.
     */
    private static FieldSearchResult getField( final Class<?> componentClass,
    		final Class<?> targetClass,
    		final String fieldName,
            final boolean acceptPrivate,
            final boolean acceptPackage,
            final ComponentLogger logger )
    throws InvocationTargetException
    {
        try
        {
            // find the declared field in this class
            final Field field = targetClass.getDeclaredField( fieldName );

            // accept public and protected fields only and ensure accessibility
            return accept( componentClass, field, acceptPrivate, acceptPackage, logger );
        }
        catch ( NoSuchFieldException nsfe )
        {
            // thrown if no field is declared with the given name and
            // parameters
            if (logger.isLogEnabled(Level.DEBUG))
            {
                logger.log(Level.DEBUG, "Declared Field {0}.{1} not found", null,
                    targetClass.getName(), fieldName);
            }
        }
        catch ( Throwable throwable )
        {
            // unexpected problem accessing the field, don't let everything
            // blow up in this situation, just throw a declared exception
            throw new InvocationTargetException( throwable, "Unexpected problem trying to get field " + fieldName );
        }

        // caught and ignored exception, assume no field and continue search
        return null;
    }

    /**
     * This method checks whether the found field is acceptable (= usable)
     * for the component instance.
     * It returns a {@code FieldSearchResult} with the usable flag set to
     * {@code true} if the field is not static and
     * <ul>
     * <li>Is public or protected</li>
     * <li>Is private and {@code acceptPrivate} is {@code true}</li>
     * <li>Is package private and {@code acceptPackage} is {@code true}</li>
     * </ul>
     * <p>
     * If the field is usable, this method makes the field accessible.
     * <p>
     * If the field is not usable, a {@code FieldSearchResult} with the usable
     * flag set to {@code false} is returned and an error is logged with
     * the provided logger.
     *
     * @param componentClass The class of the component,.
     * @param field The field to check
     * @param acceptPrivate Whether a private field is acceptable
     * @param acceptPackage Whether a package private field is acceptable
     * @param logger The logger for error logging
     * @return A field search result, this is never {@code null}
     */
    private static FieldSearchResult accept( final Class<?> componentClass,
    		final Field field,
            final boolean acceptPrivate,
            final boolean acceptPackage,
            final ComponentLogger logger)
    {
        // check modifiers now
        final int mod = field.getModifiers();

        // static fields
        if ( Modifier.isStatic( mod ) )
        {
            logger.log(Level.ERROR, "Field {0} must not be static", null,
                toString(componentClass, field));
            return new FieldSearchResult(field, false);
        }

        // accept public and protected fields
        if ( Modifier.isPublic( mod ) || Modifier.isProtected( mod ) )
        {
            setAccessible( field );
            return new FieldSearchResult(field, true);
        }

        // accept private if accepted
        if ( Modifier.isPrivate( mod ) )
        {
            if ( acceptPrivate )
            {
                setAccessible( field );
                return new FieldSearchResult(field, true);
            }

        } else {

        	// accept default (package)
        	if ( acceptPackage )
        	{
        		setAccessible( field );
        		return new FieldSearchResult(field, true);
        	}
        }

        // else don't accept
        // the method would fit the requirements but is not acceptable
        logger.log(Level.ERROR,
                "findField: Suitable but non-accessible field {0}", null, toString(componentClass, field));
        return new FieldSearchResult(field, false);
    }

    /**
     * Return a string representation of the field
     * @param componentClass The component class
     * @param field The field
     * @return A string representation of the field
     */
    public static String toString(final Class<?> componentClass,
    		final Field field)
    {
    	if ( componentClass.getName().equals(field.getDeclaringClass().getName()) )
    	{
    		return field.getName() + " in component class " + componentClass.getName();
    	}
    	return field.getName() + " in class " + field.getDeclaringClass().getName() + ", subclass of component class " + componentClass.getName();
    }

    /**
     * Make the field accessible
     * @param field The field
     */
    private static void setAccessible(final Field field)
    {
        AccessController.doPrivileged( new PrivilegedAction<Object>()
        {
            @Override
            public Object run()
            {
                field.setAccessible( true );
                return null;
            }
        } );
    }

    /**
     * Set the field, type etc.
     * @param f The field
     * @param component The component instance
     * @param value The value to set
     * @param logger The logger
     */
	public static void setField( final Field f,
    		final Object component,
    		final Object value,
    		final ComponentLogger logger )
    {
        try
        {
            f.set(component, value);
        }
        catch ( final IllegalArgumentException iae )
        {
            logger.log(Level.ERROR, "Field {0} can't be set", iae, f.getName());
        }
        catch ( final IllegalAccessException iae )
        {
            logger.log(Level.ERROR, "Field {0} can't be set", iae, f.getName());
        }
    }
}
