/*
 * 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.cli.lister;

import java.io.File;
import java.lang.annotation.Annotation;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Collection;
import java.util.List;
import java.util.Set;

import org.apache.brooklyn.api.entity.Application;
import org.apache.brooklyn.api.entity.Entity;
import org.apache.brooklyn.api.objs.BrooklynObject;
import org.apache.brooklyn.api.policy.Policy;
import org.apache.brooklyn.api.sensor.Enricher;
import org.apache.brooklyn.core.enricher.AbstractEnricher;
import org.apache.brooklyn.core.entity.AbstractApplication;
import org.apache.brooklyn.core.entity.AbstractEntity;
import org.apache.brooklyn.core.policy.AbstractPolicy;
import org.apache.brooklyn.entity.software.base.SoftwareProcessImpl;
import org.apache.brooklyn.util.core.ResourceUtils;
import org.apache.brooklyn.util.core.javalang.UrlClassLoader;
import org.apache.brooklyn.util.net.Urls;
import org.apache.brooklyn.util.os.Os;
import org.reflections.Reflections;
import org.reflections.scanners.FieldAnnotationsScanner;
import org.reflections.scanners.SubTypesScanner;
import org.reflections.scanners.TypeAnnotationsScanner;
import org.reflections.util.ConfigurationBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.annotations.Beta;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;

public class ClassFinder {

    private static final Logger log = LoggerFactory.getLogger(ClassFinder.class);

    private static final Collection<Class<?>> BORING = ImmutableList.<Class<?>>of(
            Entity.class,
            AbstractEntity.class,
            SoftwareProcessImpl.class,
            Application.class,
            AbstractApplication.class,
            Policy.class,
            Enricher.class,
            AbstractPolicy.class,
            AbstractEnricher.class);

    public static Predicate<Class<?>> notBoring() {
        return new Predicate<Class<?>>() {
            @Override
            public boolean apply(Class<?> input) {
                return (input != null && !BORING.contains(input));
            }
        };
    }
    
    public static Predicate<Class<?>> withAnnotation(final Class<? extends Annotation> annotation) {
        return new Predicate<Class<?>>() {
            @Override
            public boolean apply(Class<?> input) {
                return (input != null && input.getAnnotation(annotation) != null);
            }
        };
    }
    
    public static Predicate<? super Class<? extends BrooklynObject>> withClassNameMatching(final String typeRegex) {
        return new Predicate<Class<?>>() {
            @Override
            public boolean apply(Class<?> input) {
                return (input != null && input.getName() != null && input.getName().matches(typeRegex));
            }
        };
    }

    /** finds a jar at a url, or for directories, jars under a path */
    @Beta
    public static List<URL> toJarUrls(String url) throws MalformedURLException {
        if (url==null) throw new NullPointerException("Cannot read from null");
        if (url=="") throw new NullPointerException("Cannot read from empty string");
        
        List<URL> result = Lists.newArrayList();
        
        String protocol = Urls.getProtocol(url);
        if (protocol!=null) {
            // it's a URL - easy
            if ("file".equals(protocol)) {
                url = ResourceUtils.tidyFileUrl(url);
            }
            result.add(new URL(url));
        } else {
            // treat as file
            String tidiedPath = Os.tidyPath(url);
            File tidiedFile = new File(tidiedPath);
            if (tidiedFile.isDirectory()) {
                List<File> toscan = Lists.newLinkedList();
                toscan.add(tidiedFile);
                while (toscan.size() > 0) {
                    File file = toscan.remove(0);
                    if (file.isFile()) {
                        if (file.getName().toLowerCase().endsWith(".jar")) {
                            result.add(new URL("file://"+file.getAbsolutePath()));
                        }
                    } else if (file.isDirectory()) {
                        for (File subfile : file.listFiles()) {
                            toscan.add(subfile);
                        }
                    } else {
                        log.info("Cannot read "+file+"; not a file or directory");
                    }
                }
            } else {
                result.add(tidiedFile.toURI().toURL());
            }
        }
        
        return result;
    }

    public static <T extends BrooklynObject> Set<Class<? extends T>> findClasses(Collection<URL> urls, Class<T> clazz) {
        ClassLoader classLoader = new UrlClassLoader(urls.toArray(new URL[urls.size()]));
        
        Reflections reflections = new Reflections(new ConfigurationBuilder()
                .addClassLoader(classLoader)
                .addScanners(new SubTypesScanner(), new TypeAnnotationsScanner(), new FieldAnnotationsScanner())
                .addUrls(urls));
        
        Set<Class<? extends T>> types = reflections.getSubTypesOf(clazz);
        
        return types;
    }
}
