/*
 * 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.network.NetworkUtil;
import icy.network.URLUtil;
import icy.plugin.classloader.exception.JclException;
import icy.system.IcyExceptionHandler;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.JarURLConnection;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream;

/**
 * JarResources reads jar files and loads the class content/bytes in a HashMap
 * 
 * @author Kamran Zafar
 * @author Stephane Dallongeville
 */
public class JarResources
{
    // <resourceName, content> map
    protected Map<String, byte[]> entryContents;
    // <resourceName, fileName> map
    protected Map<String, URL> entryUrls;

    protected boolean collisionAllowed;
    // keep trace of loaded resource size
    protected int loadedSize;

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

    /**
     * Default constructor
     */
    public JarResources()
    {
        entryContents = new HashMap<String, byte[]>();
        entryUrls = new HashMap<String, URL>();
        collisionAllowed = Configuration.suppressCollisionException();
        loadedSize = 0;
    }

    public URL getResource(String name)
    {
        return entryUrls.get(name);
    }

    public byte[] getResourceContent(String name) throws IOException
    {
        byte content[] = entryContents.get(name);

        // we load the content
        if (content == null)
        {
            final URL url = entryUrls.get(name);

            if (url != null)
            {
                // load content and return it
                loadContent(name, url);
                content = entryContents.get(name);

                // try
                // {
                // // load content and return it
                // loadContent(name, url);
                // content = entryContents.get(name);
                // }
                // catch (IOException e)
                // {
                // // content cannot be loaded, remove the URL entry
                // // better to not remove it after all, so we know why it failed next time
                // // entryUrls.remove(name);
                //
                // // and throw exception
                // throw e;
                // }
            }
        }

        return content;
    }

    protected void loadContent(String name, URL url) throws IOException
    {
        // only support JAR resource here
        final byte[] content = loadJarContent(url);
        setResourceContent(name, content);
    }

    /**
     * Returns an immutable Set of all resources names
     */
    public Set<String> getResourcesName()
    {
        return Collections.unmodifiableSet(entryUrls.keySet());
    }

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

    /**
     * Returns an immutable Map of all loaded jar resources
     */
    public Map<String, byte[]> getLoadedResources()
    {
        return Collections.unmodifiableMap(entryContents);
    }

    /**
     * Loads the jar file from a specified File.<br>
     * This method actually stores all contained URL in the specified JAR file.
     * 
     * @throws IOException
     */
    public void loadJar(File file) throws IOException
    {
        final String filePath = file.getAbsolutePath();
        final String urlPrefix = "jar:" + file.toURI().toString() + "!/";

        if (logger.isLoggable(Level.FINEST))
            logger.finest("Loading jar: " + filePath);

        // we don't care about JAR specific information so just use ZipFile here
        final ZipFile zipFile = new ZipFile(file);

        try
        {
            final Enumeration<? extends ZipEntry> entries = zipFile.entries();

            while (entries.hasMoreElements())
            {
                final ZipEntry entry = entries.nextElement();

                // ignore folder entries
                if (entry.isDirectory())
                    continue;

                final String name = entry.getName();

                // ignore manifest file
                if (name.equals("META-INF/MANIFEST.MF"))
                    continue;

                if (entryUrls.containsKey(name))
                {
                    if (!collisionAllowed)
                        throw new JclException("Class/Resource " + name + " already loaded");

                    if (logger.isLoggable(Level.FINEST))
                        logger.finest("Class/Resource " + name + " already loaded; ignoring entry...");
                    continue;
                }

                // add to internal resource HashMap
                entryUrls.put(name, new URL(urlPrefix + name));
            }
        }
        finally
        {
            try
            {
                zipFile.close();
            }
            catch (IOException e)
            {
                // not important
                System.err.println("JarResources.loadJar(" + filePath + ") error:");
                IcyExceptionHandler.showErrorMessage(e, false, true);
            }
        }

    }

    /**
     * Loads the jar file from a specified URL.<br>
     * This method actually stores all contained URL in the specified JAR archive.
     * 
     * @throws IOException
     */
    public void loadJar(URL url) throws IOException
    {
        // use file loading if possible
        if (URLUtil.isFileURL(url))
        {
            File f;

            try
            {
                f = new File(url.toURI());
            }
            catch (URISyntaxException e)
            {
                f = new File(url.getPath());
            }

            loadJar(f);
            return;
        }

        if (logger.isLoggable(Level.FINEST))
            logger.finest("Loading jar: " + url.toString());

        final String urlPrefix = "jar:" + url.toString() + "!/";
        BufferedInputStream bis = null;
        // we don't care about JAR specific information so just use ZipInputStream here
        ZipInputStream zis = null;

        try
        {
            final InputStream in = url.openStream();

            if (in instanceof BufferedInputStream)
                bis = (BufferedInputStream) in;
            else
                bis = new BufferedInputStream(in);

            zis = new ZipInputStream(bis);

            ZipEntry entry = null;
            while ((entry = zis.getNextEntry()) != null)
            {
                if (logger.isLoggable(Level.FINEST))
                    logger.finest(dump(entry));

                if (entry.isDirectory())
                    continue;

                final String name = entry.getName();

                // ignore manifest file
                if (name.equals("META-INF/MANIFEST.MF"))
                    continue;

                if (entryUrls.containsKey(name))
                {
                    if (!collisionAllowed)
                        throw new JclException("Class/Resource " + name + " already loaded");

                    if (logger.isLoggable(Level.FINEST))
                        logger.finest("Class/Resource " + name + " already loaded; ignoring entry...");
                    continue;
                }

                // add to internal resource HashMap
                entryUrls.put(name, new URL(urlPrefix + name));
            }
        }
        // catch (NullPointerException e)
        // {
        // if (logger.isLoggable(Level.FINEST))
        // logger.finest("Done loading.");
        // }
        finally
        {
            if (zis != null)
            {
                try
                {
                    zis.close();
                }
                catch (IOException e)
                {
                    // not important
                    System.err.println("JarResources.loadJar(" + url + ") error:");
                    IcyExceptionHandler.showErrorMessage(e, false, true);
                }
            }

            if (bis != null)
            {
                try
                {
                    bis.close();
                }
                catch (IOException e)
                {
                    // not important
                    System.err.println("JarResources.loadJar(" + url + ") error:");
                    IcyExceptionHandler.showErrorMessage(e, false, true);
                }
            }
        }
    }

    /**
     * Load the jar contents from InputStream
     * 
     * @throws IOException
     */
    protected byte[] loadJarContent(URL url) throws IOException
    {
        final JarURLConnection uc = (JarURLConnection) url.openConnection();
        final JarEntry jarEntry = uc.getJarEntry();

        if (jarEntry != null)
        {
            if (logger.isLoggable(Level.FINEST))
                logger.finest(dump(jarEntry));

            return NetworkUtil.download(uc.getInputStream(), jarEntry.getSize(), null);
        }

        throw new IOException("JarResources.loadJarContent(" + url.toString() + ") error:\nEntry not found !");
    }

    protected void setResourceContent(String name, byte content[])
    {
        if (entryContents.containsKey(name))
        {
            if (!collisionAllowed)
                throw new JclException("Class/Resource " + name + " already loaded");

            if (logger.isLoggable(Level.FINEST))
                logger.finest("Class/Resource " + name + " already loaded; ignoring entry...");
            return;
        }

        if (logger.isLoggable(Level.FINEST))
            logger.finest("Entry Name: " + name + ", " + "Entry Size: " + content.length);

        // add to internal resource HashMap
        entryContents.put(name, content);
    }

    /**
     * For debugging
     * 
     * @param je
     * @return String
     */
    private String dump(ZipEntry ze)
    {
        StringBuffer sb = new StringBuffer();
        if (ze.isDirectory())
            sb.append("d ");
        else
            sb.append("f ");

        if (ze.getMethod() == ZipEntry.STORED)
            sb.append("stored   ");
        else
            sb.append("deflated ");

        sb.append(ze.getName());
        sb.append("\t");
        sb.append("" + ze.getSize());
        if (ze.getMethod() == ZipEntry.DEFLATED)
            sb.append("/" + ze.getCompressedSize());

        return (sb.toString());
    }
}