/*
 * JBoss, Home of Professional Open Source
 * Copyright 2009, Red Hat Middleware LLC, 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.shrinkwrap.impl.base;

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.logging.Logger;

import org.jboss.shrinkwrap.api.Archive;
import org.jboss.shrinkwrap.api.ArchiveFormat;
import org.jboss.shrinkwrap.api.Assignable;
import org.jboss.shrinkwrap.api.ClassLoaderSearchUtilDelegator;
import org.jboss.shrinkwrap.api.ConfigurationBuilder;
import org.jboss.shrinkwrap.api.ExtensionLoader;
import org.jboss.shrinkwrap.api.UnknownExtensionTypeException;
import org.jboss.shrinkwrap.api.UnknownExtensionTypeExceptionDelegator;

/**
 * ServiceExtensionLoader
 *
 * This class is the default strategy to load extensions when an instance of {@link ExtensionLoader} is not provided to
 * the {@link ConfigurationBuilder} and the {@link ConfigurationBuilder#build()} method is invoked. If the
 * {@link ConfigurationBuilder} doesn't provide any {@link ClassLoader}, {@link ConfigurationBuilder#build()} defaults
 * to a one-element collection holding the TCCL. The {@link ServiceExtensionLoader#classLoaders} are used to find the
 * provider-configuration file for the extension to be loaded in META-INF/services/. This provider-configuration file is
 * used to make an instance of the SPI implementation and cached in {@link ServiceExtensionLoader#cache}.
 *
 * @author <a href="mailto:aslak@conduct.no">Aslak Knutsen</a>
 * @author <a href="mailto:ken@glxn.net">Ken Gullaksen</a>
 * @version $Revision: $
 */
public class ServiceExtensionLoader implements ExtensionLoader {
    // -------------------------------------------------------------------------------------||
    // Class Members ----------------------------------------------------------------------||
    // -------------------------------------------------------------------------------------||

    /**
     * Logger
     */
    @SuppressWarnings("unused")
    private static final Logger log = Logger.getLogger(ServiceExtensionLoader.class.getName());

    // -------------------------------------------------------------------------------------||
    // Instance Members -------------------------------------------------------------------||
    // -------------------------------------------------------------------------------------||

    private Map<Class<?>, Class<?>> cache = new HashMap<Class<?>, Class<?>>();
    private Map<Class<?>, ExtensionWrapper> extensionMappings = new HashMap<Class<?>, ExtensionWrapper>();

    /**
     * ClassLoader used for loading extensions
     */
    private final Iterable<ClassLoader> classLoaders;

    // -------------------------------------------------------------------------------------||
    // Constructor ------------------------------------------------------------------------||
    // -------------------------------------------------------------------------------------||

    /**
     * Creates a new instance, using the specified {@link ClassLoader}s to create extensions
     *
     * @param classLoaders
     * @throws IllegalArgumentException
     *             If the {@link ClassLoader} is not specified
     */
    public ServiceExtensionLoader(final Iterable<ClassLoader> classLoaders) throws IllegalArgumentException {
        if (classLoaders == null) {
            throw new IllegalArgumentException("ClassLoader must be specified");
        }
        this.classLoaders = classLoaders;
    }

    // -------------------------------------------------------------------------------------||
    // Required Implementations - ExtensionLoader -----------------------------------------||
    // -------------------------------------------------------------------------------------||

    /**
     * {@inheritDoc}
     *
     * @see org.jboss.shrinkwrap.api.ExtensionLoader#load(java.lang.Class, org.jboss.shrinkwrap.api.Archive)
     */
    @Override
    public <T extends Assignable> T load(Class<T> extensionClass, Archive<?> baseArchive)
        throws UnknownExtensionTypeException {
        if (isCached(extensionClass)) {
            return createFromCache(extensionClass, baseArchive);
        }
        T object = createFromLoadExtension(extensionClass, baseArchive);

        addToCache(extensionClass, object.getClass());

        return object;
    }

    // -------------------------------------------------------------------------------------||
    // Internal Helper Methods - Cache ----------------------------------------------------||
    // -------------------------------------------------------------------------------------||

    boolean isCached(Class<?> extensionClass) {
        return cache.containsKey(extensionClass);
    }

    private <T extends Assignable> T createFromCache(Class<T> extensionClass, Archive<?> archive) {
        Class<T> extensionImplClass = getFromCache(extensionClass);
        return createExtension(extensionImplClass, archive);
    }

    void addToCache(Class<?> extensionClass, Class<?> extensionImplClass) {
        cache.put(extensionClass, extensionImplClass);
    }

    @SuppressWarnings("unchecked")
    <T extends Assignable> Class<T> getFromCache(Class<T> extensionClass) {
        return (Class<T>) cache.get(extensionClass);
    }

    // -------------------------------------------------------------------------------------||
    // Internal Helper Methods - Override -------------------------------------------------||
    // -------------------------------------------------------------------------------------||

    /**
     * {@inheritDoc}
     *
     * @see org.jboss.shrinkwrap.api.ExtensionLoader#addOverride(java.lang.Class, java.lang.Class)
     */
    public <T extends Assignable> ServiceExtensionLoader addOverride(final Class<T> extensionClass,
        final Class<? extends T> extensionImplClass) {
        addToCache(extensionClass, extensionImplClass);
        return this;
    }

    /**
     * {@inheritDoc}
     *
     * @see org.jboss.shrinkwrap.api.ExtensionLoader#getExtensionFromExtensionMapping(java.lang.Class)
     */
    public <T extends Assignable> String getExtensionFromExtensionMapping(final Class<T> type) {
        ExtensionWrapper extensionWrapper = extensionMappings.get(type);
        if (extensionWrapper == null) {
            loadExtensionMapping(type);
        }
        extensionWrapper = extensionMappings.get(type);
        if (extensionWrapper == null) {
            throw UnknownExtensionTypeExceptionDelegator.newExceptionInstance(type);
        }
        return extensionWrapper.getProperty("extension");
    }

    /**
     * {@inheritDoc}
     *
     * @see org.jboss.shrinkwrap.api.ExtensionLoader#getArchiveFormatFromExtensionMapping(java.lang.Class)
     */
    public <T extends Archive<T>> ArchiveFormat getArchiveFormatFromExtensionMapping(final Class<T> type) {
        ExtensionWrapper extensionWrapper = extensionMappings.get(type);
        if (extensionWrapper == null) {
            loadExtensionMapping(type);
        }
        extensionWrapper = extensionMappings.get(type);
        if (extensionWrapper == null) {
            throw UnknownExtensionTypeExceptionDelegator.newExceptionInstance(type);
        }
        String archiveFormat = extensionWrapper.getProperty("archiveFormat");
        return ArchiveFormat.valueOf(archiveFormat);
    }

    /**
     * Check to see if a specific extension interface is beeing overloaded
     *
     * @param extensionClass
     *            The ExtensionType interface class
     * @return true if found
     */
    public boolean isOverriden(Class<?> extensionClass) {
        return isCached(extensionClass);
    }

    // -------------------------------------------------------------------------------------||
    // Internal Helper Methods - Loading --------------------------------------------------||
    // -------------------------------------------------------------------------------------||

    /**
     * Creates a new instance of a <code>extensionClass</code> implementation. The implementation class is found in a
     * provider-configuration file in META-INF/services/
     *
     * @param <T>
     * @param extensionClass
     * @param archive
     * @return an instance of the <code>extensionClass</code>' implementation.
     */
    private <T extends Assignable> T createFromLoadExtension(Class<T> extensionClass, Archive<?> archive) {
        ExtensionWrapper extensionWrapper = loadExtensionMapping(extensionClass);
        if (extensionWrapper == null) {
            throw new RuntimeException("Failed to load ExtensionMapping");
        }

        Class<T> extensionImplClass = loadExtension(extensionWrapper);

        if (!extensionClass.isAssignableFrom(extensionImplClass)) {
            throw new RuntimeException("Found extension impl class " + extensionImplClass.getName()
                + " not assignable to extension interface " + extensionClass.getName());
        }
        return createExtension(extensionImplClass, archive);
    }

    /**
     * Loads the implementation class hold in {@link ExtensionWrapper#implementingClassName}
     *
     * @param <T>
     * @param extensionWrapper
     * @return
     */
    private <T extends Assignable> Class<T> loadExtension(ExtensionWrapper extensionWrapper) {
        return loadExtensionClass(extensionWrapper.implementingClassName);
    }

    /**
     * Finds the SPI configuration, wraps it into a {@link ExtensionWrapper} and loads it to
     * {@link ServiceExtensionLoader#extensionMappings}.
     *
     * @param <T>
     * @param extensionClass
     * @return
     */
    private <T extends Assignable> ExtensionWrapper loadExtensionMapping(Class<T> extensionClass) {
        final InputStream extensionStream = findExtensionImpl(extensionClass);

        ExtensionWrapper extensionWrapper = loadExtensionWrapper(extensionStream, extensionClass);
        this.extensionMappings.put(extensionClass, extensionWrapper);
        return extensionWrapper;
    }

    /**
     * Iterates through the classloaders to load the provider-configuration file for <code>extensionClass</code> in
     * META-INF/services/ using its binary name.
     *
     * @param <T>
     * @param extensionClass
     *            SPI type for which the configuration file is looked for
     * @return An {@link InputStream} representing <code>extensionClass</code>'s configuration file
     * @throws RuntimeException
     *             if it doesn't find a provider-configuration file for <code>extensionClass</code>
     * @throws UnknownExtensionTypeExceptionDelegator
     */
    private <T extends Assignable> InputStream findExtensionImpl(final Class<T> extensionClass) {
        try {
            // Add all extension impls found in all CLs
            for (final ClassLoader cl : this.getClassLoaders()) {
                final InputStream stream = cl.getResourceAsStream("META-INF/services/" + extensionClass.getName());
                if (stream != null) {
                    return stream;
                }
            }

            // None found
            throw new RuntimeException("No extension implementation found for " + extensionClass.getName()
                + ", please verify classpath or add a extensionOverride");
        } catch (Exception e) {
            throw UnknownExtensionTypeExceptionDelegator.newExceptionInstance(extensionClass);
        }
    }

    /**
     * Wraps the provider-configuration file <code>extensionStream</code>, the SPI <code>extensionClass</code> and its
     * implementation class name into a {@link ExtensionWrapper} instance.
     *
     * @param <T>
     * @param extensionStream
     *            - a bytes stream representation of the provider-configuration file
     * @param extensionClass
     *            - SPI type
     * @return a {@link ExtensionWrapper} instance
     */
    private <T extends Assignable> ExtensionWrapper loadExtensionWrapper(final InputStream extensionStream,
        Class<T> extensionClass) {
        Properties properties = new Properties();
        try {
            properties.load(extensionStream);
        } catch (IOException e) {
            throw new RuntimeException("Could not open stream for extensionURL " + extensionStream, e);
        }
        String implementingClassName = (String) properties.get("implementingClassName");
        if (implementingClassName == null) {
            throw new RuntimeException("Property implementingClassName is not present in " + extensionStream);
        }
        final Map<String, String> map = new HashMap<String, String>(properties.size());
        final Enumeration<Object> keys = properties.keys();
        while (keys.hasMoreElements()) {
            final String key = (String) keys.nextElement();
            final String value = (String) properties.get(key);
            map.put(key, value);
        }
        return new ExtensionWrapper(implementingClassName, map, extensionClass);
    }

    /**
     * Delegates class loading of <code>extensionClassName</code> to
     * {@link ClassLoaderSearchUtilDelegator#findClassFromClassLoaders(String, Iterable)} passing the
     * <code>extensionClassName</code> and the instance's <code>classLoaders</code>.
     *
     * @param <T>
     * @param extensionClassName
     * @return
     */
    @SuppressWarnings("unchecked")
    private <T extends Assignable> Class<T> loadExtensionClass(String extensionClassName) {
        try {
            return (Class<T>) ClassLoaderSearchUtilDelegator.findClassFromClassLoaders(extensionClassName,
                getClassLoaders());
        } catch (final ClassNotFoundException e) {
            throw new RuntimeException("Could not load class " + extensionClassName, e);
        }
    }

    /**
     * Creates an instance of <code>extensionImplClass</code> using <code>archive</code> as the parameter for its
     * one-argument list constructor.
     *
     * @param <T>
     * @param extensionImplClass
     * @param archive
     * @return
     */
    private <T extends Assignable> T createExtension(Class<T> extensionImplClass, Archive<?> archive) {

        T extension;
        Constructor<T> extensionImplConstructor = findConstructor(extensionImplClass);

        @SuppressWarnings("unchecked")
        Class<T> constructorArg = (Class<T>) extensionImplConstructor.getParameterTypes()[0];
        try {

            if (constructorArg.isInstance(archive)) {
                extension = extensionImplConstructor.newInstance(archive);
            } else {
                extension = extensionImplConstructor.newInstance(load(constructorArg, archive));
            }
        } catch (InstantiationException e) {
            throw new ExtensionLoadingException("Failed to instantiate class of type " + archive.getClass()
                + ". The underlying class can not be abstract.", e);
        } catch (IllegalAccessException e) {
            throw new ExtensionLoadingException("Failed to instantiate class of type " + archive.getClass()
                + ". The underlying constructor is inaccessible.", e);
        } catch (InvocationTargetException e) {
            throw new ExtensionLoadingException("Failed to instantiate class of type " + archive.getClass()
                + ". The underlying constructor threw an exception.", e);
        }

        return extension;
    }

    /**
     * Finds a constructor with a one-argument list's element which implements {@link Archive}.
     *
     * @param <T>
     * @param extensionImplClass
     *            - Implementation of {@link Assignable} with a one-argument list's element which implements
     *            {@link Archive}.
     * @return
     */
    @SuppressWarnings("unchecked")
    private <T extends Assignable> Constructor<T> findConstructor(Class<T> extensionImplClass) {
        Constructor<?>[] constructors = SecurityActions.getConstructors(extensionImplClass);
        for (Constructor<?> constructor : constructors) {
            Class<?>[] parameters = constructor.getParameterTypes();
            if (parameters.length != 1) {
                continue;
            }
            Class<?> parameter = parameters[0];
            if (Archive.class.isAssignableFrom(parameter)) {
                return (Constructor<T>) constructor;
            }
        }
        throw new ExtensionLoadingException("No constructor with a single argument of type " + Archive.class.getName()
            + " could be found");
    }

    private Iterable<ClassLoader> getClassLoaders() {
        return this.classLoaders;
    }
}
