/*
 * 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 java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Abstract class loader that can load classes from different resources
 * 
 * @author Kamran Zafar
 * @author Stephane Dallongeville
 */
@SuppressWarnings("unchecked")
public abstract class AbstractClassLoader extends ClassLoader
{
    protected final List<ProxyClassLoader> loaders = new ArrayList<ProxyClassLoader>();

    private final ProxyClassLoader systemLoader = new SystemLoader();
    private final ProxyClassLoader parentLoader = new ParentLoader();
    private final ProxyClassLoader currentLoader = new CurrentLoader();
    private final ProxyClassLoader threadLoader = new ThreadContextLoader();

    /**
     * Build a new instance of AbstractClassLoader.java.
     * 
     * @param parent
     *        parent class loader
     */
    public AbstractClassLoader(ClassLoader parent)
    {
        super(parent);

        addDefaultLoaders();
    }

    public void addLoader(ProxyClassLoader loader)
    {
        loaders.add(loader);

        Collections.sort(loaders);
    }

    protected void addDefaultLoaders()
    {
        // always add this one
        loaders.add(systemLoader);
        loaders.add(parentLoader);
        loaders.add(currentLoader);
        loaders.add(threadLoader);

        Collections.sort(loaders);
    }

    @Override
    public Class loadClass(String className) throws ClassNotFoundException
    {
        return loadClass(className, true);
    }

    /**
     * Overrides the loadClass method to load classes from other resources,
     * JarClassLoader is the only subclass in this project that loads classes
     * from jar files
     * 
     * @see java.lang.ClassLoader#loadClass(String, boolean)
     */
    @Override
    public Class loadClass(String className, boolean resolveIt) throws ClassNotFoundException
    {
        if (className == null || className.trim().equals(""))
            return null;

        final List<ProxyClassLoader> loadersDone = new ArrayList<ProxyClassLoader>();

        for (ProxyClassLoader l : loaders)
        {
            // don't search in same loader
            if (l.isEnabled() && !loadersDone.contains(l))
            {
                final Class clazz = l.loadClass(className, resolveIt);
                if (clazz != null)
                    return clazz;

                // loader done
                loadersDone.add(l);
            }
        }

        throw new ClassNotFoundException(className);
    }

    /**
     * Overrides the getResourceAsStream method to load non-class resources from
     * other sources, JarClassLoader is the only subclass in this project that
     * loads non-class resources from jar files
     * 
     * @see java.lang.ClassLoader#getResourceAsStream(java.lang.String)
     */
    @Override
    public InputStream getResourceAsStream(String name)
    {
        if (name == null || name.trim().equals(""))
            return null;

        final List<ProxyClassLoader> loadersDone = new ArrayList<ProxyClassLoader>();

        for (ProxyClassLoader l : loaders)
        {
            // don't search in same loader
            if (l.isEnabled() && !loadersDone.contains(l))
            {
                final InputStream is = l.getResourceAsStream(name);
                if (is != null)
                    return is;

                // loader done
                loadersDone.add(l);
            }
        }

        return null;
    }

    @Override
    public URL getResource(String name)
    {
        if (name == null || name.trim().equals(""))
            return null;

        final List<ProxyClassLoader> loadersDone = new ArrayList<ProxyClassLoader>();

        for (ProxyClassLoader l : loaders)
        {
            // don't search in same loader
            if (l.isEnabled() && !loadersDone.contains(l))
            {
                final URL url = l.getResource(name);
                if (url != null)
                    return url;

                // loader done
                loadersDone.add(l);
            }
        }

        return null;
    }

    @Override
    public Enumeration<URL> getResources(String name) throws IOException
    {
        if (name == null || name.trim().equals(""))
            return null;

        final Set<URL> result = new HashSet<URL>();
        final List<ProxyClassLoader> loadersDone = new ArrayList<ProxyClassLoader>();

        for (ProxyClassLoader l : loaders)
        {
            // don't search in same loader
            if (l.isEnabled() && !loadersDone.contains(l))
            {
                final Enumeration<URL> urls = l.getResources(name);

                if (urls != null)
                {
                    // avoid duplicate using Set here
                    while (urls.hasMoreElements())
                        result.add(urls.nextElement());
                }

                // loader done
                loadersDone.add(l);
            }
        }

        return Collections.enumeration(result);
    }

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

        public SystemLoader()
        {
            super(10);

            enabled = Configuration.isSystemLoaderEnabled();
        }

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

        @Override
        public Class loadClass(String className, boolean resolveIt)
        {
            Class result;

            try
            {
                result = findSystemClass(className);
            }
            catch (ClassNotFoundException e)
            {
                return null;
            }

            if (logger.isLoggable(Level.FINEST))
                logger.finest("Returning system class " + className);

            return result;
        }

        @Override
        public InputStream getResourceAsStream(String name)
        {
            InputStream is = getSystemResourceAsStream(name);

            if (is != null)
            {
                if (logger.isLoggable(Level.FINEST))
                    logger.finest("Returning system resource " + name);

                return is;
            }

            return null;
        }

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

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

                return url;
            }

            return null;
        }

        @Override
        public Enumeration<URL> getResources(String name) throws IOException
        {
            Enumeration<URL> urls = getSystemResources(name);

            if ((urls != null) && urls.hasMoreElements())
            {
                if (logger.isLoggable(Level.FINEST))
                    logger.finest("Returning system resources " + name);

                return urls;
            }

            return null;
        }
    }

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

        public ParentLoader()
        {
            super(30);

            enabled = Configuration.isParentLoaderEnabled();
        }

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

        @Override
        public Class loadClass(String className, boolean resolveIt)
        {
            Class result;

            try
            {
                result = getParent().loadClass(className);
            }
            catch (ClassNotFoundException e)
            {
                return null;
            }

            if (logger.isLoggable(Level.FINEST))
                logger.finest("Returning class " + className + " loaded with parent classloader");

            return result;
        }

        @Override
        public InputStream getResourceAsStream(String name)
        {
            InputStream is = getParent().getResourceAsStream(name);

            if (is != null)
            {
                if (logger.isLoggable(Level.FINEST))
                    logger.finest("Returning resource " + name + " loaded with parent classloader");

                return is;
            }
            return null;
        }

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

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

                return url;
            }

            return null;
        }

        @Override
        public Enumeration<URL> getResources(String name) throws IOException
        {
            Enumeration<URL> urls = getParent().getResources(name);

            if ((urls != null) && urls.hasMoreElements())
            {
                if (logger.isLoggable(Level.FINEST))
                    logger.finest("Returning resource " + name + " loaded with parent classloader");

                return urls;
            }

            return null;
        }
    }

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

        public CurrentLoader()
        {
            super(20);

            enabled = Configuration.isCurrentLoaderEnabled();
        }

        @Override
        public Object getLoader()
        {
            return getClass().getClassLoader();
        }

        @Override
        public Class loadClass(String className, boolean resolveIt)
        {
            Class result;

            try
            {
                result = getClass().getClassLoader().loadClass(className);
            }
            catch (ClassNotFoundException e)
            {
                return null;
            }

            if (logger.isLoggable(Level.FINEST))
                logger.finest("Returning class " + className + " loaded with current classloader");

            return result;
        }

        @Override
        public InputStream getResourceAsStream(String name)
        {
            InputStream is = getClass().getClassLoader().getResourceAsStream(name);

            if (is != null)
            {
                if (logger.isLoggable(Level.FINEST))
                    logger.finest("Returning resource " + name + " loaded with current classloader");

                return is;
            }

            return null;
        }

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

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

                return url;
            }

            return null;
        }

        @Override
        public Enumeration<URL> getResources(String name) throws IOException
        {
            Enumeration<URL> urls = getClass().getClassLoader().getResources(name);

            if ((urls != null) && (urls.hasMoreElements()))
            {
                if (logger.isLoggable(Level.FINEST))
                    logger.finest("Returning resources " + name + " loaded with current classloader");

                return urls;
            }

            return null;
        }

    }

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

        public ThreadContextLoader()
        {
            super(40);

            enabled = Configuration.isThreadContextLoaderEnabled();
        }

        @Override
        public Object getLoader()
        {
            return Thread.currentThread().getContextClassLoader();
        }

        @Override
        public Class loadClass(String className, boolean resolveIt)
        {
            Class result;
            try
            {
                result = Thread.currentThread().getContextClassLoader().loadClass(className);
            }
            catch (ClassNotFoundException e)
            {
                return null;
            }

            if (logger.isLoggable(Level.FINEST))
                logger.finest("Returning class " + className + " loaded with thread context classloader");

            return result;
        }

        @Override
        public InputStream getResourceAsStream(String name)
        {
            InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream(name);

            if (is != null)
            {
                if (logger.isLoggable(Level.FINEST))
                    logger.finest("Returning resource " + name + " loaded with thread context classloader");

                return is;
            }

            return null;
        }

        @Override
        public URL getResource(String name)
        {
            URL url = Thread.currentThread().getContextClassLoader().getResource(name);

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

                return url;
            }

            return null;
        }

        @Override
        public Enumeration<URL> getResources(String name) throws IOException
        {
            Enumeration<URL> urls = Thread.currentThread().getContextClassLoader().getResources(name);

            if ((urls != null) && (urls.hasMoreElements()))
            {
                if (logger.isLoggable(Level.FINEST))
                    logger.finest("Returning resources " + name + " loaded with thread context classloader");

                return urls;
            }

            return null;
        }
    }

    public ProxyClassLoader getSystemLoader()
    {
        return systemLoader;
    }

    public ProxyClassLoader getParentLoader()
    {
        return parentLoader;
    }

    public ProxyClassLoader getCurrentLoader()
    {
        return currentLoader;
    }

    public ProxyClassLoader getThreadLoader()
    {
        return threadLoader;
    }
}