/*******************************************************************************
 * Copyright (c) 2010, 2015 Sonatype, Inc.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *    Stuart McCulloch (Sonatype, Inc.) - initial API and implementation
 *******************************************************************************/
package org.eclipse.sisu.plexus;

import java.lang.reflect.Field;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.lang.reflect.WildcardType;
import java.security.AccessController;
import java.security.PrivilegedAction;

import org.codehaus.plexus.component.configurator.ComponentConfigurationException;
import org.codehaus.plexus.component.configurator.ConfigurationListener;
import org.codehaus.plexus.component.configurator.converters.ConfigurationConverter;
import org.codehaus.plexus.component.configurator.converters.ParameterizedConfigurationConverter;
import org.codehaus.plexus.component.configurator.converters.lookup.ConverterLookup;
import org.codehaus.plexus.component.configurator.expression.ExpressionEvaluator;
import org.codehaus.plexus.configuration.PlexusConfiguration;
import org.eclipse.sisu.bean.DeclaredMembers;
import org.eclipse.sisu.bean.DeclaredMembers.View;

import com.google.inject.TypeLiteral;

/**
 * Helper class that implements low-level Plexus configuration of composite beans.
 */
public final class CompositeBeanHelper
{
    // ----------------------------------------------------------------------
    // Constants
    // ----------------------------------------------------------------------

    private static final Type[] NO_TYPES = {};

    // ----------------------------------------------------------------------
    // Implementation fields
    // ----------------------------------------------------------------------

    private final ConverterLookup lookup;

    private final ClassLoader loader;

    private final ExpressionEvaluator evaluator;

    private final ConfigurationListener listener;

    // ----------------------------------------------------------------------
    // Constructors
    // ----------------------------------------------------------------------

    public CompositeBeanHelper( final ConverterLookup lookup, final ClassLoader loader,
                                final ExpressionEvaluator evaluator, final ConfigurationListener listener )
    {
        this.lookup = lookup;
        this.loader = loader;
        this.evaluator = evaluator;
        this.listener = listener;
    }

    // ----------------------------------------------------------------------
    // Public methods
    // ----------------------------------------------------------------------

    /**
     * Calls the default "set" method on the bean; re-converts the configuration if necessary.
     * 
     * @param bean The bean being configured
     * @param defaultValue The default value
     * @param configuration The configuration
     */
    public void setDefault( final Object bean, final Object defaultValue, final PlexusConfiguration configuration )
        throws ComponentConfigurationException
    {
        final Class<?> beanType = bean.getClass();
        final Type[] paramTypeHolder = new Type[1];

        // ----------------------------------------------------------------------

        final Method setter = findMethod( beanType, paramTypeHolder, "set" );
        if ( null == setter )
        {
            throw new ComponentConfigurationException( configuration, "Cannot find default setter in " + beanType );
        }

        // ----------------------------------------------------------------------

        Object value = defaultValue;
        final TypeLiteral<?> paramType = TypeLiteral.get( paramTypeHolder[0] );
        if ( !paramType.getRawType().isInstance( value ) )
        {
            if ( configuration.getChildCount() > 0 )
            {
                throw new ComponentConfigurationException( "Basic element '" + configuration.getName()
                    + "' must not contain child elements" );
            }
            value = convertProperty( beanType, paramType.getRawType(), paramType.getType(), configuration );
        }

        // ----------------------------------------------------------------------

        if ( null != value )
        {
            try
            {
                if ( null != listener )
                {
                    listener.notifyFieldChangeUsingSetter( "", value, bean );
                }
                setter.invoke( bean, value );
            }
            catch ( final Exception e )
            {
                throw new ComponentConfigurationException( configuration, "Cannot set default", e );
            }
            catch ( final LinkageError e )
            {
                throw new ComponentConfigurationException( configuration, "Cannot set default", e );
            }
        }
    }

    /**
     * Sets a property in the bean; looks for public setter/adder method before checking fields.
     * 
     * @param bean The bean being configured
     * @param propertyName The property name
     * @param valueType The value type
     * @param configuration The configuration
     */
    public void setProperty( final Object bean, final String propertyName, final Class<?> valueType,
                             final PlexusConfiguration configuration )
        throws ComponentConfigurationException
    {
        final Class<?> beanType = bean.getClass();
        final Type[] paramTypeHolder = new Type[1];

        // ----------------------------------------------------------------------

        final String title = Character.toTitleCase( propertyName.charAt( 0 ) ) + propertyName.substring( 1 );
        Method setter = findMethod( beanType, paramTypeHolder, "set" + title );
        if ( null == setter )
        {
            setter = findMethod( beanType, paramTypeHolder, "add" + title );
        }

        // ----------------------------------------------------------------------

        Throwable problem = null;
        Object value = null;

        if ( null != setter )
        {
            try
            {
                final TypeLiteral<?> paramType = TypeLiteral.get( paramTypeHolder[0] );
                Class<?> rawPropertyType = paramType.getRawType();
                if ( null != valueType && rawPropertyType.isAssignableFrom( valueType ) )
                {
                    rawPropertyType = valueType; // pick more specific type
                }
                value = convertProperty( beanType, rawPropertyType, paramType.getType(), configuration );
                if ( null != value )
                {
                    if ( null != listener )
                    {
                        listener.notifyFieldChangeUsingSetter( propertyName, value, bean );
                    }
                    setter.invoke( bean, value );
                    return;
                }
            }
            catch ( final Exception e )
            {
                problem = e;
            }
            catch ( final LinkageError e )
            {
                problem = e;
            }
        }

        // ----------------------------------------------------------------------

        final Field field = findField( beanType, propertyName );
        if ( null != field )
        {
            try
            {
                final TypeLiteral<?> fieldType = TypeLiteral.get( field.getGenericType() );
                Class<?> rawPropertyType = fieldType.getRawType();
                if ( !rawPropertyType.isInstance( value ) ) // only re-convert if we must
                {
                    if ( null != valueType && rawPropertyType.isAssignableFrom( valueType ) )
                    {
                        rawPropertyType = valueType; // pick more specific type
                    }
                    value = convertProperty( beanType, rawPropertyType, fieldType.getType(), configuration );
                }
                if ( null != value )
                {
                    if ( null != listener )
                    {
                        listener.notifyFieldChangeUsingReflection( propertyName, value, bean );
                    }
                    setField( bean, field, value );
                    return;
                }
            }
            catch ( final Exception e )
            {
                if ( null == problem )
                {
                    problem = e;
                }
            }
            catch ( final LinkageError e )
            {
                if ( null == problem )
                {
                    problem = e;
                }
            }
        }

        // ----------------------------------------------------------------------

        if ( problem instanceof ComponentConfigurationException )
        {
            throw (ComponentConfigurationException) problem;
        }
        if ( null != problem )
        {
            final String reason = "Cannot set '" + propertyName + "' in " + beanType;
            throw new ComponentConfigurationException( configuration, reason, problem );
        }
        if ( null == setter && null == field )
        {
            final String reason = "Cannot find '" + propertyName + "' in " + beanType;
            throw new ComponentConfigurationException( configuration, reason );
        }
    }

    // ----------------------------------------------------------------------
    // Implementation methods
    // ----------------------------------------------------------------------

    private Object convertProperty( final Class<?> beanType, final Class<?> rawPropertyType,
                                    final Type genericPropertyType, final PlexusConfiguration configuration )
        throws ComponentConfigurationException
    {
        final ConfigurationConverter converter = lookup.lookupConverterForType( rawPropertyType );
        if ( !( genericPropertyType instanceof Class<?> ) && converter instanceof ParameterizedConfigurationConverter )
        {
            final Type[] propertyTypeArgs = getTypeArguments( genericPropertyType );
            return ( (ParameterizedConfigurationConverter) converter ).fromConfiguration( lookup, configuration,
                                                                                          rawPropertyType,
                                                                                          propertyTypeArgs, beanType,
                                                                                          loader, evaluator, listener );
        }
        return converter.fromConfiguration( lookup, configuration, rawPropertyType, beanType, loader, evaluator,
                                            listener );
    }

    private static Type[] getTypeArguments( final Type type )
    {
        if ( type instanceof ParameterizedType )
        {
            final Type[] typeArguments = ( (ParameterizedType) type ).getActualTypeArguments();
            for ( int i = 0; i < typeArguments.length; i++ )
            {
                typeArguments[i] = expandType( typeArguments[i] );
            }
            return typeArguments;
        }
        if ( type instanceof GenericArrayType )
        {
            return new Type[] { expandType( ( (GenericArrayType) type ).getGenericComponentType() ) };
        }
        return NO_TYPES;
    }

    private static Type expandType( final Type type )
    {
        if ( type instanceof WildcardType )
        {
            return ( (WildcardType) type ).getUpperBounds()[0];
        }
        if ( type instanceof TypeVariable<?> )
        {
            return ( (TypeVariable<?>) type ).getBounds()[0];
        }
        return type;
    }

    private static Method findMethod( final Class<?> beanType, final Type[] paramTypeHolder, final String methodName )
    {
        for ( final Method m : beanType.getMethods() )
        {
            if ( methodName.equals( m.getName() ) && !Modifier.isStatic( m.getModifiers() ) )
            {
                final Type[] paramTypes = m.getGenericParameterTypes();
                if ( paramTypes.length == 1 )
                {
                    paramTypeHolder[0] = paramTypes[0];
                    return m;
                }
            }
        }
        return null;
    }

    private static Field findField( final Class<?> beanType, final String fieldName )
    {
        for ( final Member f : new DeclaredMembers( beanType, View.FIELDS ) )
        {
            if ( fieldName.equals( f.getName() ) && !Modifier.isStatic( f.getModifiers() ) )
            {
                return (Field) f;
            }
        }
        return null;
    }

    private static void setField( final Object bean, final Field field, final Object value )
        throws Exception
    {
        if ( !field.isAccessible() )
        {
            AccessController.doPrivileged( new PrivilegedAction<Void>()
            {
                public Void run()
                {
                    field.setAccessible( true );
                    return null;
                }
            } );
        }
        field.set( bean, value );
    }
}
