package com.netflix.fabricator;

import com.google.common.base.CaseFormat;
import com.google.common.base.Preconditions;
import com.google.common.collect.Maps;
import com.google.inject.Inject;
import com.netflix.fabricator.component.ComponentFactory;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nullable;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.Map.Entry;

/**
 * Utility class for creating a binding between a type string name and an
 * implementation using the builder pattern.
 * 
 * TODO: PostConstruct and PreDestroy
 * 
 * @author elandau
 *
 * @param <T>
 * 
 */
public class BindingComponentFactory<T>  {
    private static final Logger LOG = LoggerFactory.getLogger(BindingComponentFactory.class);
    
    private static final String BUILDER_METHOD_NAME = "builder";
    private static final String BUILD_METHOD_NAME   = "build";
    private static final String ID_METHOD_SUFFIX    = "Id"; // TODO: Make this configurable
    private static final String WITH_METHOD_PREFIX  = "with";
    private static final String SET_METHOD_PREFIX   = "set";
    
    private final ComponentFactory<T>        factory;
    
    public static interface Instantiator {
        public Object create(ConfigurationNode config) throws Exception ;
    }
    
    /**
     * Class determined to be the builder
     */
    private Class<?>                         builderClass;
    
    /**
     * Map of all object properties keyed by property name.  
     * 
     * TODO: What about multiple methods for the same property?
     */
    private final  Map<String, PropertyInfo> properties;
    
    /**
     * Implementation determined to be best method for instantiating an instance of T or it's builder
     */
    private Instantiator                     instantiator;
    
    private PropertyBinderResolver           binderResolver;
    
    public BindingComponentFactory(final Class<?> clazz, PropertyBinderResolver binderResolver, final InjectionSpi injector) {
        this.binderResolver = binderResolver;
        
        try {
            // Check if this class is a Builder<T> in which case just create
            // an instance of the builder and treat it as a builder with builder
            // method get().
            if (Builder.class.isAssignableFrom(clazz)) {
                this.builderClass = clazz;
                this.instantiator = new Instantiator() {
                    public Object create(@Nullable ConfigurationNode config) {
                        return (Builder<?>) injector.getInstance(clazz);
                    }
                };
            }
            // Check if there is a builder() method that returns an instance of the 
            // builder.
            else {
                final Method method = clazz.getMethod(BUILDER_METHOD_NAME);
                this.builderClass = method.invoke(null, (Object[])null).getClass();
                this.instantiator = new Instantiator() {
                    public Object create(ConfigurationNode config) throws Exception {
                        Object obj = method.invoke(null, (Object[])null);
                        injector.injectMembers(obj);
                        return obj;
                    }
                };
            }
        } 
        catch (Exception e) {
            // Otherwise, look for a Builder inner class of T
            for (final Class<?> inner : clazz.getClasses()) {
                if (inner.getSimpleName().equals("Builder")) {
                    this.builderClass = inner;
                    this.instantiator = new Instantiator() {
                        public Object create(ConfigurationNode config) {
                            return injector.getInstance(inner);
                        }
                    };
                    break;
                }
            }
        }
        
        Preconditions.checkNotNull(builderClass, "No builder class found for " + clazz.getCanonicalName());
        
        properties = makePropertiesMap(builderClass);
        this.factory = new ComponentFactory<T>() {
            @SuppressWarnings("unchecked")
            @Override
            public T create(ConfigurationNode config) {
                try {
                    // 1. Create an instance of the builder.  This still will also do basic
                    //    dependency injection using @Inject.  Named injections will be handled
                    //    by the configuration mapping phase
                    Object builder = instantiator.create(config);

                    // 2. Set the 'id'
                    mapId(builder, config);

                    // 3. Apply configuration
                    mapConfiguration(builder, config);
                    
                    // 4. call build()
                    Method buildMethod = builder.getClass().getMethod(BUILD_METHOD_NAME);
                    return (T) buildMethod.invoke(builder);
                } catch (Exception e) {
                    throw new RuntimeException(String.format("Error creating component '%s' of type '%s'", config.getId(), clazz.getName()), e);
                }
            }

            @Override
            public Map<String, PropertyInfo> getProperties() {
                return properties;
            }

            @Override
            public Class<?> getRawType() {
                return clazz;
            }
        };
    }

    private void mapId(Object builder, ConfigurationNode config) throws Exception {
        if (config.getId() != null) {
            Method idMethod = null;
            try {
                idMethod = builder.getClass().getMethod(WITH_METHOD_PREFIX + ID_METHOD_SUFFIX, String.class);
            }catch (NoSuchMethodException e) {
                // OK to ignore
            }
            if(idMethod == null) {
                try {
                    idMethod = builder.getClass().getMethod(SET_METHOD_PREFIX + ID_METHOD_SUFFIX, String.class);
                }catch (NoSuchMethodException e) {
                    // OK to ignore
                }
            }
            if (idMethod != null) {
                idMethod.invoke(builder, config.getId());
            } else {
                LOG.trace("cannot find id method");
            }
        }
    }
    
    /**
     * Perform the actual configuration mapping by iterating through all parameters
     * and applying the config.
     * 
     * @param obj
     * @param config
     * @throws Exception
     */
    private void mapConfiguration(Object obj, ConfigurationNode node) throws Exception {
        for (Entry<String, PropertyInfo> prop : properties.entrySet()) {
            ConfigurationNode child = node.getChild(prop.getKey());
            if (child != null) {
                LOG.trace("Mapping property : " + prop.getKey() + " to " + child.getValue(String.class));
                try {
                    prop.getValue().apply(obj, child);
                }
                catch (Exception e) {
                    throw new Exception("Failed to map property: " + prop.getKey(), e);
                }
            }
        }
    }

    private static boolean hasInjectAnnotation(Method method) {
        return method.isAnnotationPresent(Inject.class) ||
                method.isAnnotationPresent(javax.inject.Inject.class);
    }
    
    private static String getPropertyName(Method method) {
        if (method.getName().startsWith(WITH_METHOD_PREFIX)) {
            return CaseFormat.UPPER_CAMEL.to(
                    CaseFormat.LOWER_CAMEL, 
                        StringUtils.substringAfter(method.getName(), WITH_METHOD_PREFIX));
        }
        
        if (method.getName().startsWith(SET_METHOD_PREFIX)) {
            return CaseFormat.UPPER_CAMEL.to(
                    CaseFormat.LOWER_CAMEL, 
                        StringUtils.substringAfter(method.getName(), SET_METHOD_PREFIX));
        }
        
        return null;
    }
    
    /**
     * Return all deduced properties.
     * 
     * @param builderClass
     * @return
     */
    private Map<String, PropertyInfo> makePropertiesMap(Class<?> builderClass) {
        Map<String, PropertyInfo> properties = Maps.newHashMap();
        
        // Iterate through each method and try to match a property
        for (final Method method : builderClass.getMethods()) {
            // Skip methods that do real DI.  These will have been injected at object creation
            if (hasInjectAnnotation(method)) {
                continue;
            }
            
            // Deduce property name from the method
            final String propertyName = getPropertyName(method);
            if (propertyName == null) {
                continue;
            }
            
            // Only support methods with a single parameter.
            // TODO: Might want to support methods that take TimeUnit
            Class<?>[] types = method.getParameterTypes();
            if (types.length != 1) {
                continue;
            }
            
            PropertyInfo prop = new PropertyInfo(propertyName);
            PropertyBinder binding = binderResolver.get(method);
            if (binding != null) {
                prop.addBinding(binding);
                properties.put(propertyName, prop);
            }
        }
        
        return properties;
    }
    
    public ComponentFactory<T> get() {
        return factory;
    }
}

@@ -2933,45 +2933,21 @@
 hod.
-invoke(null, (Object%5B%5D)null).getClass
+getReturnType
 ();%0A

Fix unnecessary pre-allocation of Builders