/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you 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.apache.cxf.jca.jarloader;


import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

import org.apache.cxf.helpers.CastUtils;


/**
 * load jars to memory from an archive
 */
public final class JarLoader {
    // map of all the archives we now about
    // this keys to this map are full distinguished names of the fromat
    // someOuterArchive!/someInnerArchive!/....
    // the values of the archives map are maps of entry names to bytes
    static Map<String, Object> archives = new HashMap<>();
    static final int CHUNK_SIZE = 4096;
    static final int MAX_CHUNK_SIZE = CHUNK_SIZE * 16;

    private JarLoader() {
    }

    public static byte[] getBytesFromInputStream(InputStream is) throws IOException {
        return getBytesFromInputStream(is, -1);
    }

    public static synchronized Map<?, ?> getJarContents(String path)
        throws MalformedURLException, IOException {
        if (!archives.containsKey(path)) {
            loadArchive(path);
        }

        return (Map<?, ?>)archives.get(path);
    }

    private static void loadArchive(String path) throws MalformedURLException, IOException {
        List<String> nameComponents = tokenizePathComponents(path);

        for (int i = 0; i < nameComponents.size(); i++) {
            if (!archives.containsKey(buildPartialName(nameComponents, i + 1))) {
                readArchive(buildPartialName(nameComponents, i + 1));

                if (i != 0) {
                    // not the root archive so there is a parentMap with a reference to the
                    // entry as raw byte array.
                    // This byte array has now been exploded into a Map so the raw bytes are
                    // no longer needed, replace the entry with the exploded Map
                    //
                    Map<String, Object> parentMap =
                        CastUtils.cast((Map<?, ?>)archives.get(buildPartialName(nameComponents, i)));
                    Map<?, ?> archiveMap =
                        (Map<?, ?>)archives.get(buildPartialName(nameComponents, i + 1));

                    parentMap.put(nameComponents.get(i), archiveMap);
                }
            }
        }
    }

    private static List<String> tokenizePathComponents(String path) {
        List<String> tokens = new LinkedList<>();
        String tmpPath = new String(path);

        while (tmpPath.length() > 0) {
            if (tmpPath.indexOf("!/") == -1) {
                tokens.add(tmpPath);
                tmpPath = "";
            } else {
                tokens.add(tmpPath.substring(0, tmpPath.indexOf("!/")));
                tmpPath = tmpPath.substring(tmpPath.indexOf("!/") + 2);
            }
        }

        return tokens;
    }

    private static String buildPartialName(List<String> nameComponents, int size) {
        StringBuilder sb = new StringBuilder();

        for (int i = 0; i < size; i++) {
            sb.append(nameComponents.get(i));
            sb.append("!/");
        }

        return sb.toString();
    }

    private static String getRootArchiveName(String name) {
        int index = name.indexOf("file:");

        if (index > -1) {
            name = name.substring(index);
        }

        if (name.indexOf("!/") != -1) {
            return name.substring(0, name.indexOf("!/"));
        }
        return name;
    }

    private static void readArchive(String name) throws MalformedURLException, IOException {
        List<String> nameComponents = tokenizePathComponents(name);
        final Map<String, Object> map;

        if (nameComponents.size() == 1) {
            map = readZipStream((new URL(getRootArchiveName(name))).openStream());
        } else {
            Map<?, ?> parentMap
                = (Map<?, ?>)archives.get(buildPartialName(nameComponents, nameComponents.size() - 1));
            byte[] bytes = (byte[])(parentMap.get(nameComponents.get(nameComponents.size() - 1)));

            if (null == bytes) {
                // unexpected, classpath entry in error, referenced jar is not in the archive
                throw new IOException(
                        "Enclosing archive " + buildPartialName(nameComponents, nameComponents.size() - 1)
                        + " has no entry named:" + nameComponents.get(nameComponents.size() - 1)
                        + ", error in archive classpath");
            }

            map = readZipStream(new ByteArrayInputStream(bytes));
        }

        archives.put(name, map);
    }

    private static Map<String, Object> readZipStream(InputStream is) throws IOException {
        ZipInputStream zis = new ZipInputStream(is);
        Map<String, Object> map = new HashMap<>();

        for (ZipEntry ze = zis.getNextEntry(); ze != null; ze = zis.getNextEntry()) {
            if (ze.isDirectory()) {
                map.put(ze.getName(), ze.getName());
            } else {
                byte[] bytes = getBytesFromInputStream(zis, ze.getSize());
                map.put(ze.getName(), bytes);
            }
        }

        return map;
    }

    private static byte[] getBytesFromInputStream(InputStream is, long size) throws IOException {

        byte[] chunk = new byte[((size > CHUNK_SIZE) && (size < MAX_CHUNK_SIZE)) ? (int)size : CHUNK_SIZE];

        ByteArrayOutputStream baos = new ByteArrayOutputStream();

        for (int i = is.read(chunk, 0, chunk.length); i != -1; i = is.read(chunk, 0, chunk.length)) {
            baos.write(chunk, 0, i);
        }

        return baos.toByteArray();
    }
}
