/*
 * JBoss, Home of Professional Open Source
 * Copyright 2011, Red Hat, Inc. and/or its affiliates, and individual
 * contributors by the @authors tag. See the copyright.txt in the
 * distribution for a full listing of individual contributors.
 *
 * 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.jboss.solder.util.service;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import org.jboss.solder.logging.Logger;

/**
 * This class handles looking up service providers on the class path. It
 * implements the <a href="http://java.sun.com/javase/6/docs/technotes/guides/jar/jar.html#Service%20Provider"
 * >Service Provider section of the JAR File Specification</a>.
 * <p/>
 * The Service Provider programmatic lookup was not specified prior to Java 6 so
 * this interface allows use of the specification prior to Java 6.
 * <p/>
 * The API is copied from <a
 * href="http://java.sun.com/javase/6/docs/api/java/util/ServiceLoader.html"
 * >java.util.ServiceLoader</a>
 *
 * @author Pete Muir
 * @author <a href="mailto:dev@avalon.apache.org">Avalon Development Team</a>
 * @author Nicklas Karlsson
 */
public class ServiceLoader<S> implements Iterable<S> {
    private static final String SERVICES = "META-INF/services";

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

    /**
     * Creates a new service loader for the given service type, using the current
     * thread's context class loader.
     * <p/>
     * An invocation of this convenience method of the form
     * <p/>
     * {@code ServiceLoader.load(service)</code>}
     * <p/>
     * is equivalent to
     * <p/>
     * <code>ServiceLoader.load(service,
     * Thread.currentThread().getContextClassLoader())</code>
     *
     * @param service The interface or abstract class representing the service
     * @return A new service loader
     */
    public static <S> ServiceLoader<S> load(Class<S> service) {
        return load(service, Thread.currentThread().getContextClassLoader());
    }

    /**
     * Creates a new service loader for the given service type and class loader.
     *
     * @param service The interface or abstract class representing the service
     * @param loader  The class loader to be used to load provider-configuration
     *                files and provider classes, or null if the system class loader
     *                (or, failing that, the bootstrap class loader) is to be used
     * @return A new service loader
     */
    public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader) {
        if (loader == null) {
            loader = service.getClassLoader();
        }
        return new ServiceLoader<S>(service, loader);
    }

    /**
     * Creates a new service loader for the given service type, using the
     * extension class loader.
     * <p/>
     * This convenience method simply locates the extension class loader, call it
     * extClassLoader, and then returns
     * <p/>
     * <code>ServiceLoader.load(service, extClassLoader)</code>
     * <p/>
     * If the extension class loader cannot be found then the system class loader
     * is used; if there is no system class loader then the bootstrap class
     * loader is used.
     * <p/>
     * This method is intended for use when only installed providers are desired.
     * The resulting service will only find and load providers that have been
     * installed into the current Java virtual machine; providers on the
     * application's class path will be ignored.
     *
     * @param service The interface or abstract class representing the service
     * @return A new service loader
     */
    public static <S> ServiceLoader<S> loadInstalled(Class<S> service) {
        throw new UnsupportedOperationException("Not implemented");
    }

    private final String serviceFile;
    private Class<S> expectedType;
    private final ClassLoader loader;

    private Set<S> providers;

    private ServiceLoader(Class<S> service, ClassLoader loader) {
        this.loader = loader;
        this.serviceFile = SERVICES + "/" + service.getName();
        this.expectedType = service;
    }

    /**
     * Clear this loader's provider cache so that all providers will be reloaded.
     * <p/>
     * After invoking this method, subsequent invocations of the iterator method
     * will lazily look up and instantiate providers from scratch, just as is
     * done by a newly-created loader.
     * <p/>
     * This method is intended for use in situations in which new providers can
     * be installed into a running Java virtual machine.
     */
    public void reload() {
        providers = new HashSet<S>();

        for (URL serviceFile : loadServiceFiles()) {
            loadServiceFile(serviceFile);
        }
    }

    private List<URL> loadServiceFiles() {
        List<URL> serviceFiles = new ArrayList<URL>();
        try {
            Enumeration<URL> serviceFileEnumerator = loader.getResources(serviceFile);
            while (serviceFileEnumerator.hasMoreElements()) {
                serviceFiles.add(serviceFileEnumerator.nextElement());
            }
        } catch (IOException e) {
            throw new RuntimeException("Could not load resources from " + serviceFile, e);
        }
        return serviceFiles;
    }

    private void loadServiceFile(URL serviceFile) {
        InputStream is = null;
        try {
            is = serviceFile.openStream();
            BufferedReader reader = new BufferedReader(new InputStreamReader(is, "UTF-8"));
            String serviceClassName = null;
            while ((serviceClassName = reader.readLine()) != null) {
                serviceClassName = trim(serviceClassName);
                if (serviceClassName.length() > 0) {
                    loadService(serviceClassName);
                }
            }
        } catch (IOException e) {
            // FIXME: correct exception
            throw new RuntimeException("Could not read services file " + serviceFile);
        } finally {
            if (is != null) {
                try {
                    is.close();
                } catch (IOException e) {
                    // FIXME: correct exception
                    throw new RuntimeException("Could not close services file " + serviceFile, e);
                }
            }
        }
    }

    private String trim(String line) {
        final int comment = line.indexOf('#');

        if (comment > -1) {
            line = line.substring(0, comment);
        }
        return line.trim();
    }

    private void loadService(String serviceClassName) {
        Class<? extends S> serviceClass = loadClass(serviceClassName);
        if (serviceClass == null) {
            return;
        }
        S serviceInstance = prepareInstance(serviceClass);
        if (serviceInstance == null) {
            return;
        }
        providers.add(serviceInstance);
    }

    private Class<? extends S> loadClass(String serviceClassName) {
        Class<?> clazz = null;
        Class<? extends S> serviceClass = null;
        try {
            clazz = loader.loadClass(serviceClassName);
            serviceClass = clazz.asSubclass(expectedType);
        } catch (ClassNotFoundException e) {
            log.warn("Could not load service class " + serviceClassName);
        } catch (ClassCastException e) {
            throw new RuntimeException("Service class " + serviceClassName + " didn't implement the Extension interface");
        }
        return serviceClass;
    }

    private S prepareInstance(Class<? extends S> serviceClass) {
        try {
            // TODO Support the SM
            Constructor<? extends S> constructor = serviceClass.getDeclaredConstructor();
            constructor.setAccessible(true);
            return constructor.newInstance();
        } catch (NoClassDefFoundError e) {
            log.warn("Could not instantiate service class " + serviceClass.getName(), e);
            return null;
        } catch (InvocationTargetException e) {
            throw new RuntimeException("Error instantiating " + serviceClass, e.getCause());
        } catch (IllegalArgumentException e) {
            throw new RuntimeException("Error instantiating " + serviceClass, e);
        } catch (InstantiationException e) {
            throw new RuntimeException("Error instantiating " + serviceClass, e);
        } catch (IllegalAccessException e) {
            throw new RuntimeException("Error instantiating " + serviceClass, e);
        } catch (SecurityException e) {
            throw new RuntimeException("Error instantiating " + serviceClass, e);
        } catch (NoSuchMethodException e) {
            throw new RuntimeException("Error instantiating " + serviceClass, e);
        }
    }

    /**
     * Lazily loads the available providers of this loader's service.
     * <p/>
     * The iterator returned by this method first yields all of the elements of
     * the provider cache, in instantiation order. It then lazily loads and
     * instantiates any remaining providers, adding each one to the cache in
     * turn.
     * <p/>
     * To achieve laziness the actual work of parsing the available
     * provider-configuration files and instantiating providers must be done by
     * the iterator itself. Its hasNext and next methods can therefore throw a
     * ServiceConfigurationError if a provider-configuration file violates the
     * specified format, or if it names a provider class that cannot be found and
     * instantiated, or if the result of instantiating the class is not
     * assignable to the service type, or if any other kind of exception or error
     * is thrown as the next provider is located and instantiated. To write
     * robust code it is only necessary to catch ServiceConfigurationError when
     * using a service iterator.
     * <p/>
     * If such an error is thrown then subsequent invocations of the iterator
     * will make a best effort to locate and instantiate the next available
     * provider, but in general such recovery cannot be guaranteed.
     * <p/>
     * Design Note Throwing an error in these cases may seem extreme. The
     * rationale for this behavior is that a malformed provider-configuration
     * file, like a malformed class file, indicates a serious problem with the
     * way the Java virtual machine is configured or is being used. As such it is
     * preferable to throw an error rather than try to recover or, even worse,
     * fail silently.
     * <p/>
     * The iterator returned by this method does not support removal. Invoking
     * its remove method will cause an UnsupportedOperationException to be
     * thrown.
     *
     * @return An iterator that lazily loads providers for this loader's service
     */
    public Iterator<S> iterator() {
        if (providers == null) {
            reload();
        }
        return providers.iterator();
    }

    /**
     * Returns a string describing this service.
     *
     * @return A descriptive string
     */
    @Override
    public String toString() {
        return "Services for " + serviceFile;
    }
}
