
package com.simpligility.maven.plugins.android.config;

import org.apache.maven.execution.MavenSession;
import org.apache.maven.plugin.MojoExecution;
import org.apache.maven.plugin.PluginParameterExpressionEvaluator;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;

/**
 * ConfigHandler is able to parse the configuration of a Mojo based on the Maven injected parameters as well as a config
 * pojo and annontations for default values on properties named parsed*. See the ProguardMojo for a working
 * implementation.
 * 
 * @author Adrian Stabiszewski https://github.com/grundid/
 * @author Manfred Moser - manfred@simpligility.com
 * @see ConfigPojo
 * @see PullParameter
 */
public class ConfigHandler
{

    private Object mojo;
    private Object configPojoInstance;
    private String configPojoName;
    private String configPojoPrefix;
    private PluginParameterExpressionEvaluator evaluator;

    public ConfigHandler( Object mojo, MavenSession session, MojoExecution execution )
    {
        this.mojo = mojo;

        if ( session == null )
        {
            throw new IllegalArgumentException( "The argument session is required" );
        }
        if ( execution == null )
        {
            throw new IllegalArgumentException( "The argument execution is required" );
        }

        this.evaluator = new PluginParameterExpressionEvaluator( session, execution );

        initConfigPojo();
    }

    private Collection< Field > findPropertiesByAnnotation( Class< ? extends Annotation > annotation )
    {
        Collection< Field > result = new ArrayList< Field >();
        for ( Class< ? extends Object > cls = mojo.getClass(); cls != Object.class; cls = cls.getSuperclass() )
        {
            for ( Field field : cls.getDeclaredFields() )
            {
                if ( field.isAnnotationPresent( annotation ) )
                {
                    field.setAccessible( true );
                    result.add( field );
                }
            }
        }

        return result;
    }

    public void parseConfiguration()
    {
        Collection< Field > parsedFields = findPropertiesByAnnotation( PullParameter.class );

        for ( Field field : parsedFields )
        {
            Object value = null;
            String fieldBaseName = getFieldNameWithoutParsedPrefix( field );
            // first take the setting from the config pojo (e.g. nested config in plugin configuration)
            if ( configPojoInstance != null )
            {
                value = getValueFromPojo( fieldBaseName );
            }
            // then override with value from properties supplied in pom, settings or command line
            // unless it is null or an empty array
            Object propertyValue = getValueFromMojo( fieldBaseName );
            if ( propertyValue == null || propertyValue instanceof Object[]//
                && ( (Object[]) propertyValue ).length == 0 )
            {
                // no useful value
            }
            else
            {
                value = propertyValue;
            }
            // and only if we still have no value, get the default as declared in the annotation
            if ( value == null )
            {
                value = getValueFromAnnotation( field );
            }

            try
            {
                field.set( mojo, value );
            }
            catch ( Exception e )
            {
                e.printStackTrace();
            }
        }
    }

    private Object getValueFromAnnotation( Field field )
    {
        PullParameter annotation = field.getAnnotation( PullParameter.class );
        String[] defaultValue = annotation.defaultValue();
        boolean required = annotation.required();
        String currentParameterName = "android." + configPojoName + "." + getFieldNameWithoutParsedPrefix( field );

        if ( defaultValue.length > 0 )
        {
            if ( defaultValue.length > 1 )
            {
                throw new RuntimeException( String.format( "Too many default values for field %s", field.getName() ) );
            }

            final Class< ? > fieldType = field.getType();

            try
            {
                final Object defValue = evaluator.evaluate( defaultValue[0], fieldType );

                if ( defValue == null || fieldType.isInstance( defValue ) )
                {
                    return defValue;
                }

                return convertTo( fieldType, defValue );
            }
            catch ( RuntimeException e )
            {
                throw e;
            }
            catch ( Exception e )
            {
                throw new RuntimeException( String.format(
                    "Problem encountered converting default value for %s parameter to %s",
                    currentParameterName, fieldType ), e );
            }
        }
        else
        {
            if ( !required )
            {
                // if no default value method, simply return null
                if ( annotation.defaultValueGetterMethod().isEmpty() )
                {
                    return null;
                }

                try
                {
                    Method method = mojo.getClass().getDeclaredMethod( annotation.defaultValueGetterMethod() );
                    // even access it if the method is private
                    method.setAccessible( true );
                    return method.invoke( mojo );
                }
                catch ( Exception e )
                {
                    throw new RuntimeException( String.format(
                        "Problem encountered accessing default value for %s parameter",
                        currentParameterName ), e );
                }
            }
            else
            {
                throw new RuntimeException( String.format(
                    "Required parameter %1$ has no value. Please "
                        + "supply with -D%1$=value on the command line or as property or "
                        + "plugin configuration in your pom or settings file.",
                    currentParameterName ) );
            }
        }
    }

    private Object convertTo( Class< ? > javaType, Object defValue )
    throws Exception
    {
        // try valueOf
        try
        {
            return javaType.getMethod( "valueOf", String.class ).invoke( null, defValue );
        }
        catch ( NoSuchMethodException e )
        {
            return javaType.getConstructor( String.class ).newInstance( defValue );
        }
    }

    private Object getValueFromMojo( String fieldBaseName )
    {
        return getValueFromObject( mojo, configPojoName + toFirstLetterUppercase( fieldBaseName ) );
    }

    private Object getValueFromPojo( String fieldBaseName )
    {
        return getValueFromObject( configPojoInstance, fieldBaseName );
    }

    private Object getValueFromObject( Object object, String fieldBaseName )
    {
        Object value = null;
        try
        {
            Field pojoField = findFieldByName( object, fieldBaseName );
            if ( pojoField != null )
            {
                value = pojoField.get( object );
            }
        }
        catch ( Exception e )
        {
            // swallow
        }
        return value;
    }

    private Field findFieldByName( Object object, String name )
    {
        for ( Field field : object.getClass().getDeclaredFields() )
        {
            if ( field.getName().equals( name ) )
            {
                field.setAccessible( true );
                return field;
            }
        }
        return null;
    }

    private String getFieldNameWithoutPrefix( Field field, String prefix )
    {
        if ( field.getName().startsWith( prefix ) )
        {
            String fieldName = field.getName().substring( prefix.length() );
            return fieldName.substring( 0, 1 ).toLowerCase() + fieldName.substring( 1 );
        }
        else
        {
            return field.getName();
        }
    }

    private String toFirstLetterUppercase( String s )
    {
        return s.substring( 0, 1 ).toUpperCase() + s.substring( 1 );
    }

    private String getFieldNameWithoutParsedPrefix( Field field )
    {
        return getFieldNameWithoutPrefix( field, configPojoPrefix );
    }

    private void initConfigPojo()
    {
        try
        {
            Field configPojo = findPropertiesByAnnotation( ConfigPojo.class ).iterator().next();
            configPojoName = configPojo.getName();
            configPojoInstance = configPojo.get( mojo );
            configPojoPrefix = configPojo.getAnnotation( ConfigPojo.class ).prefix();
        }
        catch ( Exception e )
        {
            // ignore, we can live without a config pojo
        }
    }
}
