/*
 * Capsule
 * Copyright (c) 2014-2015, Parallel Universe Software Co. All rights reserved.
 * 
 * This program and the accompanying materials are licensed under the terms 
 * of the Eclipse Public License v1.0, available at
 * http://www.eclipse.org/legal/epl-v10.html
 */

package co.paralleluniverse.common;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.util.Enumeration;
import java.util.jar.Manifest;
import java.util.zip.ZipEntry;

/**
 *
 * @author pron
 */
public final class JarClassLoader extends FlexibleClassLoader {
    private final Manifest mf;
    private final byte[] buffer;
    private final Path jarFile;

    public JarClassLoader(byte[] jar, ClassLoader parent, boolean childFirst) throws IOException {
        super(parent, childFirst);
        this.buffer = jar;
        this.jarFile = null;
        this.mf = getManifest(newInputStream());
    }

    public JarClassLoader(byte[] jar, boolean childFirst) throws IOException {
        super(childFirst);
        this.buffer = jar;
        this.jarFile = null;
        this.mf = getManifest(newInputStream());
    }

    public JarClassLoader(Path jarFile, ClassLoader parent, boolean childFirst) throws IOException {
        super(parent, childFirst);
        this.buffer = null;
        this.jarFile = jarFile;
        this.mf = getManifest(newInputStream());
    }

    public JarClassLoader(Path jarFile, boolean childFirst) throws IOException {
        super(childFirst);
        this.buffer = null;
        this.jarFile = jarFile;
        this.mf = getManifest(newInputStream());
    }

    public Manifest getManifest() {
        return mf;
    }

    @Override
    protected URL findResource1(String name) {
        try {
            if (!hasResource(name))
                return null;
            else
                return new URL("jar:" + jarFile.toUri() + "!/" + name);
        } catch (MalformedURLException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    @SuppressWarnings("unchecked")
    protected Enumeration<URL> findResources1(String name) {
        final URL url = findResource1(name);
        return (Enumeration<URL>) Collections.enumeration(url != null ? Collections.singleton(url) : Collections.emptySet());
    }

    private boolean hasResource(String path) {
        try (InputStream is = findResourceAsStream(path)) {
            return is != null;
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    protected InputStream findResourceAsStream(String path) {
        try {
            final ZipInputStream jis = new ZipInputStream(newInputStream());
            for (ZipEntry entry; (entry = jis.getNextEntry()) != null;) {
                if (path.equalsIgnoreCase(entry.getName())) {
                    if (entry.isDirectory())
                        throw new FileNotFoundException(path + " is a directory");

                    return jis;
                }
            }
            return null;
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    protected byte[] readResource(String path) {
        try (ZipInputStream jis = new ZipInputStream(newInputStream())) {
            for (ZipEntry entry; (entry = jis.getNextEntry()) != null;) {
                if (path.equalsIgnoreCase(entry.getName())) {
                    if (entry.isDirectory())
                        throw new FileNotFoundException(path + " is a directory");
                    final byte[] buf;
                    final long size = entry.getSize();
                    if (size < 0) {
                        final ByteArrayOutputStream bas = new ByteArrayOutputStream();
                        copy(jis, bas);
                        bas.close();
                        buf = bas.toByteArray();
                    } else {
                        buf = new byte[(int) size];
                        int n = 0, r;
                        for (;;) {
                            if (n == size || (r = jis.read(buf, n, buf.length - n)) == -1)
                                break;
                            n += r;
                        }
                    }
                    return buf;
                }
            }
            return null;
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    private static long copy(InputStream from, OutputStream to) throws IOException {
        byte[] buf = new byte[0x1000];  // 4K
        long total = 0;
        while (true) {
            int r = from.read(buf);
            if (r == -1) {
                break;
            }
            to.write(buf, 0, r);
            total += r;
        }
        return total;
    }

    private InputStream newInputStream() {
        try {
            return buffer != null ? new ByteArrayInputStream(buffer) : Files.newInputStream(jarFile);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    private static Manifest getManifest(InputStream is) throws IOException {
        try (JarInputStream jis = new JarInputStream(is)) {
            return jis.getManifest();
        }
    }

    @Override
    public String toString() {
        return super.toString() + '{' + jarFile.toAbsolutePath() + '}';
    }
}
