/*
 * Copyright 2002-2008 the original author or authors.
 *
 * 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 org.springframework.config.java.valuesource;

import static java.lang.String.format;
import static org.springframework.util.StringUtils.collectionToCommaDelimitedString;

import java.lang.annotation.ElementType;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.Map;

import org.springframework.beans.factory.support.DefaultListableBeanFactory;


/**
 * Aggregates one or more {@link ValueResolver} instances, delegating calls to
 * {@link #resolve(String, Class)}.
 * 
 * @author Rod Johnson
 * @author Chris Beams
 */
public class CompositeValueResolver implements ValueResolver {

    private final LinkedList<ValueResolver> resolvers = new LinkedList<ValueResolver>();

    /**
     * Creates a new {@link CompositeValueResolver} instance based on a map of
     * {@link ValueResolver} instances.
     * <p/>
     * Instances are sorted in reverse-order of the names of their keys in the map.
     * 
     * @param resolvers resolvers to aggregate into one composite
     */
    public CompositeValueResolver(Map<String, ValueResolver> resolvers) {
        ArrayList<String> keys = new ArrayList<String>(resolvers.keySet());
        Collections.sort(keys);
        Collections.reverse(keys);
        for (String key : keys)
            this.add(resolvers.get(key));
    }

    public void add(ValueResolver resolver) {
        this.resolvers.add(resolver);
    }

    @SuppressWarnings("unchecked")
    public <T> T resolve(String name, Class<?> requiredType) {
        for (ValueResolver resolver : resolvers) {
            try {
                return (T) resolver.resolve(name, requiredType);
            } catch (ValueResolutionException ex) {
                // Keep going to next property source
            }
        }
        // If we get here, we didn't find a definition in any property source
        throw new ValueResolutionException(name,
                format("Cannot resolve property for name '%s' against resolvers [%s]",
                        name, listResolvers()));
    }

    /**
     * Returns a comma-delimited list of resolvers suitable for use in exception messages.
     */
    private String listResolvers() {
        ArrayList<String> resolverNames = new ArrayList<String>();
        for (ValueResolver resolver : resolvers)
            resolverNames.add(resolver.getClass().getSimpleName());
        return collectionToCommaDelimitedString(resolverNames);
    }

    /**
     * Aggregates any {@link ValueResolver} beans from <var>beanFactory</var> into a new
     * {@link CompositeValueResolver} instance.
     * 
     * @param beanFactory enclosing bean factory containing one or more ValueResolver beans
     * @param field supplied for error-reporting purposes
     * 
     * @return {@link CompositeValueResolver} instance
     * 
     * @throws IllegalStateException if no ValueResolver instances can be found in <var>beanFactory</var>
     */
    public static CompositeValueResolver forMember(DefaultListableBeanFactory beanFactory, Field field) {
        return forMember(beanFactory, ElementType.FIELD, field.getName(), field.getDeclaringClass());
    }

    /**
     * Aggregates any {@link ValueResolver} beans from <var>beanFactory</var> into a new
     * {@link CompositeValueResolver} instance.
     * 
     * @param beanFactory enclosing bean factory containing one or more ValueResolver beans
     * @param method supplied for error-reporting purposes
     * 
     * @return {@link CompositeValueResolver} instance
     * 
     * @throws IllegalStateException if no ValueResolver instances can be found in <var>beanFactory</var>
     */
    public static CompositeValueResolver forMember(DefaultListableBeanFactory beanFactory, Method method) {
        return forMember(beanFactory, ElementType.METHOD, method.getName(), method.getDeclaringClass());
    }

    /**
     * Aggregates any {@link ValueResolver} beans from <var>beanFactory</var> into a new
     * {@link CompositeValueResolver} instance.
     * 
     * @param beanFactory enclosing bean factory containing one or more ValueResolver beans
     * @param memberType type of member (used for error-reporting purposes)
     * @param memberName name of member (used for error-reporting purposes)
     * @param declaringClass class that declares <var>memberName</var> (used for error-reporting purposes)
     * 
     * @return {@link CompositeValueResolver} instance
     * 
     * @throws IllegalStateException if no ValueResolver instances can be found in <var>beanFactory</var>
     */
    private static CompositeValueResolver forMember(DefaultListableBeanFactory beanFactory, ElementType memberType,
                                                    String memberName, Class<?> declaringClass) {
        @SuppressWarnings("unchecked")
        Map<String, ValueResolver> resolvers = beanFactory.getBeansOfType(ValueResolver.class);

        if(resolvers.size() == 0) {
            String className = declaringClass.getSimpleName();
            throw new IllegalStateException(format("No ValueSource bean could be found in "
                                                   + "beanFactory while trying to resolve @ExternalValue %s %s.%s. "
                                                   + "Perhaps no @PropertiesValueSource annotation was provided on %s?",
                                                   memberType.toString().toLowerCase(), className, memberName, className));
        }

        return new CompositeValueResolver(resolvers);
    }

}

