/*
 * JBoss, Home of Professional Open Source
 * Copyright 2006, JBoss Inc., and individual contributors as indicated
 * by the @authors tag. See the copyright.txt in the distribution for a
 * full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */

package org.drools.repository;


import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Proxy;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


/**
 * Utility methods to aid in class/resource loading.
 */
public class ClassUtil
{
    private static Logger logger = LoggerFactory.getLogger(ClassUtil.class);

    /**
     * Load the specified class.
     * @param className The name of the class to load.
     * @param caller The class of the caller.
     * @return The specified class.
     * @throws ClassNotFoundException If the class cannot be found.
     */
    public static Class forName(final String className, final Class caller)
    throws ClassNotFoundException
    {
        final ClassLoader threadClassLoader = Thread.currentThread().getContextClassLoader() ;
        if (threadClassLoader != null)
        {
            try
            {
                return Class.forName(className, true, threadClassLoader) ;
            }
            catch (final ClassNotFoundException cnfe)
            {
                if (cnfe.getException() != null)
                {
                    throw cnfe ;
                }
            }
        }


        final ClassLoader classLoader = caller.getClassLoader() ;
        if (classLoader != null)
        {
            try
            {
                return Class.forName(className, true, classLoader) ;
            }
            catch (final ClassNotFoundException cnfe)
            {
                if (cnfe.getException() != null)
                {
                    throw cnfe ;
                }
            }
        }

        return Class.forName(className, true, ClassLoader.getSystemClassLoader()) ;
    }

    /**
     * Resolve a proxy for the specified interfaces.
     * @param interfaces The interfaces associated with the proxy.
     * @param caller The class of the caller.
     * @return The specified proxy class.
     * @throws ClassNotFoundException If the class cannot be found.
     */
    public static Class resolveProxy(final String[] interfaces, final Class caller)
    throws ClassNotFoundException
    {
        final int numInterfaces = (interfaces == null ? 0 : interfaces.length) ;
        if (numInterfaces == 0)
        {
            throw new ClassNotFoundException("Cannot generate proxy with no interfaces") ;
        }

        final Class[] interfaceClasses = new Class[numInterfaces] ;
        for(int count = 0 ; count < numInterfaces ; count++)
        {
            interfaceClasses[count] = forName(interfaces[count], caller) ;
        }

        final ClassLoader proxyClassLoader ;
        final ClassLoader threadClassLoader = Thread.currentThread().getContextClassLoader() ;
        if (threadClassLoader != null)
        {
            proxyClassLoader = threadClassLoader ;
        }
        else
        {
            final ClassLoader classLoader = caller.getClassLoader() ;
            if (classLoader != null)
            {
                proxyClassLoader = classLoader ;
            }
            else
            {
                proxyClassLoader = ClassLoader.getSystemClassLoader() ;
            }
        }

        return Proxy.getProxyClass(proxyClassLoader, interfaceClasses) ;
    }

    /**
     * Get the specified resource as a stream.
     * @param resourceName The name of the class to load.
     * @param caller The class of the caller.
     * @return The input stream for the resource or null if not found.
     */
    public static InputStream getResourceAsStream(final String resourceName, final Class caller)
    {
        final String resource ;
        if (resourceName.startsWith("/"))
        {
            resource = resourceName.substring(1) ;
        }
        else
        {
            final Package callerPackage = caller.getPackage() ;
            if (callerPackage != null)
            {
                resource = callerPackage.getName().replace('.', '/') + '/' + resourceName ;
            }
            else
            {
                resource = resourceName ;
            }
        }
        final ClassLoader threadClassLoader = Thread.currentThread().getContextClassLoader() ;
        if (threadClassLoader != null)
        {
            final InputStream is = threadClassLoader.getResourceAsStream(resource) ;
            if (is != null)
            {
                return is ;
            }
        }

        final ClassLoader classLoader = caller.getClassLoader() ;
        if (classLoader != null)
        {
            final InputStream is = classLoader.getResourceAsStream(resource) ;
            if (is != null)
            {
                return is ;
            }
        }

        return ClassLoader.getSystemResourceAsStream(resource) ;
    }

    public static URL getResource(final String resourceName, final Class<?> caller)
    {
        final String resource ;
        if (resourceName.startsWith("/"))
        {
            resource = resourceName.substring(1) ;
        }
        else
        {
            final Package callerPackage = caller.getPackage() ;
            if (callerPackage != null)
            {
                resource = callerPackage.getName().replace('.', '/') + '/' + resourceName ;
            }
            else
            {
                resource = resourceName ;
            }
        }
        final ClassLoader threadClassLoader = Thread.currentThread().getContextClassLoader() ;
        if (threadClassLoader != null)
        {
            final URL url = threadClassLoader.getResource(resource) ;
            if (url != null)
            {
                return url ;
            }
        }

        final ClassLoader classLoader = caller.getClassLoader() ;
        if (classLoader != null)
        {
            final URL url = classLoader.getResource(resource) ;
            if (url != null)
            {
                return url ;
            }
        }

        return ClassLoader.getSystemResource(resource) ;
    }


    public static List<URL> getResources(String resourcePath, Class<?> caller) throws IOException {
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();

        if(resourcePath.startsWith("/")) {
            resourcePath = resourcePath.substring(1);
        }

        if (classLoader != null) {
            return toList(classLoader.getResources(resourcePath));
        }

        classLoader = caller.getClassLoader();
        if (classLoader != null) {
            return toList(classLoader.getResources(resourcePath));
        }

        return new ArrayList<URL>();
    }

    private static <T> List<T> toList(Enumeration<T> objects) {
        List<T> theList = new ArrayList<T>();
        while(objects.hasMoreElements()) {
            theList.add(objects.nextElement());
        }
        return theList;
    }

    /**
     * Get a package name and convert it to a path value, so it can be used
     * in calls to methods like {@link #getResourceAsStream}.
     * <p/>
     * Adds a '/' prefix and converts all '." characters to '/'.  Doesn't add a
     * trailing slash.
     *
     * @param packageObj The package.
     * @return The package path.
     */
    public static String getPath(Package packageObj) {
        return "/" + packageObj.getName().replace('.', '/');
    }

    public static List<String> getResourceList(String regex, Class caller) {
        ClasspathResourceFilter filter = new ClasspathResourceFilter(regex);
        ClassLoader classLoader;

        classLoader = Thread.currentThread().getContextClassLoader();
        if(classLoader instanceof URLClassLoader) {
            filter.filter((URLClassLoader) classLoader);
        }
        classLoader = caller.getClassLoader();
        if(classLoader instanceof URLClassLoader) {
            filter.filter((URLClassLoader) classLoader);
        }

        return filter.getResourceList();
    }

    private static class ClasspathResourceFilter {

        private List<String> resourceList = new ArrayList<String>();
        private Pattern pattern;

        private ClasspathResourceFilter(String regex) {
            pattern = Pattern.compile(regex);
        }

        private void filter(URLClassLoader classLoader) {
            URL[] cpUrls = classLoader.getURLs();

            for (int i = 0; i < cpUrls.length; i++) {
                try {
                    File file = new File(cpUrls[i].toURI());
                    if(file.isDirectory()) {
                        searchClasspathDirTree(file, "");
                    } else {
                        searchArchive(file);
                    }
                } catch (URISyntaxException e) {
                    logger.warn("Error searching classpath resource URL '" + cpUrls[i] + "' for resource '" + pattern.pattern() + "': " + e.getMessage());
                } catch (IOException e) {
                    logger.warn("Error searching classpath resource URL '" + cpUrls[i] + "' for resource '" + pattern.pattern() + "': " + e.getMessage());
                }
            }
        }

        private void searchClasspathDirTree(File rootDir, String subDir) {
            File currentDir = new File(rootDir, subDir);
            File[] contents = currentDir.listFiles();

            for(File file: contents) {
                if(file.isDirectory()) {
                    String subSubDir = subDir + "/" + file.getName();
                    searchClasspathDirTree(rootDir, subSubDir);
                } else {
                    String resClasspathPath = file.toURI().toString().substring(rootDir.toURI().toString().length() - 1);
                    if(isToBeAdded(resClasspathPath)) {
                        resourceList.add(resClasspathPath);
                    }
                }
            }
        }

        private void searchArchive(File archiveFile) throws IOException {
            ZipFile zip = new ZipFile(archiveFile);
            Enumeration<? extends ZipEntry> entries = zip.entries();

            while (entries.hasMoreElements()) {
                ZipEntry entry = entries.nextElement();
                String resClasspathPath = "/" + entry.getName();
                if(isToBeAdded(resClasspathPath)) {
                    resourceList.add(resClasspathPath);
                }
            }
        }

        private boolean isToBeAdded(String resClasspathPath) {
            if(resourceList.contains(resClasspathPath)) {
                // Already in the list e.g. same resource in different archives...
                return false;
            }

            Matcher matcher = pattern.matcher(resClasspathPath);
            return matcher.matches();
        }

        private List<String> getResourceList() {
            return resourceList;
        }
    }
}

