/**
 * Copyright Microsoft Corporation
 * 
 * Licensed 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 com.microsoft.windowsazure.core;

import com.microsoft.windowsazure.ConfigurationException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.ServiceLoader;

import javax.inject.Inject;
import javax.inject.Named;

public class DefaultBuilder implements Builder, Builder.Registry {
    private Map<Class<?>, Factory<?>> factories;
    private Map<Class<?>, Map<Class<?>, List<Alteration<?>>>> alterations;

    public DefaultBuilder() {
        factories = new HashMap<Class<?>, Factory<?>>();
        alterations = new HashMap<Class<?>, Map<Class<?>, List<Alteration<?>>>>();
    }

    public static DefaultBuilder create() {
        DefaultBuilder builder = new DefaultBuilder();

        for (Builder.Exports exports : ServiceLoader
                .load(Builder.Exports.class)) {
            exports.register(builder);
        }

        return builder;
    }

    void addFactory(Class<?> service, Factory<?> factory) {
        factories.put(service, factory);
    }

    @Override
    public <T> Builder.Registry add(Class<T> service) {
        return add(service, service);
    }

    Constructor<?> findInjectConstructor(Class<?> implementation) {

        Constructor<?> withInject = null;
        Constructor<?> withoutInject = null;
        int count = 0;

        for (Constructor<?> ctor : implementation.getConstructors()) {
            if (ctor.getAnnotation(Inject.class) != null) {
                if (withInject != null) {
                    throw new RuntimeException(
                            "Class must not have multple @Inject annotations: "
                                    + implementation.getName());
                }
                withInject = ctor;
            } else {
                ++count;
                withoutInject = ctor;
            }
        }
        if (withInject != null) {
            return withInject;
        }
        if (count != 1) {
            throw new RuntimeException(
                    "Class without @Inject annotation must have one constructor: "
                            + implementation.getName());
        }
        return withoutInject;
    }

    @Override
    public <T, TImpl> Builder.Registry add(Class<T> service,
            final Class<TImpl> implementation) {
        final Constructor<?> ctor = findInjectConstructor(implementation);
        final Class<?>[] parameterTypes = ctor.getParameterTypes();
        final Annotation[][] parameterAnnotations = ctor
                .getParameterAnnotations();

        addFactory(service, new Builder.Factory<T>() {
            @Override
            @SuppressWarnings("unchecked")
            public <S> T create(String profile, Class<S> service,
                    Builder builder, Map<String, Object> properties) {
                Object[] initializationArguments = new Object[parameterTypes.length];
                for (int i = 0; i != parameterTypes.length; ++i) {

                    boolean located = false;

                    String named = findNamedAnnotation(parameterAnnotations[i]);
                    String fullName = dotCombine(profile, named);

                    boolean probeProperties = fullName != null
                            && fullName != "";
                    int startingIndex = 0;
                    while (!located && probeProperties) {
                        String nameProbe = fullName.substring(startingIndex);
                        if (!located && named != null
                                && properties.containsKey(nameProbe)) {
                            located = true;
                            if (parameterTypes[i] == String.class) {
                                initializationArguments[i] = properties
                                    .get(nameProbe);
                            }
                            else {
                                initializationArguments[i] = builder.build(fullName,
                                        service, parameterTypes[i], properties);
                            }
                            
                        } else {
                            startingIndex = fullName
                                    .indexOf('.', startingIndex) + 1;
                            if (startingIndex == 0) {
                                probeProperties = false;
                            }
                        }
                    }

                    if (!located) {
                        located = true;
                        initializationArguments[i] = builder.build(fullName,
                                service, parameterTypes[i], properties);
                    }
                }

                try {
                    return (T) ctor.newInstance(initializationArguments);
                } catch (InstantiationException e) {
                    throw new ConfigurationException(e);
                } catch (IllegalAccessException e) {
                    throw new ConfigurationException(e);
                } catch (InvocationTargetException e) {
                    throw new ConfigurationException(e);
                }
            }
        });
        return this;
    }

    protected String dotCombine(String profile, String named) {
        boolean noProfile = profile == null || profile.isEmpty();
        boolean noName = named == null || named.isEmpty();
        if (noName) {
            return profile;
        }
        if (noProfile) {
            return named;
        }

        return profile + "." + named;
    }

    protected String findNamedAnnotation(Annotation[] annotations) {
        for (Annotation annotation : annotations) {
            if (Named.class.isAssignableFrom(annotation.getClass())) {
                return ((Named) annotation).value();
            }
        }
        return null;
    }

    @Override
    public <T> Registry add(Factory<T> factory) {
        for (Type genericInterface : factory.getClass().getGenericInterfaces()) {
            ParameterizedType parameterizedType = (ParameterizedType) genericInterface;
            if (parameterizedType.getRawType().equals(Builder.Factory.class)) {
                Type typeArgument = parameterizedType.getActualTypeArguments()[0];
                addFactory((Class<?>) typeArgument, factory);
            }
        }
        return this;
    }

    @Override
    @SuppressWarnings("unchecked")
    public <S, T> T build(String profile, Class<S> service,
            Class<T> instanceClass, Map<String, Object> properties) {
        Factory<T> factory = (Factory<T>) factories.get(instanceClass);
        if (factory == null) {
            throw new RuntimeException("Service or property not registered: "
                    + profile + " " + service.getName() + " " + instanceClass);
        }
        T instance = factory.create(profile, service, this, properties);
        Map<Class<?>, List<Alteration<?>>> alterationMap = alterations
                .get(service);
        if (alterationMap != null) {
            List<Alteration<?>> alterationList = alterationMap
                    .get(instanceClass);
            if (alterationList != null) {
                for (Alteration<?> alteration : alterationList) {
                    instance = ((Alteration<T>) alteration).alter(profile,
                            instance, this, properties);
                }
            }
        }
        return instance;
    }

    @Override
    public <S, T> void alter(Class<S> service, Class<T> instance,
            Alteration<T> alteration) {
        if (!this.alterations.containsKey(service)) {
            this.alterations.put(service,
                    new HashMap<Class<?>, List<Alteration<?>>>());
        }
        if (!this.alterations.get(service).containsKey(instance)) {
            this.alterations.get(service).put(instance,
                    new ArrayList<Alteration<?>>());
        }
        this.alterations.get(service).get(instance).add(alteration);
    }

}

@@ -4651,36 +4651,8 @@
    %7D
-%0A                           
  els
@@ -4855,37 +4855,8 @@
   %7D%0A
-                            %0A
     

Fix styling issue