/*
 * Licensed to Elasticsearch under one or more contributor
 * license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright
 * ownership. Elasticsearch licenses this file to you 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.elasticsearch.common.inject;

import com.google.common.collect.Sets;
import org.elasticsearch.common.inject.matcher.Matcher;
import org.elasticsearch.common.inject.name.Names;
import org.elasticsearch.common.inject.spi.Message;

import java.lang.reflect.Type;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

/**
 *
 */
public class Injectors {

    public static Throwable getFirstErrorFailure(CreationException e) {
        if (e.getErrorMessages().isEmpty()) {
            return e;
        }
        // return the first message that has root cause, probably an actual error
        for (Message message : e.getErrorMessages()) {
            if (message.getCause() != null) {
                return message.getCause();
            }
        }
        return e;
    }

    /**
     * Returns an instance of the given type with the {@link org.elasticsearch.common.inject.name.Named}
     * annotation value.
     * <p/>
     * This method allows you to switch this code
     * <code>injector.getInstance(Key.get(type, Names.named(name)));</code>
     * <p/>
     * to the more concise
     * <code>Injectors.getInstance(injector, type, name);</code>
     */
    public static <T> T getInstance(Injector injector, java.lang.Class<T> type, String name) {
        return injector.getInstance(Key.get(type, Names.named(name)));
    }

    /**
     * Returns a collection of all instances of the given base type
     *
     * @param baseClass the base type of objects required
     * @param <T>       the base type
     * @return a set of objects returned from this injector
     */
    public static <T> Set<T> getInstancesOf(Injector injector, Class<T> baseClass) {
        Set<T> answer = Sets.newHashSet();
        Set<Entry<Key<?>, Binding<?>>> entries = injector.getBindings().entrySet();
        for (Entry<Key<?>, Binding<?>> entry : entries) {
            Key<?> key = entry.getKey();
            Class<?> keyType = getKeyType(key);
            if (keyType != null && baseClass.isAssignableFrom(keyType)) {
                Binding<?> binding = entry.getValue();
                Object value = binding.getProvider().get();
                if (value != null) {
                    T castValue = baseClass.cast(value);
                    answer.add(castValue);
                }
            }
        }
        return answer;
    }

    /**
     * Returns a collection of all instances matching the given matcher
     *
     * @param matcher matches the types to return instances
     * @return a set of objects returned from this injector
     */
    public static <T> Set<T> getInstancesOf(Injector injector, Matcher<Class> matcher) {
        Set<T> answer = Sets.newHashSet();
        Set<Entry<Key<?>, Binding<?>>> entries = injector.getBindings().entrySet();
        for (Entry<Key<?>, Binding<?>> entry : entries) {
            Key<?> key = entry.getKey();
            Class<?> keyType = getKeyType(key);
            if (keyType != null && matcher.matches(keyType)) {
                Binding<?> binding = entry.getValue();
                Object value = binding.getProvider().get();
                answer.add((T) value);
            }
        }
        return answer;
    }

    /**
     * Returns a collection of all of the providers matching the given matcher
     *
     * @param matcher matches the types to return instances
     * @return a set of objects returned from this injector
     */
    public static <T> Set<Provider<T>> getProvidersOf(Injector injector, Matcher<Class> matcher) {
        Set<Provider<T>> answer = Sets.newHashSet();
        Set<Entry<Key<?>, Binding<?>>> entries = injector.getBindings().entrySet();
        for (Entry<Key<?>, Binding<?>> entry : entries) {
            Key<?> key = entry.getKey();
            Class<?> keyType = getKeyType(key);
            if (keyType != null && matcher.matches(keyType)) {
                Binding<?> binding = entry.getValue();
                answer.add((Provider<T>) binding.getProvider());
            }
        }
        return answer;
    }

    /**
     * Returns a collection of all providers of the given base type
     *
     * @param baseClass the base type of objects required
     * @param <T>       the base type
     * @return a set of objects returned from this injector
     */
    public static <T> Set<Provider<T>> getProvidersOf(Injector injector, Class<T> baseClass) {
        Set<Provider<T>> answer = Sets.newHashSet();
        Set<Entry<Key<?>, Binding<?>>> entries = injector.getBindings().entrySet();
        for (Entry<Key<?>, Binding<?>> entry : entries) {
            Key<?> key = entry.getKey();
            Class<?> keyType = getKeyType(key);
            if (keyType != null && baseClass.isAssignableFrom(keyType)) {
                Binding<?> binding = entry.getValue();
                answer.add((Provider<T>) binding.getProvider());
            }
        }
        return answer;
    }

    /**
     * Returns true if a binding exists for the given matcher
     */
    public static boolean hasBinding(Injector injector, Matcher<Class> matcher) {
        return !getBindingsOf(injector, matcher).isEmpty();
    }

    /**
     * Returns true if a binding exists for the given base class
     */
    public static boolean hasBinding(Injector injector, Class<?> baseClass) {
        return !getBindingsOf(injector, baseClass).isEmpty();
    }

    /**
     * Returns true if a binding exists for the given key
     */
    public static boolean hasBinding(Injector injector, Key<?> key) {
        Binding<?> binding = getBinding(injector, key);
        return binding != null;
    }

    /**
     * Returns the binding for the given key or null if there is no such binding
     */
    public static Binding<?> getBinding(Injector injector, Key<?> key) {
        Map<Key<?>, Binding<?>> bindings = injector.getBindings();
        Binding<?> binding = bindings.get(key);
        return binding;
    }

    /**
     * Returns a collection of all of the bindings matching the given matcher
     *
     * @param matcher matches the types to return instances
     * @return a set of objects returned from this injector
     */
    public static Set<Binding<?>> getBindingsOf(Injector injector, Matcher<Class> matcher) {
        Set<Binding<?>> answer = Sets.newHashSet();
        Set<Entry<Key<?>, Binding<?>>> entries = injector.getBindings().entrySet();
        for (Entry<Key<?>, Binding<?>> entry : entries) {
            Key<?> key = entry.getKey();
            Class<?> keyType = getKeyType(key);
            if (keyType != null && matcher.matches(keyType)) {
                answer.add(entry.getValue());
            }
        }
        return answer;
    }

    /**
     * Returns a collection of all bindings of the given base type
     *
     * @param baseClass the base type of objects required
     * @return a set of objects returned from this injector
     */
    public static Set<Binding<?>> getBindingsOf(Injector injector, Class<?> baseClass) {
        Set<Binding<?>> answer = Sets.newHashSet();
        Set<Entry<Key<?>, Binding<?>>> entries = injector.getBindings().entrySet();
        for (Entry<Key<?>, Binding<?>> entry : entries) {
            Key<?> key = entry.getKey();
            Class<?> keyType = getKeyType(key);
            if (keyType != null && baseClass.isAssignableFrom(keyType)) {
                answer.add(entry.getValue());
            }
        }
        return answer;
    }

    /**
     * Returns the key type of the given key
     */
    public static <T> Class<?> getKeyType(Key<?> key) {
        Class<?> keyType = null;
        TypeLiteral<?> typeLiteral = key.getTypeLiteral();
        Type type = typeLiteral.getType();
        if (type instanceof Class) {
            keyType = (Class<?>) type;
        }
        return keyType;
    }

    public static void cleanCaches(Injector injector) {
        ((InjectorImpl) injector).clearCache();
        if (injector.getParent() != null) {
            cleanCaches(injector.getParent());
        }
    }
}
