/**
 * This file is part of muCommander, http://www.mucommander.com
 *
 * muCommander 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 3 of the License, or
 * (at your option) any later version.
 *
 * muCommander 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 program.  If not, see <http://www.gnu.org/licenses/>.
 */


package com.mucommander.commons.file.util;

import com.mucommander.commons.file.AbstractFile;
import com.mucommander.commons.file.FileFactory;
import com.mucommander.commons.file.archive.AbstractArchiveFile;
import com.mucommander.commons.file.protocol.local.LocalFile;

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

import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.net.URLDecoder;
import java.util.Enumeration;

/**
 * This class provides methods to load resources located within the reach of a <code>ClassLoader</code>. Those resources
 * can reside either in a JAR file or in a local directory that are in the classpath -- all methods of this class are
 * agnostic to either type of location.
 *
 * <p>The <code>getResourceAsURL</code> and <code>getResourceAsStream</code> methods are akin to those of
 * <code>java.lang.Class</code> and <code>java.lang.ClassLoader</code>, except that they are not sensitive to the
 * presence of a leading forward-slash separator in the resource path, and that they allow the search to be limited
 * to a particular classpath location.</p>
 *
 * <p>But the real fun lies in the <code>getResourceAsFile</code> methods, which allow to manipulate resources as
 * regular files -- again, whether they be in a regular directory or in a JAR file. Likewise,
 * the {@link #getRootPackageAsFile(Class)} allows to dynamically explore and manipulate the resource files contained
 * in a particular classpath's location.</p>
 *
 * @author Maxence Bernard
 */
public class ResourceLoader {
    private static final Logger LOGGER = LoggerFactory.getLogger(ResourceLoader.class);

    /** The default ClassLoader that is used by methods without a ClassLoader argument */
    private static ClassLoader defaultClassLoader = ResourceLoader.class.getClassLoader();

    /**
     * Returns the default <code>ClassLoader</code> that is used by methods without a <code>ClassLoader</code> argument.
     * This default <code>ClassLoader</code> is the one that loaded this class, and <b>not</b> the system <code>ClassLoader</code>. 
     *
     * @return the default <code>ClassLoader</code> that is used by methods without a <code>ClassLoader</code> argument
     */
    public static ClassLoader getDefaultClassLoader() {
        // We do not use the system class loader because it does not work with JNLP/Webstart applications.
        // A quote from a FAQ at java.sun.com:
        //
        // Java Web Start uses a user-level classloader to load all the application resources specified in the JNLP file.
        // This classloader implements the security model and the downloading model defined by the JNLP specification.
        // This is no different than how the AppletViewer or the Java Plug-In works.
        // This has the, unfortunate, side-effect that Class.forName will not find any resources that are defined in the
        // JNLP file. The same is true for looking up resources and classes using the system class loader
        // (ClassLoader.getSystemClassLoader).
        //
        return defaultClassLoader;
    }

    /**
     * Overrides the default <code>ClassLoader</code> used by this class.
     *
     * @param defaultClassLoader the default <code>ClassLoader</code> that is used by methods without a <code>ClassLoader</code> argument
     */
    public static void setDefaultClassLoader(ClassLoader defaultClassLoader) {
        ResourceLoader.defaultClassLoader = defaultClassLoader;
    }

    /**
     * This method is similar to {@link #getResourceAsURL(String)} except that it looks for a resource with a given
     * name in a specific package.
     *
     * @param ppackage package serving as a base folder for the resource to retrieve
     * @param name name of the resource in the package. This is a filename only, not a path.
     * @return a URL pointing to the resource, or <code>null</code> if the resource couldn't be located
     */
    public static URL getPackageResourceAsURL(Package ppackage, String name) {
        return getPackageResourceAsURL(ppackage, name, getDefaultClassLoader(), null);
    }

    /**
     * This method is similar to {@link #getResourceAsURL(String)} except that it looks for a resource with a given
     * name in a specific package.
     *
     * @param ppackage package serving as a base folder for the resource to retrieve
     * @param classLoader the ClassLoader used for locating the resource. May not be <code>null</code>.
     * @param rootPackageFile root package location (JAR file or directory) that limits the scope of the search,
     * <code>null</code> to look for the resource in the whole class path.
     * @param name name of the resource in the package. This is a filename only, not a path.
     * @return a URL pointing to the resource, or <code>null</code> if the resource couldn't be located
     * @see #getRootPackageAsFile(Class)
     */
    public static URL getPackageResourceAsURL(Package ppackage, String name, ClassLoader classLoader, AbstractFile rootPackageFile) {
        return ResourceLoader.getResourceAsURL(getRelativePackagePath(ppackage)+"/"+name, classLoader, rootPackageFile);
    }

    /**
     * Shorthand for {@link #getResourceAsURL(String, ClassLoader, AbstractFile)} called with the
     * {@link #getDefaultClassLoader() default class loader} and a <code>null</code> root package file.
     *
     * @param path forward slash-separated path to the resource to look for, relative to the parent classpath
     * location (directory or JAR file) that contains it.
     * @return a URL pointing to the resource, or <code>null</code> if the resource couldn't be located
     */
    public static URL getResourceAsURL(String path) {
        return getResourceAsURL(path, getDefaultClassLoader(), null);
    }

    /**
     * Finds the resource with the given path and returns a URL pointing to its location, or <code>null</code>
     * if the resource couldn't be located. The given <code>ClassLoader</code> is used for locating the resource.
     *
     * <p>The given resource path must be forward slash (<code>/</code>) separated. It may or may not start with a
     * leading forward slash character, this doesn't affect the way it is interpreted.</p>
     *
     * <p>The <code>rootPackageFile</code> argument can be used to limit the scope of the search to a specific
     * location (JAR file or directory) in the classpath: resources located outside of this location will not be matched.
     * This avoids potential ambiguities that can arise if the specified resource path exists in several locations.
     * If this parameter is <code>null</code>, the resource is looked up in the whole class path. In that case and if
     * several resources with the specified path exist, the choice of the resource to return is arbitrary.</p>
     *
     * @param path forward slash-separated path to the resource to look for, relative to the parent classpath
     * location (directory or JAR file) that contains it.
     * @param classLoader the ClassLoader used for locating the resource. May not be <code>null</code>.
     * @param rootPackageFile root package location (JAR file or directory) that limits the scope of the search,
     * <code>null</code> to look for the resource in the whole class path.
     * @return a URL pointing to the resource, or <code>null</code> if the resource couldn't be located
     */
    public static URL getResourceAsURL(String path, ClassLoader classLoader, AbstractFile rootPackageFile) {
        path = removeLeadingSlash(path);

        if(rootPackageFile==null)
            return classLoader.getResource(path);

        String separator = rootPackageFile.getSeparator();
        String nativePath;
        if(separator.equals("/"))
            nativePath = path;
        else
            nativePath = path.replace("/", separator);

        try {
            // Iterate through all resources that match the given path, and return the one located inside the
            // given root package file.
            Enumeration<URL> resourceEnum = classLoader.getResources(path);
            String rootPackagePath = rootPackageFile.getAbsolutePath();
            String resourcePath = rootPackageFile.getAbsolutePath(true)+nativePath;
            URL resourceURL;
            while(resourceEnum.hasMoreElements()) {
                resourceURL = resourceEnum.nextElement();

                if("jar".equals(resourceURL.getProtocol())) {
                    if(getJarFilePath(resourceURL).equals(rootPackagePath))
                        return resourceURL;
                }
                else {
                    if(normalizeUrlPath(getDecodedURLPath(resourceURL)).equals(resourcePath))
                        return resourceURL;
                }
            }
        }
        catch(IOException e) {
            LOGGER.info("Failed to lookup resource {}", path, e);
            return null;
        }

        return null;
    }

    /**
     * This method is similar to {@link #getResourceAsStream(String)} except that it looks for a resource with a given
     * name in a specific package.
     *
     * @param ppackage package serving as a base folder for the resource to retrieve
     * @param name name of the resource in the package. This is a filename only, not a path.
     * @return an InputStream that allows to read the resource, or <code>null</code> if the resource couldn't be located
     */
    public static InputStream getPackageResourceAsStream(Package ppackage, String name) {
        return getPackageResourceAsStream(ppackage, name, getDefaultClassLoader(), null);
    }

    /**
     * This method is similar to {@link #getResourceAsStream(String, ClassLoader, AbstractFile)} except that it looks
     * for a resource with a given name in a specific package.
     *
     * @param ppackage package serving as a base folder for the resource to retrieve
     * @param name name of the resource in the package. This is a filename only, not a path.
     * @param classLoader the ClassLoader used for locating the resource. May not be <code>null</code>.
     * @param rootPackageFile root package location (JAR file or directory) that limits the scope of the search,
     * <code>null</code> to look for the resource in the whole class path.
     * @return an InputStream that allows to read the resource, or <code>null</code> if the resource couldn't be located
     */
    public static InputStream getPackageResourceAsStream(Package ppackage, String name, ClassLoader classLoader, AbstractFile rootPackageFile) {
        return ResourceLoader.getResourceAsStream(getRelativePackagePath(ppackage)+"/"+name, classLoader, rootPackageFile);
    }

    /**
     * Shorthand for {@link #getResourceAsStream(String, ClassLoader, AbstractFile)} called with the
     * {@link #getDefaultClassLoader() default class loader} and a <code>null</code> root package file.
     *
     * @param path forward slash-separated path to the resource to look for, relative to the parent classpath
     * location (directory or JAR file) that contains it.
     * @return an InputStream that allows to read the resource, or <code>null</code> if the resource couldn't be located
     */
    public static InputStream getResourceAsStream(String path) {
        return getResourceAsStream(path, getDefaultClassLoader(), null);
    }

    /**
     * Finds the resource with the given path and returns an <code>InputStream</code> to read it, or <code>null</code>
     * if the resource couldn't be located. The given <code>ClassLoader</code> is used for locating the resource.
     *
     * <p>The given resource path must be forward slash (<code>/</code>) separated. It may or may not start with a
     * leading forward slash character, this doesn't affect the way it is interpreted.</p>
     *
     * <p>The <code>rootPackageFile</code> argument can be used to limit the scope of the search to a specific
     * location (JAR file or directory) in the classpath: resources located outside of this location will not be matched.
     * This avoids potential ambiguities that can arise if the specified resource path exists in several locations.
     * If this parameter is <code>null</code>, the resource is looked up in the whole class path. In that case and if
     * several resources with the specified path exist, the choice of the resource to return is arbitrary.</p>
     *
     * @param path forward slash-separated path to the resource to look for, relative to the parent classpath
     * location (directory or JAR file) that contains it.
     * @param classLoader the ClassLoader used for locating the resource. May not be <code>null</code>.
     * @param rootPackageFile root package location (JAR file or directory) that limits the scope of the search,
     * <code>null</code> to look for the resource in the whole class path.
     * @return an InputStream that allows to read the resource, or <code>null</code> if the resource couldn't be located
     */
    public static InputStream getResourceAsStream(String path, ClassLoader classLoader, AbstractFile rootPackageFile) {
        try {
            URL resourceURL = getResourceAsURL(path, classLoader, rootPackageFile);
            return resourceURL==null?null:resourceURL.openStream();
        }
        catch(IOException e) {
            return null;
        }
    }

    /**
     * This method is similar to {@link #getResourceAsFile(String)} except that it looks for a resource with a given
     * name in a specific package.
     *
     * @param ppackage package serving as a base folder for the resource to retrieve
     * @param name name of the resource in the package. This is a filename only, not a path.
     * @return an AbstractFile that represents the resource, or <code>null</code> if the resource couldn't be located
     */
    public static AbstractFile getPackageResourceAsFile(Package ppackage, String name) {
        return getPackageResourceAsFile(ppackage, name, getDefaultClassLoader(), null);
    }

    /**
     * This method is similar to {@link #getResourceAsFile(String, ClassLoader, AbstractFile)} except that it looks for
     * a resource with a given name in a specific package.
     *
     * @param ppackage package serving as a base folder for the resource to retrieve
     * @param name name of the resource in the package. This is a filename only, not a path.
     * @param classLoader the ClassLoader used for locating the resource. May not be <code>null</code>.
     * @param rootPackageFile root package location (JAR file or directory) that limits the scope of the search,
     * <code>null</code> to look for the resource in the whole class path.
     * @return an AbstractFile that represents the resource, or <code>null</code> if the resource couldn't be located
     */
    public static AbstractFile getPackageResourceAsFile(Package ppackage, String name, ClassLoader classLoader, AbstractFile rootPackageFile) {
        return ResourceLoader.getResourceAsFile(getRelativePackagePath(ppackage)+"/"+name, classLoader, rootPackageFile);
    }

    /**
     * Shorthand for {@link #getResourceAsFile(String, ClassLoader, AbstractFile)} called with the
     * {@link #getDefaultClassLoader() default class loader} and a <code>null</code> root package file.
     *
     * @param path forward slash-separated path to the resource to look for, relative to the parent classpath
     * location (directory or JAR file) that contains it.
     * @return an AbstractFile that represents the resource, or <code>null</code> if the resource couldn't be located
     */
    public static AbstractFile getResourceAsFile(String path) {
        return getResourceAsFile(removeLeadingSlash(path), getDefaultClassLoader(), null);
    }

    /**
     * Finds the resource with the given path and returns an {@link AbstractFile} that gives full access to it,
     * or <code>null</code> if the resource couldn't be located. The given <code>ClassLoader</code> is used for locating
     * the resource.
     *
     * <p>The given resource path must be forward slash (<code>/</code>) separated. It may or may not start with a 
     * leading forward slash character, this doesn't affect the way it is interpreted.</p>
     *
     * <p>It is worth noting that this method may be slower than {@link #getResourceAsStream(String)} if
     * the resource is located inside a JAR file, because the Zip file headers will have to be parsed the first time
     * the archive is accessed. Therefore, the latter approach should be favored if the file is simply used for
     * reading the resource.</p>
     *
     * <p>The <code>rootPackageFile</code> argument can be used to limit the scope of the search to a specific
     * location (JAR file or directory) in the classpath: resources located outside of this location will not be matched.
     * This avoids potential ambiguities that can arise if the specified resource path exists in several locations.
     * If this parameter is <code>null</code>, the resource is looked up in the whole class path. In that case and if
     * several resources with the specified path exist, the choice of the resource to return is arbitrary.</p>
     *
     * @param path forward slash-separated path to the resource to look for, relative to the parent classpath
     * location (directory or JAR file) that contains it.
     * @param classLoader the ClassLoader is used for locating the resource
     * @param rootPackageFile root package location (JAR file or directory) that limits the scope of the search,
     * <code>null</code> to look for the resource in the whole class path.
     * @return an AbstractFile that represents the resource, or <code>null</code> if the resource couldn't be located
     */
    public static AbstractFile getResourceAsFile(String path, ClassLoader classLoader, AbstractFile rootPackageFile) {
        if(classLoader==null)
            classLoader = getDefaultClassLoader();

        path = removeLeadingSlash(path);

        URL aClassURL = getResourceAsURL(path, classLoader, rootPackageFile);
        if(aClassURL==null)
            return null;        // no resource under that path

        if("jar".equals(aClassURL.getProtocol())) {
            try {
                return ((AbstractArchiveFile)FileFactory.getFile(getJarFilePath(aClassURL))).getArchiveEntryFile(path);
            }
            catch(Exception e) {
                // Shouldn't normally happen, unless the JAR file is corrupt or cannot be parsed by the file API
                return null;
            }
        }

        return FileFactory.getFile(getLocalFilePath(aClassURL));
    }


    /**
     * Returns an {@link AbstractFile} to the root package of the given <code>Class</code>. For example, if the
     * specified <code>Class</code> is <code>java.lang.Object</code>'s, the returned file will be the Java runtime
     * JAR file, which on most platforms is <code>$JAVA_HOME/lib/jre/rt.jar</code>.<br>
     * The returned file can be used to list or manipulate all resource files contained in a particular classpath's
     * location, including the .class files.
     *
     * @param aClass the class for which to locate the root package.
     * @return an AbstractFile to the root package of the given <code>Class</code>
     */
    public static AbstractFile getRootPackageAsFile(Class<?> aClass) {
        ClassLoader classLoader = aClass.getClassLoader();
        if(classLoader==null)
            classLoader = getDefaultClassLoader();

        String aClassRelPath = getRelativeClassPath(aClass);
        URL aClassURL = getResourceAsURL(aClassRelPath, classLoader, null);

        if(aClassURL==null)
            return null;    // no resource under that path

        if("jar".equals(aClassURL.getProtocol()))
            return FileFactory.getFile(getJarFilePath(aClassURL));

        String aClassPath = getLocalFilePath(aClassURL);
        return FileFactory.getFile(aClassPath.substring(0, aClassPath.length()-aClassRelPath.length()));
    }

    /**
     * Returns a path to the given package. The returned path is relative, forward slash-separated and does not end
     * with a trailing separator. For example, if the package <code>com.mucommander.commons.file</code> is passed, the returned
     * path will be <code>com/mucommander/commons/file</code>.
     *
     * @param ppackage the package for which to return a path
     * @return a path to the given package
     */
    public static String getRelativePackagePath(Package ppackage) {
        return ppackage.getName().replace('.', '/');
    }

    /**
     * Returns a path to the given class. The returned path is relative, forward slash-separated. For example, if the
     * class <code>com.mucommander.commons.file.AbstractFile</code> is passed,  the returned path will be
     * <code>com/mucommander/commons/file/AbstractFile.class</code>.
     *
     * @param cclass the class for which to return a path
     * @return a path to the given package
     */
    public static String getRelativeClassPath(Class<?> cclass) {
        return cclass.getName().replace('.', '/')+".class";
    }


    /////////////////////
    // Private methods //
    /////////////////////

    /**
     * Extracts and returns the path to the JAR file from a URL that points to a resource inside a JAR file.
     * The returned path is in a format that {@link FileFactory} can turn into an {@link AbstractFile}.
     *
     * @param url a URL that points to a resource inside a JAR file
     * @return returns the path to the JAR file
     */
    private static String getJarFilePath(URL url) {
        // URL-decode the path
        String path = getDecodedURLPath(url);

        // Here are a couple examples of such paths:
        // file:/System/Library/Frameworks/JavaVM.framework/Versions/1.6.0/Classes/classes.jar!/java/lang/Object.class
        // http://www.mucommander.com/webstart/nightly/mucommander.jar!/com/mucommander/RuntimeConstants.class

        int pos = path.indexOf(".jar!");
        if(pos==-1)
            return path;

        // Strip out the part after ".jar" and normalize the path
        return normalizeUrlPath(path.substring(0, pos+4));
    }

    /**
     * Extracts and returns the path to a local file represented by the given URL.
     * The returned path is in a format that {@link FileFactory} can turn into an {@link AbstractFile}.
     *
     * @param url a URL that points to a resource inside a JAR file
     * @return returns the path to the JAR file
     */
    private static String getLocalFilePath(URL url) {
        // Here's an example of such a path under Windows:
        // /C:/cygwin/home/Administrator/mucommander/tmp/compile/classes/

        // URL-decode the path and normalize it
        return normalizeUrlPath(getDecodedURLPath(url));
    }

    /**
     * Removes any leading slash from the given path and returns it. Does nothing if the path does not have a
     * leading path.
     *
     * @param path the path to normalize
     * @return the path without a leading slash
     */
    private static String removeLeadingSlash(String path) {
        return PathUtils.removeLeadingSeparator(path, "/");
    }

    /**
     * Normalizes the specified path issued from a <code>java.net.URL</code> and returns it.
     * The returned path is in a format that {@link FileFactory} can turn into an {@link AbstractFile}.
     *
     * @param path the URL path to normalize
     * @return the normalized path
     */
    private static String normalizeUrlPath(String path) {
        // Don't touch http/https URLs
        if(path.startsWith("http:") || path.startsWith("https:"))
            return path;

        // Remove the leading "file:" (if any)
        if(path.startsWith("file:"))
            path = path.substring(5, path.length());

        // Under platforms that use root drives (Windows and OS/2), strip out the leading '/'
        if(LocalFile.hasRootDrives() && path.startsWith("/"))
            path = removeLeadingSlash(path);

        // Use the local file separator
        String separator = LocalFile.SEPARATOR;
        if(!"/".equals(separator))
            path = path.replace("/", separator);

        return path;
    }

    /**
     * Returns the URL-decoded path of the given <code>java.net.URL</code>. The encoding used for URL-decoding is
     * <code>UTF-8</code>.
     *
     * @param url the URL for which to decode the path
     * @return the URL-decoded path of the given URL
     */
    private static String getDecodedURLPath(URL url) {
        try {
            // Decode the URL's path which may contain URL-encoded characters such as %20 for spaces, or non-ASCII
            // characters.
            // Note: the Java API's javadoc doesn't specify which encoding has been used to encoded URL paths.
            // The only indication is in URLDecoder#decode(String, String) javadoc which says:
            // "The World Wide Web Consortium Recommendation states that UTF-8 should be used. Not doing so may
            // introduce incompatibilites."
            // Also Note that URLDecoder#decode(String) uses System.getProperty("file.encoding") as the default encoding,
            // using this value has been tested without luck under Mac OS X where the value equals "MacRoman" but
            // URL are actually encoded in UTF-8. The bottom line is that we blindly use UTF-8 to decode resource URLs.
            return URLDecoder.decode(url.getPath(), "UTF-8");
        }
        catch(UnsupportedEncodingException e) {
            // This should never happen, UTF-8 is necessarily supported by the Java runtime
            return null;
        }
    }
}
