/**
 * 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.openejb.javaee.api.activator.org.apache.openejb.javaee.api.locator;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Properties;
import java.util.Set;

@SuppressWarnings("UnusedDeclaration")
public class ProviderLocator {
    // our bundle context
    static private Object context;
    // a service tracker for the registry service
    // NB:  This is declared as just Object to avoid classloading issues if we're running
    // outside of an OSGi environment.
    static private Object registryTracker;

    private static Class<?> bundleContextClazz = null;
    private static Class<?> serviceTrackerClazz = null;

    static {
        final ClassLoader cl = Thread.currentThread().getContextClassLoader();
        try {
            bundleContextClazz = cl.loadClass("org.osgi.framework.BundleContext");
            serviceTrackerClazz = cl.loadClass("org.osgi.util.tracker.ServiceTracker");
        } catch (final ClassNotFoundException e) {
            e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
        }
    }

    private ProviderLocator() {
        // private constructor to prevent an instance from getting created.
    }

    /**
     * Utility class for locating a class with OSGi registry
     * support.  Uses the thread context classloader as part of
     * the search order.
     *
     * @param className The name of the target class.
     *
     * @return The loaded class.
     * @exception ClassNotFoundException
     *                   Thrown if the class cannot be located.
     */
    static public Class<?> loadClass(final String className) throws ClassNotFoundException {
        return loadClass(className, null, Thread.currentThread().getContextClassLoader());
    }

    /**
     * Utility class for locating a class with OSGi registry
     * support.  Uses the thread context classloader as part of
     * the search order.
     *
     * @param className The name of the target class.
     * @param contextClass The context class to retrive the classloader.
     *
     * @return The loaded class.
     * @exception ClassNotFoundException
     *                   Thrown if the class cannot be located.
     */
    static public Class<?> loadClass(final String className, final Class<?> contextClass) throws ClassNotFoundException {
        return loadClass(className, contextClass, Thread.currentThread().getContextClassLoader());
    }

    /**
     * Standardized utility method for performing class lookups
     * with support for OSGi registry lookups.
     *
     * @param className The name of the target class.
     * @param contextClass The context class to retrive the classloader.
     * @param loader    An optional class loader.
     *
     * @return The loaded class
     * @exception ClassNotFoundException
     *                   Thrown if the class cannot be loaded.
     */
    static public Class<?> loadClass(final String className, final Class<?>contextClass, ClassLoader loader) throws ClassNotFoundException {
        if (loader != null) {
            try {
                return loader.loadClass(className);
            } catch (final ClassNotFoundException x) {
                //ignore
            }
        }
        if (contextClass != null) {
            loader = contextClass.getClassLoader();
        }
        // try again using the class context loader
        return Class.forName(className, true, loader);
    }


    /**
     * Get a single service instance that matches an interface
     * definition.
     *
     * @param iface  The name of the required interface.
     * @param contextClass
     *               The class requesting the lookup (used for class resolution).
     * @param loader A class loader to use for searching for service definitions
     *               and loading classes.
     *
     * @return The service instance, or null if no matching services
     *         can be found.
     * @exception Exception Thrown for any classloading or exceptions thrown
     *                      trying to instantiate a service instance.
     */
    static public Object getService(final String iface, final Class<?> contextClass, final ClassLoader loader) throws Exception {
        // try for a classpath locatable instance next.  If we find an appropriate class mapping,
        // create an instance and return it.
        final Class<?> cls = locateServiceClass(iface, contextClass, loader);
        if (cls != null) {
            return cls.newInstance();
        }
        // a provider was not found
        return null;
    }


    /**
     * Locate a service class that matches an interface
     * definition.
     *
     * @param iface  The name of the required interface.
     * @param contextClass
     *               The class requesting the lookup (used for class resolution).
     * @param loader A class loader to use for searching for service definitions
     *               and loading classes.
     *
     * @return The located class, or null if no matching services
     *         can be found.
     * @exception ClassNotFoundException Thrown for any classloading exceptions thrown
     *                                   trying to load the class.
     */
    static public Class<?> getServiceClass(final String iface, final Class<?> contextClass, final ClassLoader loader) throws ClassNotFoundException {
        return locateServiceClass(iface, contextClass, loader);
    }


    /**
     * Get a list of services that match a given interface
     * name.  This searches both the current class path and
     * the global repository for matches.
     *
     * @param iface  The name of the required interface.
     * @param contextClass
     *               The class requesting the lookup (used for class resolution).
     * @param loader A class loader to use for searching for service definitions
     *               and loading classes.
     *
     * @return A list of matching services.  Returns an empty list if there
     *         are no matches.
     * @exception Exception Thrown for any classloading or exceptions thrown
     *                      trying to instantiate a service instance.
     */
    static public List<Object> getServices(final String iface, final Class<?> contextClass, final ClassLoader loader) throws Exception {
        final List<Object> services = new ArrayList<Object>();

        // try for a classpath locatable instance second.  If we find an appropriate class mapping,
        // create an instance and return it.
        final Collection<Class<?>> classes = locateServiceClasses(iface, contextClass, loader);
        if (classes != null) {
            // create an instance of each of these classes
            for (final Class<?> cls : classes) {
                services.add(cls.newInstance());
            }
        }

        // now return the merged set
        return services;
    }


    /**
     * Get a list of service class implementations that match
     * an interface name.  This searches both the current class path and
     * the global repository for matches.
     *
     * @param iface  The name of the required interface.
     * @param contextClass
     *               The class requesting the lookup (used for class resolution).
     * @param loader A class loader to use for searching for service definitions
     *               and loading classes.
     *
     * @return A list of matching provider classes.  Returns an empty list if there
     *         are no matches.
     * @exception Exception Thrown for any classloading exceptions thrown
     *                      trying to load a provider class.
     */
    static public List<Class<?>> getServiceClasses(final String iface, final Class<?> contextClass, final ClassLoader loader) throws Exception {
        final Set<Class<?>> serviceClasses = new LinkedHashSet<Class<?>>();

        // try for a classpath locatable classes second.  If we find an appropriate class mapping,
        // add this to our return collection.
        final Collection<Class<?>> classes = locateServiceClasses(iface, contextClass, loader);
        if (classes != null) {
            serviceClasses.addAll(classes);
        }
        // now return the merged set
        return new ArrayList(serviceClasses);
    }


    /**
     * Locate the first class name for a META-INF/services definition
     * of a given class.  The first matching provider is
     * returned.
     *
     * @param iface  The interface class name used for the match.
     * @param loader The classloader for locating resources.
     *
     * @return The mapped provider name, if found.  Returns null if
     *         no mapping is located.
     */
    static private String locateServiceClassName(final String iface, final Class<?> contextClass, final ClassLoader loader) {
        // search first with the loader class path
        String name = locateServiceClassName(iface, loader);
        if (name != null) {
            return name;
        }
        // then with the context class, if there is one
        if (contextClass != null) {
            name = locateServiceClassName(iface, contextClass.getClassLoader());
            if (name != null) {
                return name;
            }
        }
        // not found
        return null;
    }


    /**
     * Locate a classpath-define service mapping.
     *
     * @param iface  The required interface name.
     * @param loader The ClassLoader instance to use to locate the service.
     *
     * @return The mapped class name, if one is found.  Returns null if the
     *         mapping is not located.
     */
    static private String locateServiceClassName(final String iface, final ClassLoader loader) {
        if (loader != null) {
            try {
                // we only look at resources that match the file name, using the specified loader
                final String service = "META-INF/services/" + iface;
                final Enumeration<URL> providers = loader.getResources(service);

                while (providers.hasMoreElements()) {
                    final List<String>providerNames = parseServiceDefinition(providers.nextElement());
                    // if there is something defined here, return the first entry
                    if (!providerNames.isEmpty()) {
                        return providerNames.get(0);
                    }
                }
            } catch (final IOException e) {
                //ignore
            }
        }
        // not found
        return null;
    }


    /**
     * Locate the first class for a META-INF/services definition
     * of a given interface class.  The first matching provider is
     * returned.
     *
     * @param iface  The interface class name used for the match.
     * @param loader The classloader for locating resources.
     *
     * @return The mapped provider class, if found.  Returns null if
     *         no mapping is located.
     */
    static private Class<?> locateServiceClass(final String iface, final Class<?> contextClass, final ClassLoader loader) throws ClassNotFoundException {
        final String className = locateServiceClassName(iface, contextClass, loader);
        if (className == null) {
            return null;
        }

        // we found a name, try loading the class.  This will throw an exception if there is an error
        return loadClass(className, contextClass, loader);
    }


    /**
     * Locate all class names name for a META-INF/services definition
     * of a given class.
     *
     * @param iface  The interface class name used for the match.
     * @param loader The classloader for locating resources.
     *
     * @return The mapped provider name, if found.  Returns null if
     *         no mapping is located.
     */
    static private Collection<String> locateServiceClassNames(final String iface, final Class<?> contextClass, final ClassLoader loader) {
        final Set<String> names = new LinkedHashSet<String>();

        locateServiceClassNames(iface, loader, names);
        if (contextClass != null) {
            locateServiceClassNames(iface, contextClass.getClassLoader(), names);
        }

        return names;
    }


    /**
     * Locate all class names name for a META-INF/services definition
     * of a given class.
     *
     * @param iface  The interface class name used for the match.
     * @param loader The classloader for locating resources.
     *
     * @return The mapped provider name, if found.  Returns null if
     *         no mapping is located.
     */
    static void locateServiceClassNames(final String iface, final ClassLoader loader, final Set names) {
        if (loader != null) {

            try {
                // we only look at resources that match the file name, using the specified loader
                final String service = "META-INF/services/" + iface;
                final Enumeration<URL> providers = loader.getResources(service);

                while (providers.hasMoreElements()) {
                    final List<String>providerNames = parseServiceDefinition(providers.nextElement());
                    // just add all of these to the list
                    names.addAll(providerNames);
                }
            } catch (final IOException e) {
            }
        }
    }


    /**
     * Locate all classes that map to a given provider class definition.  This will
     * search both the services directories, as well as the provider classes from the
     * OSGi provider registry.
     *
     * @param iface  The interface class name used for the match.
     * @param loader The classloader for locating resources.
     *
     * @return A list of all mapped classes, if found.  Returns an empty list if
     *         no mappings are found.
     */
    static private Collection<Class<?>> locateServiceClasses(final String iface, final Class<?> contextClass, final ClassLoader loader) throws ClassNotFoundException {
        // get the set of names from services definitions on the classpath
        final Collection<String> classNames = locateServiceClassNames(iface, contextClass, loader);
        final Set<Class<?>> classes = new LinkedHashSet<Class<?>>();

        // load each class and add to our return set
        for (final String name : classNames) {
            classes.add(loadClass(name, contextClass, loader));
        }
        return classes;
    }


    /**
     * Parse a definition file and return the names of all included implementation classes
     * contained within the file.
     *
     * @param u      The URL of the file
     *
     * @return A list of all matching classes.  Returns an empty list
     *         if no matches are found.
     */
    static private List<String> parseServiceDefinition(final URL u) {
        final String url = u.toString();
        final List<String> classes = new ArrayList<String>();
        // ignore directories
        if (url.endsWith("/")) {
            return classes;
        }
        // the identifier used for the provider is the last item in the URL.
        final String providerId = url.substring(url.lastIndexOf("/") + 1);
        try {
            final BufferedReader br = new BufferedReader(new InputStreamReader(u.openStream(), "UTF-8"));
            // the file can be multiple lines long, with comments.  A single file can define multiple providers
            // for a single key, so we might need to create multiple entries.  If the file does not contain any
            // definition lines, then as a default, we use the providerId as an implementation class also.
            String line = br.readLine();
            while (line != null) {
                // we allow comments on these lines, and a line can be all comment
                final int comment = line.indexOf('#');
                if (comment != -1) {
                    line = line.substring(0, comment);
                }
                line = line.trim();
                // if there is nothing left on the line after stripping white space and comments, skip this
                if (line.length() > 0) {
                    // add this to our list
                    classes.add(line);
                }
                // keep reading until the end.
                line = br.readLine();
            }
            br.close();
        } catch (final IOException e) {
            // ignore errors and handle as default
        }
        return classes;
    }

    /**
     * Perform a service class discovery by looking for a
     * property in a target properties file located in the
     * java.home directory.
     *
     * @param path     The relative path to the desired properties file.
     * @param property The name of the required property.
     *
     * @return The value of the named property within the properties file.  Returns
     *         null if the property doesn't exist or the properties file doesn't exist.
     */
    public static String lookupByJREPropertyFile(final String path, final String property) throws IOException {
        final String jreDirectory = System.getProperty("java.home");
        final File configurationFile = new File(jreDirectory + File.separator + path);
        if (configurationFile.exists() && configurationFile.canRead()) {
            final Properties properties = new Properties();
            InputStream in = null;
            try {
                in = new FileInputStream(configurationFile);
                properties.load(in);
                return properties.getProperty(property);
            } finally {
                if (in != null) {
                    try {
                        in.close();
                    } catch (final Exception e) {
                        //ignore
                    }
                }
            }
        }
        return null;
    }
}
