/*
 * 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.
 */

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.jar.JarOutputStream;
import java.util.zip.ZipEntry;

import org.apache.bcel.Const;
import org.apache.bcel.classfile.ClassParser;
import org.apache.bcel.classfile.Constant;
import org.apache.bcel.classfile.ConstantClass;
import org.apache.bcel.classfile.ConstantPool;
import org.apache.bcel.classfile.ConstantUtf8;
import org.apache.bcel.classfile.JavaClass;
import org.apache.bcel.classfile.Utility;
import org.apache.bcel.util.ClassPath;

/**
 * Package the client. Creates a jar file in the current directory that contains a minimal set of classes needed to run
 * the client.
 *
 * Use BCEL to extract class names and read/write classes
 */
public class Package {

    /**
     * The name of the resulting jar is Client.jar
     */
    static String defaultJar = "Client.jar";

    /*
     * See usage() for arguments. Create an instance and run that (just so not all members have to be static)
     */
    static void main(final String args[]) {
        final Package instance = new Package();
        try {
            instance.go(args);
        } catch (final Exception e) {
            e.printStackTrace();
            instance.usage();
        }
    }

    /**
     * We use a "default ClassPath object which uses the environments CLASSPATH
     */
    ClassPath classPath = ClassPath.SYSTEM_CLASS_PATH;

    /**
     * A map for all Classes, the ones we're going to package. Store class name against the JavaClass. From the JavaClass we
     * get the bytes to create the jar.
     */
    Map<String, JavaClass> allClasses = new TreeMap<>();

    /**
     * We start at the root classes, put them in here, then go through this list, putting dependent classes in here and from
     * there into allClasses. Store class names against class names of their dependents
     */
    TreeMap<String, String> dependents = new TreeMap<>();

    /**
     * Collect all classes that could not be found in the classpath. Store class names against class names of their
     * dependents
     */
    TreeMap<String, String> notFound = new TreeMap<>();

    /**
     * See wheather we print the classes that were not found (default = false)
     */
    boolean showNotFound = false;
    /**
     * Remember wheather to print allClasses at the end (default = false)
     */
    boolean printClasses = false;
    /**
     * Wheather we log classes during processing (default = false)
     */
    boolean log = false;

    /**
     * add given class to dependents (from is where its dependent from) some fiddeling to be done because of array class
     * notation
     */
    void addClassString(final String clas, final String from) throws IOException {
        if (log) {
            System.out.println("processing: " + clas + " referenced by " + from);
        }

        // must check if it's an arrary (start with "[")
        if (clas.startsWith("[")) {
            if (clas.length() == 2) {
                // it's an array of built in type, ignore
                return;
            }
            if ('L' == clas.charAt(1)) {
                // it's an array of objects, the class name is between [L and ;
                // like [Ljava/lang/Object;
                addClassString(clas.substring(2, clas.length() - 1), from);
                return;
            }
            if ('[' == clas.charAt(1)) {
                // it's an array of arrays, call recursive
                addClassString(clas.substring(1), from);
                return;
            }
            throw new IllegalArgumentException("Can't recognize class name =" + clas);
        }

        if (!clas.startsWith("java/") && allClasses.get(clas) == null) {
            dependents.put(clas, from);
            // System.out.println(" yes" );
        } else {
            // System.out.println(" no" );
        }
    }

    /**
     * Add this class to allClasses. Then go through all its dependents and add them to the dependents list if they are not
     * in allClasses
     */
    void addDependents(final JavaClass clazz) throws IOException {
        final String name = Utility.packageToPath(clazz.getClassName());
        allClasses.put(name, clazz);
        final ConstantPool pool = clazz.getConstantPool();
        for (int i = 1; i < pool.getLength(); i++) {
            final Constant cons = pool.getConstant(i);
            // System.out.println("("+i+") " + cons );
            if (cons != null && cons.getTag() == Const.CONSTANT_Class) {
                final int idx = ((ConstantClass) pool.getConstant(i)).getNameIndex();
                final String clas = ((ConstantUtf8) pool.getConstant(idx)).getBytes();
                addClassString(clas, name);
            }
        }
    }

    /**
     * the main of this class
     */
    void go(final String[] args) throws IOException {
        JavaClass clazz;
        // sort the options
        for (final String arg : args) {
            if (arg.startsWith("-e")) {
                showNotFound = true;
                continue;
            }
            if (arg.startsWith("-s")) {
                printClasses = true;
                continue;
            }
            if (arg.startsWith("-l")) {
                log = true;
                continue;
            }
            String clName = arg;
            if (clName.endsWith(JavaClass.EXTENSION)) {
                clName = clName.substring(0, clName.length() - JavaClass.EXTENSION.length());
            }
            clName = Utility.packageToPath(clName);
            try (final InputStream inputStream = classPath.getInputStream(clName)) {
                clazz = new ClassParser(inputStream, clName).parse();
            }
            // here we create the root set of classes to process
            addDependents(clazz);
            System.out.println("Packaging for class: " + clName);
        }

        if (dependents.isEmpty()) {
            usage();
            return;
        }

        System.out.println("Creating jar file: " + defaultJar);

        // starting processing: Grab from the dependents list and add back to it
        // and the allClasses list. see addDependents
        while (!dependents.isEmpty()) {
            final String name = dependents.firstKey();
            final String from = dependents.remove(name);
            if (allClasses.get(name) == null) {
                try (final InputStream inputStream = classPath.getInputStream(name)) {
                    clazz = new ClassParser(inputStream, name).parse();
                    addDependents(clazz);
                } catch (final IOException e) {
                    // System.err.println("Error, class not found " + name );
                    notFound.put(name, from);
                }
            }
        }

        if (printClasses) { // if wanted show all classes
            printAllClasses();
        }

        // create the jar
        try (final JarOutputStream jarFile = new JarOutputStream(new FileOutputStream(defaultJar))) {
            jarFile.setLevel(5); // use compression
            int written = 0;
            for (final String name : allClasses.keySet()) { // add entries for every class
                final JavaClass claz = allClasses.get(name);
                final ZipEntry zipEntry = new ZipEntry(name + JavaClass.EXTENSION);
                final byte[] bytes = claz.getBytes();
                final int length = bytes.length;
                jarFile.putNextEntry(zipEntry);
                jarFile.write(bytes, 0, length);
                written += length; // for logging
            }
            System.err.println("The jar file contains " + allClasses.size() + " classes and contains " + written + " bytes");
        }

        if (!notFound.isEmpty()) {
            System.err.println(notFound.size() + " classes could not be found");
            if (showNotFound) { // if wanted show the actual classes that we not found
                while (!notFound.isEmpty()) {
                    final String name = notFound.firstKey();
                    System.err.println(name + " (" + notFound.remove(name) + ")");
                }
            } else {
                System.err.println("Use '-e' option to view classes that were not found");
            }
        }
    }

    /**
     * Print all classes that were packaged. Sort alphabetically for better overview. Enabled by -s option
     */
    void printAllClasses() {
        final List<String> names = new ArrayList<>(allClasses.keySet());
        Collections.sort(names);
        names.forEach(System.err::println);
    }

    public void usage() {
        System.out.println(" This program packages classes and all their dependents");
        System.out.println(" into one jar. Give all starting classes (your main)");
        System.out.println(" on the command line. Use / as separator, the .class is");
        System.out.println(" optional. We use the environments CLASSPATH to resolve");
        System.out.println(" classes. Anything but java.* packages are packaged.");
        System.out.println(" If you use Class.forName (or similar), be sure to");
        System.out.println(" include the classes that you load dynamically on the");
        System.out.println(" command line.\n");
        System.out.println(" These options are recognized:");
        System.out.println(" -e -error  Show errors, meaning classes that could not ");
        System.out.println("   resolved + the classes that referenced them.");
        System.out.println(" -l -log  Show classes as they are processed. This will");
        System.out.println("   include doubles, java classes and is difficult to");
        System.out.println("   read. I use it as a sort of progress monitor");
        System.out.println(" -s -show  Prints all the classes that were packaged");
        System.out.println("   in alphabetical order, which is ordered by package");
        System.out.println("   for the most part.");
    }
}
