/*
 * Copyright (C) The Apache Software Foundation. All rights reserved.
 *
 * This software is published under the terms of the Apache Software License
 * version 1.1, a copy of which has been included with this distribution in
 * the LICENSE.txt file.
 */
package org.apache.myrmidon.components.configurer;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import org.apache.aut.converter.Converter;
import org.apache.aut.converter.ConverterException;
import org.apache.avalon.excalibur.i18n.ResourceManager;
import org.apache.avalon.excalibur.i18n.Resources;
import org.apache.avalon.framework.configuration.Configurable;
import org.apache.avalon.framework.configuration.Configuration;
import org.apache.avalon.framework.configuration.ConfigurationException;
import org.apache.avalon.framework.logger.AbstractLogEnabled;
import org.apache.avalon.framework.logger.LogEnabled;
import org.apache.avalon.framework.service.ServiceException;
import org.apache.avalon.framework.service.ServiceManager;
import org.apache.avalon.framework.service.Serviceable;
import org.apache.myrmidon.api.TaskContext;
import org.apache.myrmidon.api.TaskException;
import org.apache.myrmidon.interfaces.configurer.Configurer;

/**
 * Class used to configure tasks.
 *
 * @author <a href="mailto:peter@apache.org">Peter Donald</a>
 * @ant.type type="configurer" name="classic"
 */
public class ClassicConfigurer
    extends AbstractLogEnabled
    implements Configurer, Serviceable, LogEnabled
{
    private static final Resources REZ =
        ResourceManager.getPackageResources( DefaultConfigurer.class );

    ///Compile time constant to turn on extreme debugging
    private static final boolean DEBUG = false;

    ///Converter to use for converting between values
    private Converter m_converter;

    public void service( final ServiceManager serviceManager )
        throws ServiceException
    {
        m_converter = (Converter)serviceManager.lookup( Converter.ROLE );
    }

    /**
     * Configure an object based on a configuration in a particular context.
     * This configuring can be done in different ways for different
     * configurers.
     *
     * The implementation of this method should only use the methods
     * specified by the supplied class. It is an error for the specified
     * class not to be a base class or interface compatible with specified
     * object.
     *
     * @param object the object
     * @param clazz the Class object to  use during configuration
     * @param configuration the configuration
     * @param context the Context
     * @exception ConfigurationException if an error occurs
     */
    public void configureElement( Object object,
                                  Class clazz,
                                  Configuration configuration,
                                  TaskContext context )
        throws ConfigurationException
    {
        throw new UnsupportedOperationException();
    }

    /**
     * Configure named attribute of object in a particular context.
     * This configuring can be done in different ways for different
     * configurers.
     *
     * The implementation of this method should only use the methods
     * specified by the supplied class. It is an error for the specified
     * class not to be a base class or interface compatible with specified
     * object.
     *
     * @param object the object
     * @param clazz the Class object to  use during configuration
     * @param name the attribute name
     * @param value the attribute value
     * @param context the Context
     * @exception ConfigurationException if an error occurs
     */
    public void configureAttribute( Object object,
                                    Class clazz,
                                    String name,
                                    String value,
                                    TaskContext context )
        throws ConfigurationException
    {
        throw new UnsupportedOperationException();
    }

    /**
     * Configure a task based on a configuration in a particular context.
     * This configuring can be done in different ways for different
     * configurers.
     * This one does it by first checking if object implements Configurable
     * and if it does will pass the task the configuration - else it will use
     * mapping rules to map configuration to types
     *
     * @param object the object
     * @param configuration the configuration
     * @param context the Context
     * @exception ConfigurationException if an error occurs
     */
    public void configureElement( final Object object,
                                  final Configuration configuration,
                                  final TaskContext context )
        throws ConfigurationException
    {
        if( DEBUG )
        {
            final String message = REZ.getString( "configuring-object.notice", object );
            getLogger().debug( message );
        }

        if( object instanceof Configurable )
        {
            if( DEBUG )
            {
                final String message = REZ.getString( "configurable.notice" );
                getLogger().debug( message );
            }

            final Configurable configurable = (Configurable)object;
            configurable.configure( configuration );
        }
        else
        {
            if( DEBUG )
            {
                final String message = REZ.getString( "reflection.notice" );
                getLogger().debug( message );
            }

            final String[] attributes = configuration.getAttributeNames();
            for( int i = 0; i < attributes.length; i++ )
            {
                final String name = attributes[ i ];
                final String value = configuration.getAttribute( name );

                if( DEBUG )
                {
                    final String message = REZ.getString( "configure-attribute.notice", name, value );
                    getLogger().debug( message );
                }

                doConfigureAttribute( object, name, value, context );
            }

            final Configuration[] children = configuration.getChildren();

            for( int i = 0; i < children.length; i++ )
            {
                final Configuration child = children[ i ];

                if( DEBUG )
                {
                    final String message =
                        REZ.getString( "configure-subelement.notice", child.getName() );
                    getLogger().debug( message );
                }

                doConfigureElement( object, child, context );
            }

            final String content = configuration.getValue( null );
            if( null != content )
            {
                if( !content.trim().equals( "" ) )
                {
                    if( DEBUG )
                    {
                        final String message =
                            REZ.getString( "configure-content.notice", content );
                        getLogger().debug( message );
                    }

                    configureContent( object, content, context );
                }
            }
        }
    }

    /**
     * Configure named attribute of object in a particular context.
     * This configuring can be done in different ways for different
     * configurers.
     *
     * @param object the object
     * @param name the attribute name
     * @param value the attribute value
     * @param context the Context
     * @exception ConfigurationException if an error occurs
     */
    public void configureAttribute( final Object object,
                                    final String name,
                                    final String value,
                                    final TaskContext context )
        throws ConfigurationException
    {
        doConfigureAttribute( object, name, value, context );
    }

    /**
     * Try to configure content of an object.
     *
     * @param object the object
     * @param content the content value to be set
     * @param context the Context
     * @exception ConfigurationException if an error occurs
     */
    private void configureContent( final Object object,
                                   final String content,
                                   final TaskContext context )
        throws ConfigurationException
    {
        setValue( object, "addContent", content, context );
    }

    private void doConfigureAttribute( final Object object,
                                       final String name,
                                       final String value,
                                       final TaskContext context )
        throws ConfigurationException
    {
        final String methodName = getMethodNameFor( name );
        setValue( object, methodName, value, context );
    }

    private void setValue( final Object object,
                           final String methodName,
                           final String value,
                           final TaskContext context )
        throws ConfigurationException
    {
        // OMFG the rest of this is soooooooooooooooooooooooooooooooo
        // slow. Need to cache results per class etc.

        final Class clazz = object.getClass();
        final Method[] methods = getMethodsFor( clazz, methodName );
        if( 0 == methods.length )
        {
            final String message =
                REZ.getString( "no-attribute-method.error", methodName );
            throw new ConfigurationException( message );
        }

        setValue( object, value, context, methods );
    }

    private void setValue( final Object object,
                           final String value,
                           final TaskContext context,
                           final Method[] methods )
        throws ConfigurationException
    {
        try
        {
            final Object objectValue = context.resolveValue( value );
            setValue( object, objectValue, methods, context );
        }
        catch( final TaskException te )
        {
            final String message =
                REZ.getString( "bad-property-resolve.error", value );
            throw new ConfigurationException( message, te );
        }
    }

    private void setValue( final Object object,
                           Object value,
                           final Method[] methods,
                           final TaskContext context )
        throws ConfigurationException
    {
        final Class sourceClass = value.getClass();
        final String source = sourceClass.getName();

        for( int i = 0; i < methods.length; i++ )
        {
            if( setValue( object, value, methods[ i ], context ) )
            {
                return;
            }
        }

        final String message =
            REZ.getString( "no-can-convert.error", methods[ 0 ].getName(), source );
        throw new ConfigurationException( message );
    }

    private boolean setValue( final Object object,
                              final Object originalValue,
                              final Method method,
                              final TaskContext context )
        throws ConfigurationException
    {
        Class parameterType = method.getParameterTypes()[ 0 ];
        if( parameterType.isPrimitive() )
        {
            parameterType = getComplexTypeFor( parameterType );
        }

        Object value = originalValue;
        try
        {
            value = m_converter.convert( parameterType, value, context );
        }
        catch( final ConverterException ce )
        {
            if( DEBUG )
            {
                final String message = REZ.getString( "no-converter.error" );
                getLogger().debug( message, ce );
            }

            throw new ConfigurationException( ce.getMessage(), ce );
        }
        catch( final Exception e )
        {
            final String message =
                REZ.getString( "bad-convert-for-attribute.error", method.getName() );
            throw new ConfigurationException( message, e );
        }

        if( null == value )
        {
            return false;
        }

        try
        {
            method.invoke( object, new Object[]{value} );
        }
        catch( final IllegalAccessException iae )
        {
            //should never happen ....
            final String message = REZ.getString( "illegal-access.error" );
            throw new ConfigurationException( message, iae );
        }
        catch( final InvocationTargetException ite )
        {
            final String message = REZ.getString( "invoke-target.error", method.getName() );
            throw new ConfigurationException( message, ite );
        }

        return true;
    }

    private Class getComplexTypeFor( final Class clazz )
    {
        if( String.class == clazz )
        {
            return String.class;
        }
        else if( Integer.TYPE.equals( clazz ) )
        {
            return Integer.class;
        }
        else if( Long.TYPE.equals( clazz ) )
        {
            return Long.class;
        }
        else if( Short.TYPE.equals( clazz ) )
        {
            return Short.class;
        }
        else if( Byte.TYPE.equals( clazz ) )
        {
            return Byte.class;
        }
        else if( Boolean.TYPE.equals( clazz ) )
        {
            return Boolean.class;
        }
        else if( Float.TYPE.equals( clazz ) )
        {
            return Float.class;
        }
        else if( Double.TYPE.equals( clazz ) )
        {
            return Double.class;
        }
        else
        {
            final String message = REZ.getString( "no-complex-type.error", clazz.getName() );
            throw new IllegalArgumentException( message );
        }
    }

    private Method[] getMethodsFor( final Class clazz, final String methodName )
    {
        final Method[] methods = clazz.getMethods();
        final ArrayList matches = new ArrayList();

        for( int i = 0; i < methods.length; i++ )
        {
            final Method method = methods[ i ];
            if( methodName.equals( method.getName() ) &&
                Method.PUBLIC == ( method.getModifiers() & Method.PUBLIC ) )
            {
                if( method.getReturnType().equals( Void.TYPE ) )
                {
                    final Class[] parameters = method.getParameterTypes();
                    if( 1 == parameters.length )
                    {
                        matches.add( method );
                    }
                }
            }
        }

        return (Method[])matches.toArray( new Method[ 0 ] );
    }

    private Method[] getCreateMethodsFor( final Class clazz, final String methodName )
    {
        final Method[] methods = clazz.getMethods();
        final ArrayList matches = new ArrayList();

        for( int i = 0; i < methods.length; i++ )
        {
            final Method method = methods[ i ];
            if( methodName.equals( method.getName() ) &&
                Method.PUBLIC == ( method.getModifiers() & Method.PUBLIC ) )
            {
                final Class returnType = method.getReturnType();
                if( !returnType.equals( Void.TYPE ) &&
                    !returnType.isPrimitive() )
                {
                    final Class[] parameters = method.getParameterTypes();
                    if( 0 == parameters.length )
                    {
                        matches.add( method );
                    }
                }
            }
        }

        return (Method[])matches.toArray( new Method[ 0 ] );
    }

    private String getMethodNameFor( final String attribute )
    {
        return "set" + getJavaNameFor( attribute.toLowerCase() );
    }

    private String getJavaNameFor( final String name )
    {
        final StringBuffer sb = new StringBuffer();

        int index = name.indexOf( '-' );
        int last = 0;

        while( -1 != index )
        {
            final String word = name.substring( last, index ).toLowerCase();
            sb.append( Character.toUpperCase( word.charAt( 0 ) ) );
            sb.append( word.substring( 1, word.length() ) );
            last = index + 1;
            index = name.indexOf( '-', last );
        }

        index = name.length();
        final String word = name.substring( last, index ).toLowerCase();
        sb.append( Character.toUpperCase( word.charAt( 0 ) ) );
        sb.append( word.substring( 1, word.length() ) );

        return sb.toString();
    }

    private void doConfigureElement( final Object object,
                                     final Configuration configuration,
                                     final TaskContext context )
        throws ConfigurationException
    {
        final String name = configuration.getName();
        final String javaName = getJavaNameFor( name );

        // OMFG the rest of this is soooooooooooooooooooooooooooooooo
        // slow. Need to cache results per class etc.
        final Class clazz = object.getClass();
        Method[] methods = getMethodsFor( clazz, "add" + javaName );

        if( 0 != methods.length )
        {
            //guess it is first method ????
            addElement( object, methods[ 0 ], configuration, context );
        }
        else
        {
            methods = getCreateMethodsFor( clazz, "create" + javaName );

            if( 0 == methods.length )
            {
                final String message =
                    REZ.getString( "no-element-method.error", javaName );
                throw new ConfigurationException( message );
            }

            //guess it is first method ????
            createElement( object, methods[ 0 ], configuration, context );
        }
    }

    private void createElement( final Object object,
                                final Method method,
                                final Configuration configuration,
                                final TaskContext context )
        throws ConfigurationException
    {
        try
        {
            final Object created = method.invoke( object, new Object[ 0 ] );
            doConfigureElement( created, configuration, context );
        }
        catch( final ConfigurationException ce )
        {
            throw ce;
        }
        catch( final Exception e )
        {
            final String message = REZ.getString( "subelement-create.error" );
            throw new ConfigurationException( message, e );
        }
    }

    private void addElement( final Object object,
                             final Method method,
                             final Configuration configuration,
                             final TaskContext context )
        throws ConfigurationException
    {
        try
        {
            final Class clazz = method.getParameterTypes()[ 0 ];
            final Object created = clazz.newInstance();

            doConfigureElement( created, configuration, context );
            method.invoke( object, new Object[]{created} );
        }
        catch( final ConfigurationException ce )
        {
            throw ce;
        }
        catch( final Exception e )
        {
            final String message = REZ.getString( "subelement-create.error" );
            throw new ConfigurationException( message, e );
        }
    }
}
