/*
 * Copyright 2010-2015 Institut Pasteur.
 * 
 * This file is part of Icy.
 * 
 * Icy is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Icy 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 General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with Icy. If not, see <http://www.gnu.org/licenses/>.
 */

package icy.plugin.classloader;

import icy.plugin.classloader.exception.JclException;
import icy.plugin.classloader.exception.ResourceNotFoundException;
import icy.system.IcyExceptionHandler;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Reads the class bytes from jar files and other resources using
 * ClasspathResources
 * 
 * @author Kamran Zafar
 * @author Stephane Dallongeville
 */
@SuppressWarnings("rawtypes")
public class JarClassLoader extends AbstractClassLoader
{
    /**
     * Class cache
     */
    protected final Map<String, Class> loadedClasses;

    protected final ClasspathResources classpathResources;
    private char classNameReplacementChar;
    private final ProxyClassLoader localLoader = new LocalLoader();

    private static Logger logger = Logger.getLogger(JarClassLoader.class.getName());

    public JarClassLoader(ClassLoader parent)
    {
        super(parent);

        classpathResources = new ClasspathResources();
        loadedClasses = Collections.synchronizedMap(new HashMap<String, Class>());

        addLoader(localLoader);
    }

    public JarClassLoader()
    {
        this(getSystemClassLoader());
    }

    /**
     * Loads classes from different sources
     * 
     * @param sources
     */
    public JarClassLoader(Object[] sources)
    {
        this();

        addAll(sources);
    }

    /**
     * Loads classes from different sources
     * 
     * @param sources
     */
    public JarClassLoader(List sources)
    {
        this();

        addAll(sources);
    }

    /**
     * Add all jar/class sources
     * 
     * @param sources
     */
    public void addAll(Object[] sources)
    {
        for (Object source : sources)
            add(source);
    }

    /**
     * Add all jar/class sources
     * 
     * @param sources
     */
    public void addAll(List sources)
    {
        for (Object source : sources)
            add(source);
    }

    /**
     * Loads local/remote source
     * 
     * @param source
     */
    public void add(Object source)
    {
        if (source instanceof InputStream)
            throw new JclException("Unsupported resource type");
        else if (source instanceof URL)
            add((URL) source);
        else if (source instanceof String)
            add((String) source);
        else
            throw new JclException("Unknown Resource type");

    }

    /**
     * Loads local/remote resource
     * 
     * @param resourceName
     */
    public void add(String resourceName)
    {
        classpathResources.loadResource(resourceName);
    }

    /**
     * Loads classes from InputStream.
     * 
     * @deprecated Not anymore supported (we need URL for getResource(..) method)
     */
    @Deprecated
    public void add(InputStream jarStream)
    {
        // classpathResources.loadJar(jarStream);
    }

    /**
     * Loads local/remote resource
     * 
     * @param url
     */
    public void add(URL url)
    {
        classpathResources.loadResource(url);
    }

    /**
     * Release all loaded resources and classes.
     * The ClassLoader cannot be used anymore to load any new resource.
     */
    public void unloadAll()
    {
        // unload resources
        classpathResources.entryContents.clear();
        // unload classes
        loadedClasses.clear();
    }

    /**
     * Reads the class bytes from different local and remote resources using
     * ClasspathResources
     * 
     * @param className
     * @return byte[]
     * @throws IOException
     */
    protected byte[] getClassBytes(String className) throws IOException
    {
        return classpathResources.getResourceContent(formatClassName(className));
    }

    /**
     * Attempts to unload class, it only unloads the locally loaded classes by
     * JCL
     * 
     * @param className
     */
    public void unloadClass(String className)
    {
        if (logger.isLoggable(Level.FINEST))
            logger.finest("Unloading class " + className);

        if (loadedClasses.containsKey(className))
        {
            if (logger.isLoggable(Level.FINEST))
                logger.finest("Removing loaded class " + className);
            loadedClasses.remove(className);
            try
            {
                classpathResources.unload(formatClassName(className));
            }
            catch (ResourceNotFoundException e)
            {
                throw new JclException("Something is very wrong!!!"
                        + "The locally loaded classes must be in synch with ClasspathResources", e);
            }
        }
        else
        {
            try
            {
                classpathResources.unload(formatClassName(className));
            }
            catch (ResourceNotFoundException e)
            {
                throw new JclException("Class could not be unloaded "
                        + "[Possible reason: Class belongs to the system]", e);
            }
        }
    }

    /**
     * @param className
     * @return String
     */
    protected String formatClassName(String className)
    {
        String cname = className.replace('/', '~');

        if (classNameReplacementChar == '\u0000')
            // '/' is used to map the package to the path
            cname = cname.replace('.', '/') + ".class";
        else
            // Replace '.' with custom char, such as '_'
            cname = cname.replace('.', classNameReplacementChar) + ".class";

        return cname.replace('~', '/');
    }

    /**
     * Local class loader
     */
    class LocalLoader extends ProxyClassLoader
    {
        private final Logger logger = Logger.getLogger(LocalLoader.class.getName());

        public LocalLoader()
        {
            super(50);

            enabled = Configuration.isLocalLoaderEnabled();
        }

        @Override
        public Object getLoader()
        {
            return this;
        }

        @Override
        public Class loadClass(String className, boolean resolveIt) throws ClassNotFoundException
        {
            Class result = null;
            byte[] classBytes;

            result = loadedClasses.get(className);
            if (result != null)
            {
                if (logger.isLoggable(Level.FINEST))
                    logger.finest("Returning local loaded class [" + className + "] from cache");
                return result;
            }

            // try to find from already loaded class (by other method)
            result = findLoadedClass(className);
            // not loaded ?
            if (result == null)
            {
                try
                {
                    classBytes = getClassBytes(className);
                }
                catch (IOException e)
                {
                    // we got a severe error here --> throw an exception
                    throw new ClassNotFoundException(className, e);
                }

                if (classBytes == null)
                    return null;

                result = defineClass(className, classBytes, 0, classBytes.length);

                if (result == null)
                    return null;
            }

            /*
             * Preserve package name.
             */
            if (result.getPackage() == null)
            {
                int lastDotIndex = className.lastIndexOf('.');
                String packageName = (lastDotIndex >= 0) ? className.substring(0, lastDotIndex) : "";
                definePackage(packageName, null, null, null, null, null, null, null);
            }

            if (resolveIt)
                resolveClass(result);

            loadedClasses.put(className, result);
            if (logger.isLoggable(Level.FINEST))
                logger.finest("Return new local loaded class " + className);

            return result;
        }

        @Override
        public InputStream getResourceAsStream(String name)
        {
            try
            {
                byte[] arr = classpathResources.getResourceContent(name);

                if (arr != null)
                {
                    if (logger.isLoggable(Level.FINEST))
                        logger.finest("Returning newly loaded resource " + name);

                    return new ByteArrayInputStream(arr);
                }
            }
            catch (IOException e)
            {
                IcyExceptionHandler.showErrorMessage(e, false, true);
            }

            return null;
        }

        @Override
        public URL getResource(String name)
        {
            URL url = classpathResources.getResource(name);

            if (url != null)
            {
                if (logger.isLoggable(Level.FINEST))
                    logger.finest("Returning newly loaded resource " + name);

                return url;
            }

            return null;
        }

        @Override
        public Enumeration<URL> getResources(String name) throws IOException
        {
            final URL url = getResource(name);

            return new Enumeration<URL>()
            {
                boolean hasMore = (url != null);

                @Override
                public boolean hasMoreElements()
                {
                    return hasMore;
                }

                @Override
                public URL nextElement()
                {
                    if (hasMore)
                    {
                        hasMore = false;
                        return url;
                    }

                    return null;
                }
            };
        }
    }

    public char getClassNameReplacementChar()
    {
        return classNameReplacementChar;
    }

    public void setClassNameReplacementChar(char classNameReplacementChar)
    {
        this.classNameReplacementChar = classNameReplacementChar;
    }

    /**
     * Returns an immutable Set of all resources name
     */
    public Set<String> getResourcesName()
    {
        return classpathResources.getResourcesName();
    }

    /**
     * Returns an immutable Map of all resources
     */
    public Map<String, URL> getResources()
    {
        return classpathResources.getResources();
    }

    /**
     * Returns all currently loaded classes and resources.
     */
    public Map<String, byte[]> getLoadedResources()
    {
        return classpathResources.getLoadedResources();
    }

    /**
     * @return Local JCL ProxyClassLoader
     */
    public ProxyClassLoader getLocalLoader()
    {
        return localLoader;
    }

    /**
     * Returns all JCL-loaded classes as an immutable Map
     * 
     * @return Map
     */
    public Map<String, Class> getLoadedClasses()
    {
        return Collections.unmodifiableMap(loadedClasses);
    }
}