/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF 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.apache.brooklyn.util.core.javalang;

import java.lang.annotation.Annotation;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;

import org.apache.brooklyn.util.text.Strings;
import org.reflections.ReflectionUtils;
import org.reflections.Reflections;
import org.reflections.Store;
import org.reflections.scanners.Scanner;
import org.reflections.scanners.SubTypesScanner;
import org.reflections.scanners.TypeAnnotationsScanner;
import org.reflections.util.ClasspathHelper;
import org.reflections.util.ConfigurationBuilder;
import org.reflections.util.FilterBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;

/** Facade on {@link Reflections} which logs warnings for unloadable classes but does not fail */
public class ReflectionScanner {

    private static final Logger log = LoggerFactory.getLogger(ReflectionScanner.class);
    
    protected final ClassLoader[] classLoaders;
    protected final Reflections reflections;

    /** as {@link #ReflectionScanner(Iterable, String, Predicate, ClassLoader...)} using the prefix as the base for the filter */
    public ReflectionScanner(
            final Iterable<URL> urlsToScan, 
            final String optionalPrefix,
            final ClassLoader ...classLoaders) {
        this(urlsToScan, optionalPrefix, 
            Strings.isNonEmpty(optionalPrefix) ? new FilterBuilder.Include(FilterBuilder.prefix(optionalPrefix)) : null,
            classLoaders);
    }
    
    /** scanner which will look in the given urls 
     * (or if those are null attempt to infer from the first entry in the classloaders,
     * although currently that seems to only pick up directories, not JAR's),
     * optionally filtering for the given prefix;
     * any or all arguments can be null to accept all (and use default classpath for classloading).
     **/
    public ReflectionScanner(
            final Iterable<URL> urlsToScan, 
            final String optionalPrefix,
            final Predicate<String> filter,
            final ClassLoader ...classLoaders) {
        if (Reflections.log==null) {
            Reflections.log = log;
        }
        reflections = new Reflections(new ConfigurationBuilder() {
            {
                if (urlsToScan!=null)
                    setUrls(ImmutableSet.copyOf(urlsToScan));
                else if (classLoaders.length>0 && classLoaders[0]!=null)
                    setUrls(
                            ClasspathHelper.forPackage(Strings.isNonEmpty(optionalPrefix) ? optionalPrefix : "",
                                    asClassLoaderVarArgs(classLoaders[0])));
                
                if (filter!=null) filterInputsBy(filter);

                Scanner typeScanner = new TypeAnnotationsScanner();
                if (filter!=null) typeScanner = typeScanner.filterResultsBy(filter);
                Scanner subTypeScanner = new SubTypesScanner();
                if (filter!=null) subTypeScanner = subTypeScanner.filterResultsBy(filter);
                setScanners(typeScanner, subTypeScanner);
                
                for (ClassLoader cl: classLoaders)
                    if (cl!=null) addClassLoader(cl);
            }
        });
        this.classLoaders = Iterables.toArray(Iterables.filter(Arrays.asList(classLoaders), Predicates.notNull()), ClassLoader.class);
    }

    private static ClassLoader[] asClassLoaderVarArgs(final ClassLoader classLoaderToSearch) {
        return classLoaderToSearch==null ? new ClassLoader[0] : new ClassLoader[] { classLoaderToSearch };
    }

    public Store getStore() {
        return reflections.getStore();
    }

    /** overrides delegate so as to log rather than throw exception if a class cannot be loaded */
    public <T> Set<Class<? extends T>> getSubTypesOf(final Class<T> type) {
        return ImmutableSet.copyOf(reflections.getSubTypesOf(type));
    }
    
    /** overrides delegate so as to log rather than throw exception if a class cannot be loaded */
    public Set<Class<?>> getTypesAnnotatedWith(Class<? extends Annotation> annotation) {
        // Second parameter to getTypesAnnotatedWith instructs reflections to honour the
        // Inherited meta-annotation. When `false` the returned set includes unannotated
        // classes that implement annotated interfaces. In practice this means that the
        // Brooklyn catalog would include entity implementations.
        return ImmutableSet.copyOf(reflections.getTypesAnnotatedWith(annotation, true));
    }

    @SuppressWarnings("unchecked")
    protected <T> List<Class<? extends T>> forNames(Set<String> classNames, final String context) {
        List<Class<? extends T>> result = new ArrayList<Class<? extends T>>();
        for (String className : classNames) {
            //noinspection unchecked
            try {
                Class<? extends T> clazz = (Class<? extends T>) loadClass(className);
                if (clazz != null) {
                    result.add(clazz);
                } else {
                    log.warn("Unable to instantiate '"+className+"' ("+context+")");
                }
            } catch (Throwable e) {
                log.warn("Unable to instantiate '"+className+"' ("+context+"): "+e);
            }
        }
        return result;
    }
    
    protected Class<?> loadClass(String className) {
        return ReflectionUtils.forName(className, classLoaders);
    }

}
