/*
 * Copyright 2007 The Codehaus Foundation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.codehaus.plexus.archiver.zip;

import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.charset.Charset;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.jar.JarFile;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipFile;
import org.codehaus.plexus.components.io.functions.SymlinkDestinationSupplier;
import org.codehaus.plexus.components.io.resources.AbstractPlexusIoArchiveResourceCollection;
import org.codehaus.plexus.components.io.resources.EncodingSupported;
import org.codehaus.plexus.components.io.resources.PlexusIoResource;
import org.codehaus.plexus.components.io.resources.PlexusIoURLResource;

public class PlexusIoZipFileResourceCollection
    extends AbstractPlexusIoArchiveResourceCollection
    implements EncodingSupported
{

    /**
     * The zip file resource collections role hint.
     */
    public static final String ROLE_HINT = "zipFile";

    /**
     * The zip file resource collections role hint for jar files.
     */
    public static final String JAR_ROLE_HINT = "jarFile";

    private Charset charset = Charset.forName( "UTF-8" );

    public PlexusIoZipFileResourceCollection()
    {

    }

    @Override
    public boolean isConcurrentAccessSupported()
    {
        // Maybe we could support concurrent some time in the future
        return false;
    }

    @Override
    protected Iterator<PlexusIoResource> getEntries()
        throws IOException
    {
        final File f = getFile();
        if ( f == null )
        {
            throw new IOException( "The zip file has not been set." );
        }
        final URLClassLoader urlClassLoader = new URLClassLoader( new URL[]
        {
            f.toURI().toURL()
        }, null )
        {

            @Override
            public URL getResource( String name )
            {
                return findResource( name );
            }

        };

        final URL url = new URL( "jar:" + f.toURI().toURL() + "!/" );
        final JarFile jarFile = new JarFile( f );
        final ZipFile zipFile = new ZipFile( f, charset != null ? charset.name() : "UTF8" );
        final Enumeration<ZipArchiveEntry> en = zipFile.getEntriesInPhysicalOrder();
        return new ZipFileResourceIterator( en, url, jarFile, zipFile, urlClassLoader );
    }

    private static class ZipFileResourceIterator
        implements Iterator<PlexusIoResource>, Closeable
    {

        private class ZipFileResource
            extends PlexusIoURLResource
        {
            private final JarFile jarFile;

            private ZipFileResource( JarFile jarFile, ZipArchiveEntry entry )
            {
                super( entry.getName(),
                       entry.getTime() == -1 ? PlexusIoResource.UNKNOWN_MODIFICATION_DATE : entry.getTime(),
                       entry.isDirectory() ? PlexusIoResource.UNKNOWN_RESOURCE_SIZE : entry.getSize(),
                       !entry.isDirectory(), entry.isDirectory(), true );

                this.jarFile = jarFile;
            }

            @Override
            public InputStream getContents() throws IOException {
                // Uses the JarFile to get the input stream for the entry.
                // The super method will do the same, why overriding it?
                // But it will create new JarFile for every entry
                // and that could be very expensive in some cases (when the JAR is signed).
                // Enabling the URLConnection cache would solve the problem
                // but the cache is global and shared during the build so
                // if the AJR file changed during the causes another problem
                // (see plexus-io#2).
                // Using local JarFile instance solves the two issues -
                // JarFile is initialized once so there is no performance penalty
                // and it is local so if the file changed during the build
                // would not be a problem.
                // And we know the URL returned by getURL is part of the JAR
                // because that is how we constructed it.
                return jarFile.getInputStream( jarFile.getEntry( getName() ) );
            }

            @Override
            public URL getURL()
                throws IOException
            {
                String spec = getName();
                if ( spec.startsWith( "/" ) )
                {
                    // Code path for PLXCOMP-170. Note that urlClassloader does not seem to produce correct
                    // urls for this. Which again means files loaded via this path cannot have file names
                    // requiring url encoding
                    spec = "./" + spec;
                    return new URL( url, spec );
                }
                return urlClassLoader.getResource( spec );
            }

        }

        private class ZipFileSymlinkResource
            extends ZipFileResource
            implements SymlinkDestinationSupplier
        {

            private final ZipArchiveEntry entry;

            private ZipFileSymlinkResource( JarFile jarFile, ZipArchiveEntry entry )
            {
                super( jarFile, entry );

                this.entry = entry;
            }

            @Override
            public String getSymlinkDestination()
                throws IOException
            {
                return zipFile.getUnixSymlink( entry );
            }

            @Override
            public boolean isSymbolicLink()
            {
                return true;
            }

        }

        private final Enumeration<ZipArchiveEntry> en;

        private final URL url;

        private final JarFile jarFile;

        private final ZipFile zipFile;

        private final URLClassLoader urlClassLoader;

        public ZipFileResourceIterator( Enumeration<ZipArchiveEntry> en, URL url, JarFile jarFile, ZipFile zipFile,
                                        URLClassLoader urlClassLoader )
        {
            this.en = en;
            this.url = url;
            this.jarFile = jarFile;
            this.zipFile = zipFile;
            this.urlClassLoader = urlClassLoader;
        }

        @Override
        public boolean hasNext()
        {
            return en.hasMoreElements();
        }

        @Override
        public PlexusIoResource next()
        {
            final ZipArchiveEntry entry = en.nextElement();
            return entry.isUnixSymlink()
                       ? new ZipFileSymlinkResource( jarFile, entry )
                       : new ZipFileResource( jarFile, entry );

        }

        @Override
        public void remove()
        {
            throw new UnsupportedOperationException( "Removing isn't implemented." );
        }

        @Override
        public void close()
            throws IOException
        {
            try
            {
                urlClassLoader.close();
            }
            finally
            {
                try
                {
                    zipFile.close();
                } finally
                {
                    jarFile.close();
                }

            }
        }

    }

    @Override
    public void setEncoding( Charset charset )
    {
        this.charset = charset;
    }

}
