/*
 * Copyright (c) 2006, 2018, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code 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
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package build.tools.symbolgenerator;

import build.tools.symbolgenerator.CreateSymbols
                                  .ModuleHeaderDescription
                                  .ProvidesDescription;
import build.tools.symbolgenerator.CreateSymbols
                                  .ModuleHeaderDescription
                                  .RequiresDescription;

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.StringWriter;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.FileVisitResult;
import java.nio.file.FileVisitor;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.stream.Stream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;
import java.util.TimeZone;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import javax.tools.JavaFileManager;
import javax.tools.JavaFileManager.Location;
import javax.tools.JavaFileObject;
import javax.tools.JavaFileObject.Kind;
import javax.tools.StandardLocation;

import com.sun.source.util.JavacTask;
import com.sun.tools.classfile.AccessFlags;
import com.sun.tools.classfile.Annotation;
import com.sun.tools.classfile.Annotation.Annotation_element_value;
import com.sun.tools.classfile.Annotation.Array_element_value;
import com.sun.tools.classfile.Annotation.Class_element_value;
import com.sun.tools.classfile.Annotation.Enum_element_value;
import com.sun.tools.classfile.Annotation.Primitive_element_value;
import com.sun.tools.classfile.Annotation.element_value;
import com.sun.tools.classfile.Annotation.element_value_pair;
import com.sun.tools.classfile.AnnotationDefault_attribute;
import com.sun.tools.classfile.Attribute;
import com.sun.tools.classfile.Attributes;
import com.sun.tools.classfile.ClassFile;
import com.sun.tools.classfile.ClassWriter;
import com.sun.tools.classfile.ConstantPool;
import com.sun.tools.classfile.ConstantPool.CONSTANT_Class_info;
import com.sun.tools.classfile.ConstantPool.CONSTANT_Double_info;
import com.sun.tools.classfile.ConstantPool.CONSTANT_Float_info;
import com.sun.tools.classfile.ConstantPool.CONSTANT_Integer_info;
import com.sun.tools.classfile.ConstantPool.CONSTANT_Long_info;
import com.sun.tools.classfile.ConstantPool.CONSTANT_Module_info;
import com.sun.tools.classfile.ConstantPool.CONSTANT_Package_info;
import com.sun.tools.classfile.ConstantPool.CONSTANT_String_info;
import com.sun.tools.classfile.ConstantPool.CONSTANT_Utf8_info;
import com.sun.tools.classfile.ConstantPool.CPInfo;
import com.sun.tools.classfile.ConstantPool.InvalidIndex;
import com.sun.tools.classfile.ConstantPoolException;
import com.sun.tools.classfile.ConstantValue_attribute;
import com.sun.tools.classfile.Deprecated_attribute;
import com.sun.tools.classfile.Descriptor;
import com.sun.tools.classfile.Exceptions_attribute;
import com.sun.tools.classfile.Field;
import com.sun.tools.classfile.InnerClasses_attribute;
import com.sun.tools.classfile.InnerClasses_attribute.Info;
import com.sun.tools.classfile.Method;
import com.sun.tools.classfile.ModuleResolution_attribute;
import com.sun.tools.classfile.ModuleTarget_attribute;
import com.sun.tools.classfile.Module_attribute;
import com.sun.tools.classfile.Module_attribute.ExportsEntry;
import com.sun.tools.classfile.Module_attribute.OpensEntry;
import com.sun.tools.classfile.Module_attribute.ProvidesEntry;
import com.sun.tools.classfile.Module_attribute.RequiresEntry;
import com.sun.tools.classfile.RuntimeAnnotations_attribute;
import com.sun.tools.classfile.RuntimeInvisibleAnnotations_attribute;
import com.sun.tools.classfile.RuntimeInvisibleParameterAnnotations_attribute;
import com.sun.tools.classfile.RuntimeParameterAnnotations_attribute;
import com.sun.tools.classfile.RuntimeVisibleAnnotations_attribute;
import com.sun.tools.classfile.RuntimeVisibleParameterAnnotations_attribute;
import com.sun.tools.classfile.Signature_attribute;
import com.sun.tools.javac.api.JavacTool;
import com.sun.tools.javac.jvm.Target;
import com.sun.tools.javac.util.Assert;
import com.sun.tools.javac.util.Context;
import com.sun.tools.javac.util.Pair;

/**
 * A tool for processing the .sym.txt files. It allows to:
 *  * convert the .sym.txt into class/sig files for ct.sym
 *  * in cooperation with the adjacent history Probe, construct .sym.txt files for previous platforms
 *  * enhance existing .sym.txt files with a a new set .sym.txt for the current platform
 *
 * To convert the .sym.txt files to class/sig files from ct.sym, run:
 *     java build.tool.symbolgenerator.CreateSymbols build-ctsym <platform-description-file> <target-directory>
 *
 * The <platform-description-file> is a file of this format:
 *     generate platforms <platform-ids-to-generate separate with ':'>
 *     platform version <platform-id1> files <.sym.txt files containing history data for given platform, separate with ':'>
 *     platform version <platform-id2> base <base-platform-id> files <.sym.txt files containing history data for given platform, separate with ':'>
 *
 * The content of platform "<base-platform-id>" is also automatically added to the content of
 * platform "<platform-id2>", unless explicitly excluded in "<platform-id2>"'s .sym.txt files.
 *
 * To create the .sym.txt files, first run the history Probe for all the previous platforms:
 *     <jdk-N>/bin/java build.tools.symbolgenerator.Probe <classes-for-N>
 *
 * Where <classes-for-N> is a name of a file into which the classes from the bootclasspath of <jdk-N>
 * will be written.
 *
 * Then create the <platform-description-file> file and the .sym.txt files like this:
 *     java build.tools.symbolgenerator.CreateSymbols build-description <target-directory> <path-to-a-JDK-root> <include-list-file>
 *                                                    <platform-id1> <target-file-for-platform1> "<none>"
 *                                                    <platform-id2> <target-file-for-platform2> <diff-against-platform2>
 *                                                    <platform-id3> <target-file-for-platform3> <diff-against-platform3>
 *                                                    ...
 *
 * The <include-list-file> is a file that specifies classes that should be included/excluded.
 * Lines that start with '+' represent class or package that should be included, '-' class or package
 * that should be excluded. '/' should be used as package name delimiter, packages should end with '/'.
 * Several include list files may be specified, separated by File.pathSeparator.
 *
 * When <diff-against-platformN> is specified, the .sym.txt files for platform N will only contain
 * differences between platform N and the specified platform. The first platform (denoted F further)
 * that is specified should use literal value "<none>", to have all the APIs of the platform written to
 * the .sym.txt files. If there is an existing platform with full .sym.txt files in the repository,
 * that platform should be used as the first platform to avoid unnecessary changes to the .sym.txt
 * files. The <diff-against-platformN> for platform N should be determined as follows: if N < F, then
 * <diff-against-platformN> should be N + 1. If F < N, then <diff-against-platformN> should be N - 1.
 * If N is a custom/specialized sub-version of another platform N', then <diff-against-platformN> should be N'.
 *
 * To generate the .sym.txt files for OpenJDK 7 and 8:
 *     <jdk-7>/bin/java build.tools.symbolgenerator.Probe OpenJDK7.classes
 *     <jdk-8>/bin/java build.tools.symbolgenerator.Probe OpenJDK8.classes
 *     java build.tools.symbolgenerator.CreateSymbols build-description make/data/symbols $TOPDIR make/data/symbols/include.list
 *                                                    8 OpenJDK8.classes '<none>'
 *                                                    7 OpenJDK7.classes 8
 *
 * Note: the versions are expected to be a single character.
 *
 * To enhance existing historical data with data for JDK N, run:
 *     <jdk-N>/bin/java build.tools.symbolgenerator.CreateSymbols build-description-incremental make/data/symbols/symbols make/data/symbols/include.list
 *
 */
public class CreateSymbols {

    //<editor-fold defaultstate="collapsed" desc="ct.sym construction">
    /**Create sig files for ct.sym reading the classes description from the directory that contains
     * {@code ctDescriptionFile}, using the file as a recipe to create the sigfiles.
     */
    @SuppressWarnings("unchecked")
    public void createSymbols(String ctDescriptionFile, String ctSymLocation, CtSymKind ctSymKind) throws IOException {
        LoadDescriptions data = load(Paths.get(ctDescriptionFile));

        splitHeaders(data.classes);

        for (ModuleDescription md : data.modules.values()) {
            for (ModuleHeaderDescription mhd : md.header) {
                List<String> versionsList =
                        Collections.singletonList(mhd.versions);
                writeModulesForVersions(ctSymLocation,
                                        md,
                                        mhd,
                                        versionsList);
            }
        }

        for (ClassDescription classDescription : data.classes) {
            for (ClassHeaderDescription header : classDescription.header) {
                switch (ctSymKind) {
                    case JOINED_VERSIONS:
                        Set<String> jointVersions = new HashSet<>();
                        jointVersions.add(header.versions);
                        limitJointVersion(jointVersions, classDescription.fields);
                        limitJointVersion(jointVersions, classDescription.methods);
                        writeClassesForVersions(ctSymLocation, classDescription, header, jointVersions);
                        break;
                    case SEPARATE:
                        Set<String> versions = new HashSet<>();
                        for (char v : header.versions.toCharArray()) {
                            versions.add("" + v);
                        }
                        writeClassesForVersions(ctSymLocation, classDescription, header, versions);
                        break;
                }
            }
        }
    }

    public static String EXTENSION = ".sig";

    LoadDescriptions load(Path ctDescription) throws IOException {
        List<PlatformInput> platforms = new ArrayList<>();
        Set<String> generatePlatforms = null;

        try (LineBasedReader reader = new LineBasedReader(ctDescription)) {
            while (reader.hasNext()) {
                switch (reader.lineKey) {
                    case "generate":
                        String[] platformsAttr = reader.attributes.get("platforms").split(":");
                        generatePlatforms = new HashSet<>(List.of(platformsAttr));
                        reader.moveNext();
                        break;
                    case "platform":
                        PlatformInput platform = PlatformInput.load(reader);
                        platforms.add(platform);
                        reader.moveNext();
                        break;
                    default:
                        throw new IllegalStateException("Unknown key: " + reader.lineKey);
                }
            }
        }

        Map<String, ClassDescription> classes = new LinkedHashMap<>();
        Map<String, ModuleDescription> modules = new LinkedHashMap<>();

        for (PlatformInput platform: platforms) {
            for (ClassDescription cd : classes.values()) {
                addNewVersion(cd.header, platform.basePlatform, platform.version);
                addNewVersion(cd.fields, platform.basePlatform, platform.version);
                addNewVersion(cd.methods, platform.basePlatform, platform.version);
            }
            for (ModuleDescription md : modules.values()) {
                addNewVersion(md.header, platform.basePlatform, platform.version);
            }
            for (String input : platform.files) {
                Path inputFile = ctDescription.getParent().resolve(input);
                try (LineBasedReader reader = new LineBasedReader(inputFile)) {
                    while (reader.hasNext()) {
                        String nameAttr = reader.attributes.get("name");
                        switch (reader.lineKey) {
                            case "class": case "-class":
                                ClassDescription cd =
                                        classes.computeIfAbsent(nameAttr,
                                                n -> new ClassDescription());
                                if ("-class".equals(reader.lineKey)) {
                                    removeVersion(cd.header, h -> true,
                                                  platform.version);
                                    reader.moveNext();
                                    continue;
                                }
                                cd.read(reader, platform.basePlatform,
                                        platform.version);
                                break;
                            case "module": {
                                ModuleDescription md =
                                        modules.computeIfAbsent(nameAttr,
                                                n -> new ModuleDescription());
                                md.read(reader, platform.basePlatform,
                                        platform.version);
                                break;
                            }
                            case "-module": {
                                ModuleDescription md =
                                        modules.computeIfAbsent(nameAttr,
                                                n -> new ModuleDescription());
                                removeVersion(md.header, h -> true,
                                              platform.version);
                                reader.moveNext();
                                break;
                            }
                        }
                    }
                }
            }
        }

        ClassList result = new ClassList();

        classes.values().forEach(result::add);
        return new LoadDescriptions(result,
                                    modules,
                                    new ArrayList<>(platforms));
    }

    private static void removeVersion(LoadDescriptions load, String deletePlatform) {
        for (Iterator<ClassDescription> it = load.classes.iterator(); it.hasNext();) {
            ClassDescription desc = it.next();
            Iterator<ClassHeaderDescription> chdIt = desc.header.iterator();

            while (chdIt.hasNext()) {
                ClassHeaderDescription chd = chdIt.next();

                chd.versions = removeVersion(chd.versions, deletePlatform);
                if (chd.versions.isEmpty()) {
                    chdIt.remove();
                }
            }

            if (desc.header.isEmpty()) {
                it.remove();
                continue;
            }

            Iterator<MethodDescription> methodIt = desc.methods.iterator();

            while (methodIt.hasNext()) {
                MethodDescription method = methodIt.next();

                method.versions = removeVersion(method.versions, deletePlatform);
                if (method.versions.isEmpty())
                    methodIt.remove();
            }

            Iterator<FieldDescription> fieldIt = desc.fields.iterator();

            while (fieldIt.hasNext()) {
                FieldDescription field = fieldIt.next();

                field.versions = removeVersion(field.versions, deletePlatform);
                if (field.versions.isEmpty())
                    fieldIt.remove();
            }
        }

        for (Iterator<ModuleDescription> it = load.modules.values().iterator(); it.hasNext();) {
            ModuleDescription desc = it.next();
            Iterator<ModuleHeaderDescription> mhdIt = desc.header.iterator();

            while (mhdIt.hasNext()) {
                ModuleHeaderDescription mhd = mhdIt.next();

                mhd.versions = removeVersion(mhd.versions, deletePlatform);
                if (mhd.versions.isEmpty())
                    mhdIt.remove();
            }

            if (desc.header.isEmpty()) {
                it.remove();
                continue;
            }
        }
    }

    static final class LoadDescriptions {
        public final ClassList classes;
        public final Map<String, ModuleDescription> modules;
        public final List<PlatformInput> versions;

        public LoadDescriptions(ClassList classes,
                                Map<String, ModuleDescription>  modules,
                                List<PlatformInput> versions) {
            this.classes = classes;
            this.modules = modules;
            this.versions = versions;
        }

    }

    static final class LineBasedReader implements AutoCloseable {
        private final BufferedReader input;
        public String lineKey;
        public Map<String, String> attributes = new HashMap<>();

        public LineBasedReader(Path input) throws IOException {
            this.input = Files.newBufferedReader(input);
            moveNext();
        }

        public void moveNext() throws IOException {
            String line = input.readLine();

            if (line == null) {
                lineKey = null;
                return ;
            }

            if (line.trim().isEmpty() || line.startsWith("#")) {
                moveNext();
                return ;
            }

            String[] parts = line.split(" ");

            lineKey = parts[0];
            attributes.clear();

            for (int i = 1; i < parts.length; i += 2) {
                attributes.put(parts[i], unquote(parts[i + 1]));
            }
        }

        public boolean hasNext() {
            return lineKey != null;
        }

        @Override
        public void close() throws IOException {
            input.close();
        }
    }

    private static String reduce(String original, String other) {
        Set<String> otherSet = new HashSet<>();

        for (char v : other.toCharArray()) {
            otherSet.add("" + v);
        }

        return reduce(original, otherSet);
    }

    private static String reduce(String original, Set<String> generate) {
        StringBuilder sb = new StringBuilder();

        for (char v : original.toCharArray()) {
            if (generate.contains("" + v)) {
                sb.append(v);
            }
        }
        return sb.toString();
    }

    private static String removeVersion(String original, String remove) {
        StringBuilder sb = new StringBuilder();

        for (char v : original.toCharArray()) {
            if (v != remove.charAt(0)) {
                sb.append(v);
            }
        }
        return sb.toString();
    }

    private static class PlatformInput {
        public final String version;
        public final String basePlatform;
        public final List<String> files;
        public PlatformInput(String version, String basePlatform, List<String> files) {
            this.version = version;
            this.basePlatform = basePlatform;
            this.files = files;
        }

        public static PlatformInput load(LineBasedReader in) throws IOException {
            return new PlatformInput(in.attributes.get("version"),
                                     in.attributes.get("base"),
                                     List.of(in.attributes.get("files").split(":")));
        }
    }

    static void addNewVersion(Collection<? extends FeatureDescription> features,
                       String baselineVersion,
                       String version) {
        features.stream()
                .filter(f -> f.versions.contains(baselineVersion))
                .forEach(f -> f.versions += version);
    }

    static <T extends FeatureDescription> void removeVersion(Collection<T> features,
                                                             Predicate<T> shouldRemove,
                                                             String version) {
        for (T existing : features) {
            if (shouldRemove.test(existing) && existing.versions.endsWith(version)) {
                existing.versions = existing.versions.replace(version, "");
                return;
            }
        }
    }

    /**Changes to class header of an outer class (like adding a new type parameter) may affect
     * its innerclasses. So if the outer class's header is different for versions A and B, need to
     * split its innerclasses headers to also be different for versions A and B.
     */
    static void splitHeaders(ClassList classes) {
        Set<String> ctVersions = new HashSet<>();

        for (ClassDescription cd : classes) {
            for (ClassHeaderDescription header : cd.header) {
                for (char c : header.versions.toCharArray()) {
                    ctVersions.add("" + c);
                }
            }
        }

        classes.sort();

        for (ClassDescription cd : classes) {
            Map<String, String> outerSignatures2Version = new HashMap<>();

            for (String version : ctVersions) { //XXX
                ClassDescription outer = cd;
                String outerSignatures = "";

                while ((outer = classes.enclosingClass(outer)) != null) {
                    for (ClassHeaderDescription outerHeader : outer.header) {
                        if (outerHeader.versions.contains(version)) {
                            outerSignatures += outerHeader.signature;
                        }
                    }
                }

                outerSignatures2Version.compute(outerSignatures,
                                                 (key, value) -> value != null ? value + version : version);
            }

            List<ClassHeaderDescription> newHeaders = new ArrayList<>();

            HEADER_LOOP: for (ClassHeaderDescription header : cd.header) {
                for (String versions : outerSignatures2Version.values()) {
                    if (containsAll(versions, header.versions)) {
                        newHeaders.add(header);
                        continue HEADER_LOOP;
                    }
                    if (disjoint(versions, header.versions)) {
                        continue;
                    }
                    ClassHeaderDescription newHeader = new ClassHeaderDescription();
                    newHeader.classAnnotations = header.classAnnotations;
                    newHeader.deprecated = header.deprecated;
                    newHeader.extendsAttr = header.extendsAttr;
                    newHeader.flags = header.flags;
                    newHeader.implementsAttr = header.implementsAttr;
                    newHeader.innerClasses = header.innerClasses;
                    newHeader.runtimeAnnotations = header.runtimeAnnotations;
                    newHeader.signature = header.signature;
                    newHeader.versions = reduce(versions, header.versions);

                    newHeaders.add(newHeader);
                }
            }

            cd.header = newHeaders;
        }
    }

    void limitJointVersion(Set<String> jointVersions, List<? extends FeatureDescription> features) {
        for (FeatureDescription feature : features) {
            for (String version : jointVersions) {
                if (!containsAll(feature.versions, version) &&
                    !disjoint(feature.versions, version)) {
                    StringBuilder featurePart = new StringBuilder();
                    StringBuilder otherPart = new StringBuilder();
                    for (char v : version.toCharArray()) {
                        if (feature.versions.indexOf(v) != (-1)) {
                            featurePart.append(v);
                        } else {
                            otherPart.append(v);
                        }
                    }
                    jointVersions.remove(version);
                    if (featurePart.length() == 0 || otherPart.length() == 0) {
                        throw new AssertionError();
                    }
                    jointVersions.add(featurePart.toString());
                    jointVersions.add(otherPart.toString());
                    break;
                }
            }
        }
    }

    private static boolean containsAll(String versions, String subVersions) {
        for (char c : subVersions.toCharArray()) {
            if (versions.indexOf(c) == (-1))
                return false;
        }
        return true;
    }

    private static boolean disjoint(String version1, String version2) {
        for (char c : version2.toCharArray()) {
            if (version1.indexOf(c) != (-1))
                return false;
        }
        return true;
    }

    void writeClassesForVersions(String ctSymLocation,
                                 ClassDescription classDescription,
                                 ClassHeaderDescription header,
                                 Iterable<String> versions)
            throws IOException {
        for (String ver : versions) {
            writeClass(ctSymLocation, classDescription, header, ver);
        }
    }

    void writeModulesForVersions(String ctSymLocation,
                                 ModuleDescription moduleDescription,
                                 ModuleHeaderDescription header,
                                 Iterable<String> versions)
            throws IOException {
        for (String ver : versions) {
            writeModule(ctSymLocation, moduleDescription, header, ver);
        }
    }

    public enum CtSymKind {
        JOINED_VERSIONS,
        SEPARATE;
    }

    //<editor-fold defaultstate="collapsed" desc="Class Writing">
    void writeModule(String ctSymLocation,
                    ModuleDescription moduleDescription,
                    ModuleHeaderDescription header,
                    String version) throws IOException {
        List<CPInfo> constantPool = new ArrayList<>();
        constantPool.add(null);
        int currentClass = addClass(constantPool, "module-info");
        int superclass = 0;
        int[] interfaces = new int[0];
        AccessFlags flags = new AccessFlags(header.flags);
        Map<String, Attribute> attributesMap = new HashMap<>();
        addAttributes(moduleDescription, header, constantPool, attributesMap);
        Attributes attributes = new Attributes(attributesMap);
        CPInfo[] cpData = constantPool.toArray(new CPInfo[constantPool.size()]);
        ConstantPool cp = new ConstantPool(cpData);
        ClassFile classFile = new ClassFile(0xCAFEBABE,
                Target.DEFAULT.minorVersion,
                Target.DEFAULT.majorVersion,
                cp,
                flags,
                currentClass,
                superclass,
                interfaces,
                new Field[0],
                new Method[0],
                attributes);

        Path outputClassFile = Paths.get(ctSymLocation,
                                         version + "-modules",
                                         moduleDescription.name,
                                         "module-info" + EXTENSION);

        Files.createDirectories(outputClassFile.getParent());

        try (OutputStream out = Files.newOutputStream(outputClassFile)) {
            ClassWriter w = new ClassWriter();

            w.write(classFile, out);
        }
    }

    void writeClass(String ctSymLocation,
                    ClassDescription classDescription,
                    ClassHeaderDescription header,
                    String version) throws IOException {
        List<CPInfo> constantPool = new ArrayList<>();
        constantPool.add(null);
        List<Method> methods = new ArrayList<>();
        for (MethodDescription methDesc : classDescription.methods) {
            if (disjoint(methDesc.versions, version))
                continue;
            Descriptor descriptor = new Descriptor(addString(constantPool, methDesc.descriptor));
            //TODO: LinkedHashMap to avoid param annotations vs. Signature problem in javac's ClassReader:
            Map<String, Attribute> attributesMap = new LinkedHashMap<>();
            addAttributes(methDesc, constantPool, attributesMap);
            Attributes attributes = new Attributes(attributesMap);
            AccessFlags flags = new AccessFlags(methDesc.flags);
            int nameString = addString(constantPool, methDesc.name);
            methods.add(new Method(flags, nameString, descriptor, attributes));
        }
        List<Field> fields = new ArrayList<>();
        for (FieldDescription fieldDesc : classDescription.fields) {
            if (disjoint(fieldDesc.versions, version))
                continue;
            Descriptor descriptor = new Descriptor(addString(constantPool, fieldDesc.descriptor));
            Map<String, Attribute> attributesMap = new HashMap<>();
            addAttributes(fieldDesc, constantPool, attributesMap);
            Attributes attributes = new Attributes(attributesMap);
            AccessFlags flags = new AccessFlags(fieldDesc.flags);
            int nameString = addString(constantPool, fieldDesc.name);
            fields.add(new Field(flags, nameString, descriptor, attributes));
        }
        int currentClass = addClass(constantPool, classDescription.name);
        int superclass = header.extendsAttr != null ? addClass(constantPool, header.extendsAttr) : 0;
        int[] interfaces = new int[header.implementsAttr.size()];
        int i = 0;
        for (String intf : header.implementsAttr) {
            interfaces[i++] = addClass(constantPool, intf);
        }
        AccessFlags flags = new AccessFlags(header.flags);
        Map<String, Attribute> attributesMap = new HashMap<>();
        addAttributes(header, constantPool, attributesMap);
        Attributes attributes = new Attributes(attributesMap);
        ConstantPool cp = new ConstantPool(constantPool.toArray(new CPInfo[constantPool.size()]));
        ClassFile classFile = new ClassFile(0xCAFEBABE,
                Target.DEFAULT.minorVersion,
                Target.DEFAULT.majorVersion,
                cp,
                flags,
                currentClass,
                superclass,
                interfaces,
                fields.toArray(new Field[0]),
                methods.toArray(new Method[0]),
                attributes);

        Path outputClassFile = Paths.get(ctSymLocation, version, classDescription.name + EXTENSION);

        Files.createDirectories(outputClassFile.getParent());

        try (OutputStream out = Files.newOutputStream(outputClassFile)) {
            ClassWriter w = new ClassWriter();

            w.write(classFile, out);
        }
    }

    private void addAttributes(ModuleDescription md,
                               ModuleHeaderDescription header,
                               List<CPInfo> cp,
                               Map<String, Attribute> attributes) {
        addGenericAttributes(header, cp, attributes);
        if (header.moduleResolution != null) {
            int attrIdx = addString(cp, Attribute.ModuleResolution);
            final ModuleResolution_attribute resIdx =
                    new ModuleResolution_attribute(attrIdx,
                                                   header.moduleResolution);
            attributes.put(Attribute.ModuleResolution, resIdx);
        }
        if (header.moduleTarget != null) {
            int attrIdx = addString(cp, Attribute.ModuleTarget);
            int targetIdx = addString(cp, header.moduleTarget);
            attributes.put(Attribute.ModuleTarget,
                           new ModuleTarget_attribute(attrIdx, targetIdx));
        }
        int attrIdx = addString(cp, Attribute.Module);
        attributes.put(Attribute.Module,
                       new Module_attribute(attrIdx,
                             addModuleName(cp, md.name),
                             0,
                             0,
                             header.requires
                                   .stream()
                                   .map(r -> createRequiresEntry(cp, r))
                                   .collect(Collectors.toList())
                                   .toArray(new RequiresEntry[0]),
                             header.exports
                                   .stream()
                                   .map(e -> createExportsEntry(cp, e))
                                   .collect(Collectors.toList())
                                   .toArray(new ExportsEntry[0]),
                             header.opens
                                   .stream()
                                   .map(e -> createOpensEntry(cp, e))
                                   .collect(Collectors.toList())
                                   .toArray(new OpensEntry[0]),
                             header.uses
                                   .stream()
                                   .mapToInt(u -> addClassName(cp, u))
                                   .toArray(),
                             header.provides
                                   .stream()
                                   .map(p -> createProvidesEntry(cp, p))
                                   .collect(Collectors.toList())
                                   .toArray(new ProvidesEntry[0])));
        addInnerClassesAttribute(header, cp, attributes);
    }

    private static RequiresEntry createRequiresEntry(List<CPInfo> cp,
            RequiresDescription r) {
        final int idx = addModuleName(cp, r.moduleName);
        return new RequiresEntry(idx,
                                 r.flags,
                                 r.version != null
                                         ? addInt(cp, r.version)
                                         : 0);
    }

    private static ExportsEntry createExportsEntry(List<CPInfo> cp,
                                                   String e) {
        return new ExportsEntry(addPackageName(cp, e), 0, new int[0]);
    }

    private static OpensEntry createOpensEntry(List<CPInfo> cp, String e) {
        return new OpensEntry(addPackageName(cp, e), 0, new int[0]);
    }

    private static ProvidesEntry createProvidesEntry(List<CPInfo> cp,
            ModuleHeaderDescription.ProvidesDescription p) {
        final int idx = addClassName(cp, p.interfaceName);
        return new ProvidesEntry(idx, p.implNames
                                       .stream()
                                       .mapToInt(i -> addClassName(cp, i))
                                       .toArray());
    }

    private void addAttributes(ClassHeaderDescription header,
            List<CPInfo> constantPool, Map<String, Attribute> attributes) {
        addGenericAttributes(header, constantPool, attributes);
        addInnerClassesAttribute(header, constantPool, attributes);
    }

    private void addInnerClassesAttribute(HeaderDescription header,
            List<CPInfo> constantPool, Map<String, Attribute> attributes) {
        if (header.innerClasses != null && !header.innerClasses.isEmpty()) {
            Info[] innerClasses = new Info[header.innerClasses.size()];
            int i = 0;
            for (InnerClassInfo info : header.innerClasses) {
                innerClasses[i++] =
                        new Info(info.innerClass == null ? 0 : addClass(constantPool, info.innerClass),
                                 info.outerClass == null ? 0 : addClass(constantPool, info.outerClass),
                                 info.innerClassName == null ? 0 : addString(constantPool, info.innerClassName),
                                 new AccessFlags(info.innerClassFlags));
            }
            int attributeString = addString(constantPool, Attribute.InnerClasses);
            attributes.put(Attribute.InnerClasses,
                           new InnerClasses_attribute(attributeString, innerClasses));
        }
    }

    private void addAttributes(MethodDescription desc, List<CPInfo> constantPool, Map<String, Attribute> attributes) {
        addGenericAttributes(desc, constantPool, attributes);
        if (desc.thrownTypes != null) {
            int[] exceptions = new int[desc.thrownTypes.size()];
            int i = 0;
            for (String exc : desc.thrownTypes) {
                exceptions[i++] = addClass(constantPool, exc);
            }
            int attributeString = addString(constantPool, Attribute.Exceptions);
            attributes.put(Attribute.Exceptions,
                           new Exceptions_attribute(attributeString, exceptions));
        }
        if (desc.annotationDefaultValue != null) {
            int attributeString = addString(constantPool, Attribute.AnnotationDefault);
            element_value attributeValue = createAttributeValue(constantPool,
                                                                desc.annotationDefaultValue);
            attributes.put(Attribute.AnnotationDefault,
                           new AnnotationDefault_attribute(attributeString, attributeValue));
        }
        if (desc.classParameterAnnotations != null && !desc.classParameterAnnotations.isEmpty()) {
            int attributeString =
                    addString(constantPool, Attribute.RuntimeInvisibleParameterAnnotations);
            Annotation[][] annotations =
                    createParameterAnnotations(constantPool, desc.classParameterAnnotations);
            attributes.put(Attribute.RuntimeInvisibleParameterAnnotations,
                           new RuntimeInvisibleParameterAnnotations_attribute(attributeString,
                                   annotations));
        }
        if (desc.runtimeParameterAnnotations != null && !desc.runtimeParameterAnnotations.isEmpty()) {
            int attributeString =
                    addString(constantPool, Attribute.RuntimeVisibleParameterAnnotations);
            Annotation[][] annotations =
                    createParameterAnnotations(constantPool, desc.runtimeParameterAnnotations);
            attributes.put(Attribute.RuntimeVisibleParameterAnnotations,
                           new RuntimeVisibleParameterAnnotations_attribute(attributeString,
                                   annotations));
        }
    }

    private void addAttributes(FieldDescription desc, List<CPInfo> constantPool, Map<String, Attribute> attributes) {
        addGenericAttributes(desc, constantPool, attributes);
        if (desc.constantValue != null) {
            Pair<Integer, Character> constantPoolEntry =
                    addConstant(constantPool, desc.constantValue, false);
            Assert.checkNonNull(constantPoolEntry);
            int constantValueString = addString(constantPool, Attribute.ConstantValue);
            attributes.put(Attribute.ConstantValue,
                           new ConstantValue_attribute(constantValueString, constantPoolEntry.fst));
        }
    }

    private void addGenericAttributes(FeatureDescription desc, List<CPInfo> constantPool, Map<String, Attribute> attributes) {
        if (desc.deprecated) {
            int attributeString = addString(constantPool, Attribute.Deprecated);
            attributes.put(Attribute.Deprecated,
                           new Deprecated_attribute(attributeString));
        }
        if (desc.signature != null) {
            int attributeString = addString(constantPool, Attribute.Signature);
            int signatureString = addString(constantPool, desc.signature);
            attributes.put(Attribute.Signature,
                           new Signature_attribute(attributeString, signatureString));
        }
        if (desc.classAnnotations != null && !desc.classAnnotations.isEmpty()) {
            int attributeString = addString(constantPool, Attribute.RuntimeInvisibleAnnotations);
            Annotation[] annotations = createAnnotations(constantPool, desc.classAnnotations);
            attributes.put(Attribute.RuntimeInvisibleAnnotations,
                           new RuntimeInvisibleAnnotations_attribute(attributeString, annotations));
        }
        if (desc.runtimeAnnotations != null && !desc.runtimeAnnotations.isEmpty()) {
            int attributeString = addString(constantPool, Attribute.RuntimeVisibleAnnotations);
            Annotation[] annotations = createAnnotations(constantPool, desc.runtimeAnnotations);
            attributes.put(Attribute.RuntimeVisibleAnnotations,
                           new RuntimeVisibleAnnotations_attribute(attributeString, annotations));
        }
    }

    private Annotation[] createAnnotations(List<CPInfo> constantPool, List<AnnotationDescription> desc) {
        Annotation[] result = new Annotation[desc.size()];
        int i = 0;

        for (AnnotationDescription ad : desc) {
            result[i++] = createAnnotation(constantPool, ad);
        }

        return result;
    }

    private Annotation[][] createParameterAnnotations(List<CPInfo> constantPool, List<List<AnnotationDescription>> desc) {
        Annotation[][] result = new Annotation[desc.size()][];
        int i = 0;

        for (List<AnnotationDescription> paramAnnos : desc) {
            result[i++] = createAnnotations(constantPool, paramAnnos);
        }

        return result;
    }

    private Annotation createAnnotation(List<CPInfo> constantPool, AnnotationDescription desc) {
        return new Annotation(null,
                              addString(constantPool, desc.annotationType),
                              createElementPairs(constantPool, desc.values));
    }

    private element_value_pair[] createElementPairs(List<CPInfo> constantPool, Map<String, Object> annotationAttributes) {
        element_value_pair[] pairs = new element_value_pair[annotationAttributes.size()];
        int i = 0;

        for (Entry<String, Object> e : annotationAttributes.entrySet()) {
            int elementNameString = addString(constantPool, e.getKey());
            element_value value = createAttributeValue(constantPool, e.getValue());
            pairs[i++] = new element_value_pair(elementNameString, value);
        }

        return pairs;
    }

    private element_value createAttributeValue(List<CPInfo> constantPool, Object value) {
        Pair<Integer, Character> constantPoolEntry = addConstant(constantPool, value, true);
        if (constantPoolEntry != null) {
            return new Primitive_element_value(constantPoolEntry.fst, constantPoolEntry.snd);
        } else if (value instanceof EnumConstant) {
            EnumConstant ec = (EnumConstant) value;
            return new Enum_element_value(addString(constantPool, ec.type),
                                          addString(constantPool, ec.constant),
                                          'e');
        } else if (value instanceof ClassConstant) {
            ClassConstant cc = (ClassConstant) value;
            return new Class_element_value(addString(constantPool, cc.type), 'c');
        } else if (value instanceof AnnotationDescription) {
            Annotation annotation = createAnnotation(constantPool, ((AnnotationDescription) value));
            return new Annotation_element_value(annotation, '@');
        } else if (value instanceof Collection) {
            @SuppressWarnings("unchecked")
                    Collection<Object> array = (Collection<Object>) value;
            element_value[] values = new element_value[array.size()];
            int i = 0;

            for (Object elem : array) {
                values[i++] = createAttributeValue(constantPool, elem);
            }

            return new Array_element_value(values, '[');
        }
        throw new IllegalStateException(value.getClass().getName());
    }

    private static Pair<Integer, Character> addConstant(List<CPInfo> constantPool, Object value, boolean annotation) {
        if (value instanceof Boolean) {
            return Pair.of(addToCP(constantPool, new CONSTANT_Integer_info(((Boolean) value) ? 1 : 0)), 'Z');
        } else if (value instanceof Byte) {
            return Pair.of(addToCP(constantPool, new CONSTANT_Integer_info((byte) value)), 'B');
        } else if (value instanceof Character) {
            return Pair.of(addToCP(constantPool, new CONSTANT_Integer_info((char) value)), 'C');
        } else if (value instanceof Short) {
            return Pair.of(addToCP(constantPool, new CONSTANT_Integer_info((short) value)), 'S');
        } else if (value instanceof Integer) {
            return Pair.of(addToCP(constantPool, new CONSTANT_Integer_info((int) value)), 'I');
        } else if (value instanceof Long) {
            return Pair.of(addToCP(constantPool, new CONSTANT_Long_info((long) value)), 'J');
        } else if (value instanceof Float) {
            return Pair.of(addToCP(constantPool, new CONSTANT_Float_info((float) value)), 'F');
        } else if (value instanceof Double) {
            return Pair.of(addToCP(constantPool, new CONSTANT_Double_info((double) value)), 'D');
        } else if (value instanceof String) {
            int stringIndex = addString(constantPool, (String) value);
            if (annotation) {
                return Pair.of(stringIndex, 's');
            } else {
                return Pair.of(addToCP(constantPool, new CONSTANT_String_info(null, stringIndex)), 's');
            }
        }

        return null;
    }

    private static int addString(List<CPInfo> constantPool, String string) {
        Assert.checkNonNull(string);

        int i = 0;
        for (CPInfo info : constantPool) {
            if (info instanceof CONSTANT_Utf8_info) {
                if (((CONSTANT_Utf8_info) info).value.equals(string)) {
                    return i;
                }
            }
            i++;
        }

        return addToCP(constantPool, new CONSTANT_Utf8_info(string));
    }

    private static int addInt(List<CPInfo> constantPool, int value) {
        int i = 0;
        for (CPInfo info : constantPool) {
            if (info instanceof CONSTANT_Integer_info) {
                if (((CONSTANT_Integer_info) info).value == value) {
                    return i;
                }
            }
            i++;
        }

        return addToCP(constantPool, new CONSTANT_Integer_info(value));
    }

    private static int addModuleName(List<CPInfo> constantPool, String moduleName) {
        int nameIdx = addString(constantPool, moduleName);
        int i = 0;
        for (CPInfo info : constantPool) {
            if (info instanceof CONSTANT_Module_info) {
                if (((CONSTANT_Module_info) info).name_index == nameIdx) {
                    return i;
                }
            }
            i++;
        }

        return addToCP(constantPool, new CONSTANT_Module_info(null, nameIdx));
    }

    private static int addPackageName(List<CPInfo> constantPool, String packageName) {
        int nameIdx = addString(constantPool, packageName);
        int i = 0;
        for (CPInfo info : constantPool) {
            if (info instanceof CONSTANT_Package_info) {
                if (((CONSTANT_Package_info) info).name_index == nameIdx) {
                    return i;
                }
            }
            i++;
        }

        return addToCP(constantPool, new CONSTANT_Package_info(null, nameIdx));
    }

    private static int addClassName(List<CPInfo> constantPool, String className) {
        int nameIdx = addString(constantPool, className);
        int i = 0;
        for (CPInfo info : constantPool) {
            if (info instanceof CONSTANT_Class_info) {
                if (((CONSTANT_Class_info) info).name_index == nameIdx) {
                    return i;
                }
            }
            i++;
        }

        return addToCP(constantPool, new CONSTANT_Class_info(null, nameIdx));
    }

    private static int addToCP(List<CPInfo> constantPool, CPInfo entry) {
        int result = constantPool.size();

        constantPool.add(entry);

        if (entry.size() > 1) {
            constantPool.add(null);
        }

        return result;
    }

    private static int addClass(List<CPInfo> constantPool, String className) {
        int classNameIndex = addString(constantPool, className);

        int i = 0;
        for (CPInfo info : constantPool) {
            if (info instanceof CONSTANT_Class_info) {
                if (((CONSTANT_Class_info) info).name_index == classNameIndex) {
                    return i;
                }
            }
            i++;
        }

        return addToCP(constantPool, new CONSTANT_Class_info(null, classNameIndex));
    }
    //</editor-fold>
    //</editor-fold>

    //<editor-fold defaultstate="collapsed" desc="Create Symbol Description">
    public void createBaseLine(List<VersionDescription> versions,
                               ExcludeIncludeList excludesIncludes,
                               Path descDest,
                               String[] args) throws IOException {
        ClassList classes = new ClassList();
        Map<String, ModuleDescription> modules = new HashMap<>();

        for (VersionDescription desc : versions) {
            Iterable<byte[]> classFileData = loadClassData(desc.classes);

            loadVersionClasses(classes, modules, classFileData, excludesIncludes, desc.version, null);
        }

        List<PlatformInput> platforms =
                versions.stream()
                        .map(desc -> new PlatformInput(desc.version,
                                                       desc.primaryBaseline,
                                                       null))
                        .collect(Collectors.toList());

        dumpDescriptions(classes, modules, platforms, Set.of(), descDest.resolve("symbols"), args);
    }
    //where:
        private static final String DO_NO_MODIFY =
            "#\n" +
            "# Copyright (c) {YEAR}, Oracle and/or its affiliates. All rights reserved.\n" +
            "# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.\n" +
            "#\n" +
            "# This code is free software; you can redistribute it and/or modify it\n" +
            "# under the terms of the GNU General Public License version 2 only, as\n" +
            "# published by the Free Software Foundation.  Oracle designates this\n" +
            "# particular file as subject to the \"Classpath\" exception as provided\n" +
            "# by Oracle in the LICENSE file that accompanied this code.\n" +
            "#\n" +
            "# This code is distributed in the hope that it will be useful, but WITHOUT\n" +
            "# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or\n" +
            "# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License\n" +
            "# version 2 for more details (a copy is included in the LICENSE file that\n" +
            "# accompanied this code).\n" +
            "#\n" +
            "# You should have received a copy of the GNU General Public License version\n" +
            "# 2 along with this work; if not, write to the Free Software Foundation,\n" +
            "# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.\n" +
            "#\n" +
            "# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA\n" +
            "# or visit www.oracle.com if you need additional information or have any\n" +
            "# questions.\n" +
            "#\n" +
            "# ##########################################################\n" +
            "# ### THIS FILE IS AUTOMATICALLY GENERATED. DO NOT EDIT. ###\n" +
            "# ##########################################################\n" +
            "#\n";

        private Iterable<byte[]> loadClassData(String path) {
            List<byte[]> classFileData = new ArrayList<>();

            try (BufferedReader descIn =
                    Files.newBufferedReader(Paths.get(path))) {
                String line;
                while ((line = descIn.readLine()) != null) {
                    ByteArrayOutputStream data = new ByteArrayOutputStream();
                    for (int i = 0; i < line.length(); i += 2) {
                        String hex = line.substring(i, i + 2);
                        data.write(Integer.parseInt(hex, 16));
                    }
                    classFileData.add(data.toByteArray());
                }
            } catch (IOException ex) {
                throw new IllegalStateException(ex);
            }

            return classFileData;
        }

    private void loadVersionClasses(ClassList classes,
                                    Map<String, ModuleDescription> modules,
                                    Iterable<byte[]> classData,
                                    ExcludeIncludeList excludesIncludes,
                                    String version,
                                    String baseline) {
        Map<String, ModuleDescription> currentVersionModules =
                new HashMap<>();

        for (byte[] classFileData : classData) {
            try (InputStream in = new ByteArrayInputStream(classFileData)) {
                inspectModuleInfoClassFile(in,
                                           currentVersionModules, version);
            } catch (IOException | ConstantPoolException ex) {
                throw new IllegalStateException(ex);
            }
        }

        ExcludeIncludeList currentEIList = excludesIncludes;

        if (!currentVersionModules.isEmpty()) {
            Set<String> includes = new HashSet<>();

            for (ModuleDescription md : currentVersionModules.values()) {
                md.header.get(0).exports.stream().map(e -> e + '/')
                                        .forEach(includes::add);
            }

            currentEIList = new ExcludeIncludeList(includes,
                                                   Collections.emptySet());
        }

        ClassList currentVersionClasses = new ClassList();

        for (byte[] classFileData : classData) {
            try (InputStream in = new ByteArrayInputStream(classFileData)) {
                inspectClassFile(in, currentVersionClasses,
                                 currentEIList, version);
            } catch (IOException | ConstantPoolException ex) {
                throw new IllegalStateException(ex);
            }
        }

        ModuleDescription unsupported =
                currentVersionModules.get("jdk.unsupported");

        if (unsupported != null) {
            for (ClassDescription cd : currentVersionClasses.classes) {
                if (unsupported.header
                               .get(0)
                               .exports
                               .contains(cd.packge().replace('.', '/'))) {
                    ClassHeaderDescription ch = cd.header.get(0);
                    if (ch.classAnnotations == null) {
                        ch.classAnnotations = new ArrayList<>();
                    }
                    AnnotationDescription ad;
                    ad = new AnnotationDescription(PROPERITARY_ANNOTATION,
                                                   Collections.emptyMap());
                    ch.classAnnotations.add(ad);
                }
            }
        }

        Set<String> includedClasses = new HashSet<>();
        boolean modified;

        do {
            modified = false;

            for (ClassDescription clazz : currentVersionClasses) {
                ClassHeaderDescription header = clazz.header.get(0);

                if (includeEffectiveAccess(currentVersionClasses, clazz)) {
                    modified |= include(includedClasses, currentVersionClasses, clazz.name);
                }

                if (includedClasses.contains(clazz.name)) {
                    modified |= include(includedClasses, currentVersionClasses, header.extendsAttr);
                    for (String i : header.implementsAttr) {
                        modified |= include(includedClasses, currentVersionClasses, i);
                    }

                    modified |= includeOutputType(Collections.singleton(header),
                                                  h -> "",
                                                  includedClasses,
                                                  currentVersionClasses);
                    modified |= includeOutputType(clazz.fields,
                                                  f -> f.descriptor,
                                                  includedClasses,
                                                  currentVersionClasses);
                    modified |= includeOutputType(clazz.methods,
                                                  m -> m.descriptor,
                                                  includedClasses,
                                                  currentVersionClasses);
                }
            }
        } while (modified);

        for (ClassDescription clazz : currentVersionClasses) {
            if (!includedClasses.contains(clazz.name)) {
                continue;
            }

            ClassHeaderDescription header = clazz.header.get(0);

            if (header.innerClasses != null) {
                Iterator<InnerClassInfo> innerClassIt = header.innerClasses.iterator();

                while(innerClassIt.hasNext()) {
                    InnerClassInfo ici = innerClassIt.next();
                    if (!includedClasses.contains(ici.innerClass))
                        innerClassIt.remove();
                }
            }

            ClassDescription existing = classes.find(clazz.name, true);

            if (existing != null) {
                addClassHeader(existing, header, version, baseline);
                for (MethodDescription currentMethod : clazz.methods) {
                    addMethod(existing, currentMethod, version, baseline);
                }
                for (FieldDescription currentField : clazz.fields) {
                    addField(existing, currentField, version, baseline);
                }
            } else {
                classes.add(clazz);
            }
        }

        for (ModuleDescription module : currentVersionModules.values()) {
            ModuleHeaderDescription header = module.header.get(0);

            if (header.innerClasses != null) {
                Iterator<InnerClassInfo> innerClassIt =
                        header.innerClasses.iterator();

                while(innerClassIt.hasNext()) {
                    InnerClassInfo ici = innerClassIt.next();
                    if (!includedClasses.contains(ici.innerClass))
                        innerClassIt.remove();
                }
            }

            ModuleDescription existing = modules.get(module.name);

            if (existing != null) {
                addModuleHeader(existing, header, version);
            } else {
                modules.put(module.name, module);
            }
        }
    }
    //where:
        private static final String PROPERITARY_ANNOTATION =
                "Lsun/Proprietary+Annotation;";

    private void dumpDescriptions(ClassList classes,
                                  Map<String, ModuleDescription> modules,
                                  List<PlatformInput> versions,
                                  Set<String> forceWriteVersions,
                                  Path ctDescriptionFile,
                                  String[] args) throws IOException {
        classes.sort();

        Map<String, String> package2Modules = new HashMap<>();

        for (ModuleDescription md : modules.values()) {
            md.header
              .stream()
              .filter(h -> h.versions.contains("9"))
              .flatMap(h -> h.exports.stream())
              .map(p -> p.replace('/', '.'))
              .forEach(p -> package2Modules.put(p, md.name));
        }

        package2Modules.put("java.awt.dnd.peer", "java.desktop");
        package2Modules.put("java.awt.peer", "java.desktop");
        package2Modules.put("jdk", "java.base");

        Map<String, List<ClassDescription>> module2Classes = new HashMap<>();

        for (ClassDescription clazz : classes) {
            String pack = clazz.packge();
            String module = package2Modules.get(pack);

            if (module == null) {
                module = "java.base";

                OUTER: while (!pack.isEmpty()) {
                    for (Entry<String, String> p2M : package2Modules.entrySet()) {
                        if (p2M.getKey().startsWith(pack)) {
                            module = p2M.getValue();
                            break OUTER;
                        }
                    }
                    int dot = pack.lastIndexOf('.');
                    if (dot == (-1))
                        break;
                    pack = pack.substring(0, dot);
                }
            }
            module2Classes.computeIfAbsent(module, m -> new ArrayList<>())
                    .add(clazz);
        }

        modules.keySet()
               .stream()
               .filter(m -> !module2Classes.containsKey(m))
               .forEach(m -> module2Classes.put(m, Collections.emptyList()));

        Files.createDirectories(ctDescriptionFile.getParent());

        int year = Calendar.getInstance(TimeZone.getTimeZone("UTF"), Locale.ROOT)
                           .get(Calendar.YEAR);

        try (Writer symbolsOut = Files.newBufferedWriter(ctDescriptionFile)) {
            Map<PlatformInput, List<String>> outputFiles = new LinkedHashMap<>();

            for (PlatformInput desc : versions) {
                List<String> files = desc.files;

                if (files == null || forceWriteVersions.contains(desc.version)) {
                    files = new ArrayList<>();
                    for (Entry<String, List<ClassDescription>> e : module2Classes.entrySet()) {
                        StringWriter data = new StringWriter();
                        ModuleDescription module = modules.get(e.getKey());

                        module.write(data, desc.basePlatform, desc.version);

                        for (ClassDescription clazz : e.getValue()) {
                            clazz.write(data, desc.basePlatform, desc.version);
                        }

                        String fileName = e.getKey() + "-" + desc.version + ".sym.txt";
                        Path f = ctDescriptionFile.getParent().resolve(fileName);

                        String dataString = data.toString();

                        if (!dataString.isEmpty()) {
                            String existingYear = null;
                            boolean hasChange = true;
                            if (Files.isReadable(f)) {
                                String oldContent = new String(Files.readAllBytes(f), StandardCharsets.UTF_8);
                                int yearPos = DO_NO_MODIFY.indexOf("{YEAR}");
                                String headerPattern =
                                        Pattern.quote(DO_NO_MODIFY.substring(0, yearPos)) +
                                        "([0-9]+)(, [0-9]+)?" +
                                        Pattern.quote(DO_NO_MODIFY.substring(yearPos + "{YEAR}".length()));
                                String pattern = headerPattern +
                                                 Pattern.quote(dataString);
                                Matcher m = Pattern.compile(pattern, Pattern.MULTILINE).matcher(oldContent);
                                if (m.matches()) {
                                    hasChange = false;
                                } else {
                                    m = Pattern.compile(headerPattern).matcher(oldContent);
                                    if (m.find()) {
                                        existingYear = m.group(1);
                                    }
                                }
                            }
                            if (hasChange) {
                                try (Writer out = Files.newBufferedWriter(f, StandardCharsets.UTF_8)) {
                                    String currentYear = String.valueOf(year);
                                    String yearSpec = (existingYear != null && !currentYear.equals(existingYear) ? existingYear + ", " : "") + currentYear;
                                    out.append(DO_NO_MODIFY.replace("{YEAR}", yearSpec));
                                    out.write(dataString);
                                }
                            }
                            files.add(f.getFileName().toString());
                        }
                    }
                }

                outputFiles.put(desc, files);
            }
            symbolsOut.append(DO_NO_MODIFY.replace("{YEAR}", "2015, " + year));
            symbolsOut.append("#command used to generate this file:\n");
            symbolsOut.append("#")
                      .append(CreateSymbols.class.getName())
                      .append(" ")
                      .append(Arrays.stream(args)
                                    .collect(Collectors.joining(" ")))
                      .append("\n");
            symbolsOut.append("#\n");
            symbolsOut.append("generate platforms ")
                      .append(versions.stream()
                                      .map(v -> v.version)
                                      .sorted()
                                      .collect(Collectors.joining(":")))
                      .append("\n");
            for (Entry<PlatformInput, List<String>> versionFileEntry : outputFiles.entrySet()) {
                symbolsOut.append("platform version ")
                          .append(versionFileEntry.getKey().version);
                if (versionFileEntry.getKey().basePlatform != null) {
                    symbolsOut.append(" base ")
                              .append(versionFileEntry.getKey().basePlatform);
                }
                symbolsOut.append(" files ")
                          .append(versionFileEntry.getValue()
                                                  .stream()
                                                  .map(p -> p)
                                                  .sorted()
                                                  .collect(Collectors.joining(":")))
                          .append("\n");
            }
        }
    }

    private void incrementalUpdate(String ctDescriptionFile,
                                   String excludeFile,
                                   String platformVersion,
                                   Iterable<byte[]> classBytes,
                                   Function<LoadDescriptions, String> baseline,
                                   String[] args) throws IOException {
        String currentVersion =
                Integer.toString(Integer.parseInt(platformVersion), Character.MAX_RADIX);
        String version = currentVersion.toUpperCase(Locale.ROOT);
        Path ctDescriptionPath = Paths.get(ctDescriptionFile).toAbsolutePath();
        LoadDescriptions data = load(ctDescriptionPath);

        ClassList classes = data.classes;
        Map<String, ModuleDescription> modules = data.modules;
        List<PlatformInput> versions = data.versions;

        ExcludeIncludeList excludeList =
                ExcludeIncludeList.create(excludeFile);
        loadVersionClasses(classes, modules, classBytes, excludeList, "$", version);

        removeVersion(data, version);

        for (ModuleDescription md : data.modules.values()) {
            for (ModuleHeaderDescription header : md.header) {
                header.versions = header.versions.replace("$", version);
            }
        }

        for (ClassDescription clazzDesc : data.classes) {
            for (ClassHeaderDescription header : clazzDesc.header) {
                header.versions = header.versions.replace("$", version);
            }
            for (MethodDescription method : clazzDesc.methods) {
                method.versions = method.versions.replace("$", version);
            }
            for (FieldDescription field : clazzDesc.fields) {
                field.versions = field.versions.replace("$", version);
            }
        }

        if (versions.stream().noneMatch(inp -> version.equals(inp.version))) {
            versions.add(new PlatformInput(version, baseline.apply(data), null));
        }

        Set<String> writeVersions = new HashSet<>();

        writeVersions.add(version);

        //re-write all platforms that have version as their basline:
        versions.stream()
                .filter(inp -> version.equals(inp.basePlatform))
                .map(inp -> inp.version)
                .forEach(writeVersions::add);
        dumpDescriptions(classes, modules, versions, writeVersions, ctDescriptionPath, args);
    }

    public void createIncrementalBaseLineFromDataFile(String ctDescriptionFile,
                                                      String excludeFile,
                                                      String version,
                                                      String dataFile,
                                                      String baseline,
                                                      String[] args) throws IOException {
        incrementalUpdate(ctDescriptionFile, excludeFile, version, loadClassData(dataFile), x -> baseline, args);
    }

    public void createIncrementalBaseLine(String ctDescriptionFile,
                                          String excludeFile,
                                          String[] args) throws IOException {
        String specVersion = System.getProperty("java.specification.version");
        Iterable<byte[]> classBytes = dumpCurrentClasses();
        Function<LoadDescriptions, String> baseline = data -> {
            if (data.versions.isEmpty()) {
                return null;
            } else {
                return data.versions.stream()
                                    .sorted((v1, v2) -> v2.version.compareTo(v1.version))
                                    .findFirst()
                                    .get()
                                    .version;
            }
        };
        incrementalUpdate(ctDescriptionFile, excludeFile, specVersion, classBytes, baseline, args);
    }

    private List<byte[]> dumpCurrentClasses() throws IOException {
        JavacTool tool = JavacTool.create();
        Context ctx = new Context();
        String version = System.getProperty("java.specification.version");
        JavacTask task = tool.getTask(null, null, null,
                                      List.of("--release", version),
                                      null, null, ctx);
        task.getElements().getTypeElement("java.lang.Object");
        JavaFileManager fm = ctx.get(JavaFileManager.class);

        List<byte[]> data = new ArrayList<>();
        for (Location modLoc : LOCATIONS) {
            for (Set<JavaFileManager.Location> module :
                    fm.listLocationsForModules(modLoc)) {
                for (JavaFileManager.Location loc : module) {
                    Iterable<JavaFileObject> files =
                            fm.list(loc,
                                    "",
                                    EnumSet.of(Kind.CLASS),
                                    true);

                    for (JavaFileObject jfo : files) {
                        try (InputStream is = jfo.openInputStream();
                             InputStream in =
                                     new BufferedInputStream(is)) {
                            ByteArrayOutputStream baos =
                                    new ByteArrayOutputStream();

                            in.transferTo(baos);
                            data.add(baos.toByteArray());
                        }
                    }
                }
            }
        }

        return data;
    }
    //where:
        private static final List<StandardLocation> LOCATIONS =
                List.of(StandardLocation.SYSTEM_MODULES,
                        StandardLocation.UPGRADE_MODULE_PATH);

    //<editor-fold defaultstate="collapsed" desc="Class Reading">
    //non-final for tests:
    public static String PROFILE_ANNOTATION = "Ljdk/Profile+Annotation;";
    public static boolean ALLOW_NON_EXISTING_CLASSES = false;

    private void inspectClassFile(InputStream in, ClassList classes, ExcludeIncludeList excludesIncludes, String version) throws IOException, ConstantPoolException {
        ClassFile cf = ClassFile.read(in);

        if (cf.access_flags.is(AccessFlags.ACC_MODULE)) {
            return ;
        }

        if (!excludesIncludes.accepts(cf.getName())) {
            return ;
        }

        ClassHeaderDescription headerDesc = new ClassHeaderDescription();

        headerDesc.flags = cf.access_flags.flags;

        if (cf.super_class != 0) {
            headerDesc.extendsAttr = cf.getSuperclassName();
        }
        List<String> interfaces = new ArrayList<>();
        for (int i = 0; i < cf.interfaces.length; i++) {
            interfaces.add(cf.getInterfaceName(i));
        }
        headerDesc.implementsAttr = interfaces;
        for (Attribute attr : cf.attributes) {
            if (!readAttribute(cf, headerDesc, attr))
                return ;
        }

        ClassDescription clazzDesc = null;

        for (ClassDescription cd : classes) {
            if (cd.name.equals(cf.getName())) {
                clazzDesc = cd;
                break;
            }
        }

        if (clazzDesc == null) {
            clazzDesc = new ClassDescription();
            clazzDesc.name = cf.getName();
            classes.add(clazzDesc);
        }

        addClassHeader(clazzDesc, headerDesc, version, null);

        for (Method m : cf.methods) {
            if (!include(m.access_flags.flags))
                continue;
            MethodDescription methDesc = new MethodDescription();
            methDesc.flags = m.access_flags.flags;
            methDesc.name = m.getName(cf.constant_pool);
            methDesc.descriptor = m.descriptor.getValue(cf.constant_pool);
            for (Attribute attr : m.attributes) {
                readAttribute(cf, methDesc, attr);
            }
            addMethod(clazzDesc, methDesc, version, null);
        }
        for (Field f : cf.fields) {
            if (!include(f.access_flags.flags))
                continue;
            FieldDescription fieldDesc = new FieldDescription();
            fieldDesc.flags = f.access_flags.flags;
            fieldDesc.name = f.getName(cf.constant_pool);
            fieldDesc.descriptor = f.descriptor.getValue(cf.constant_pool);
            for (Attribute attr : f.attributes) {
                readAttribute(cf, fieldDesc, attr);
            }
            addField(clazzDesc, fieldDesc, version, null);
        }
    }

    private void inspectModuleInfoClassFile(InputStream in,
            Map<String, ModuleDescription> modules,
            String version) throws IOException, ConstantPoolException {
        ClassFile cf = ClassFile.read(in);

        if (!cf.access_flags.is(AccessFlags.ACC_MODULE)) {
            return ;
        }

        ModuleHeaderDescription headerDesc = new ModuleHeaderDescription();

        headerDesc.versions = version;
        headerDesc.flags = cf.access_flags.flags;

        for (Attribute attr : cf.attributes) {
            if (!readAttribute(cf, headerDesc, attr))
                return ;
        }

        String name = headerDesc.name;

        ModuleDescription moduleDesc = modules.get(name);

        if (moduleDesc == null) {
            moduleDesc = new ModuleDescription();
            moduleDesc.name = name;
            modules.put(moduleDesc.name, moduleDesc);
        }

        addModuleHeader(moduleDesc, headerDesc, version);
    }

    private void addModuleHeader(ModuleDescription moduleDesc,
                                 ModuleHeaderDescription headerDesc,
                                 String version) {
        //normalize:
        boolean existed = false;
        for (ModuleHeaderDescription existing : moduleDesc.header) {
            if (existing.equals(headerDesc)) {
                headerDesc = existing;
                existed = true;
            }
        }

        headerDesc.versions += version;

        if (!existed) {
            moduleDesc.header.add(headerDesc);
        }
    }

    private boolean include(int accessFlags) {
        return (accessFlags & (AccessFlags.ACC_PUBLIC | AccessFlags.ACC_PROTECTED)) != 0;
    }

    private void addClassHeader(ClassDescription clazzDesc, ClassHeaderDescription headerDesc, String version, String baseline) {
        //normalize:
        boolean existed = false;
        for (ClassHeaderDescription existing : clazzDesc.header) {
            if (existing.equals(headerDesc) && (!existed || (baseline != null && existing.versions.contains(baseline)))) {
                headerDesc = existing;
                existed = true;
            } else {
                //check if the only difference between the 7 and 8 version is the Profile annotation
                //if so, copy it to the pre-8 version, so save space
                List<AnnotationDescription> annots = existing.classAnnotations;

                if (annots != null) {
                    for (AnnotationDescription ad : annots) {
                        if (PROFILE_ANNOTATION.equals(ad.annotationType)) {
                            existing.classAnnotations = new ArrayList<>(annots);
                            existing.classAnnotations.remove(ad);
                            if (existing.equals(headerDesc)) {
                                headerDesc = existing;
                                existed = true;
                            }
                            existing.classAnnotations = annots;
                            break;
                        }
                    }
                }
            }
        }

        headerDesc.versions += version;

        if (!existed) {
            clazzDesc.header.add(headerDesc);
        }
    }

    private void addMethod(ClassDescription clazzDesc, MethodDescription methDesc, String version, String baseline) {
        //normalize:
        boolean methodExisted = false;
        for (MethodDescription existing : clazzDesc.methods) {
            if (existing.equals(methDesc) && (!methodExisted || (baseline != null && existing.versions.contains(baseline)))) {
                methodExisted = true;
                methDesc = existing;
            }
        }
        methDesc.versions += version;
        if (!methodExisted) {
            clazzDesc.methods.add(methDesc);
        }
    }

    private void addField(ClassDescription clazzDesc, FieldDescription fieldDesc, String version, String baseline) {
        boolean fieldExisted = false;
        for (FieldDescription existing : clazzDesc.fields) {
            if (existing.equals(fieldDesc) && (!fieldExisted || (baseline != null && existing.versions.contains(baseline)))) {
                fieldExisted = true;
                fieldDesc = existing;
            }
        }
        fieldDesc.versions += version;
        if (!fieldExisted) {
            clazzDesc.fields.add(fieldDesc);
        }
    }

    private boolean readAttribute(ClassFile cf, FeatureDescription feature, Attribute attr) throws ConstantPoolException {
        String attrName = attr.getName(cf.constant_pool);
        switch (attrName) {
            case Attribute.AnnotationDefault:
                assert feature instanceof MethodDescription;
                element_value defaultValue = ((AnnotationDefault_attribute) attr).default_value;
                ((MethodDescription) feature).annotationDefaultValue =
                        convertElementValue(cf.constant_pool, defaultValue);
                break;
            case "Deprecated":
                feature.deprecated = true;
                break;
            case "Exceptions":
                assert feature instanceof MethodDescription;
                List<String> thrownTypes = new ArrayList<>();
                Exceptions_attribute exceptionAttr = (Exceptions_attribute) attr;
                for (int i = 0; i < exceptionAttr.exception_index_table.length; i++) {
                    thrownTypes.add(exceptionAttr.getException(i, cf.constant_pool));
                }
                ((MethodDescription) feature).thrownTypes = thrownTypes;
                break;
            case Attribute.InnerClasses:
                if (feature instanceof ModuleHeaderDescription)
                    break; //XXX
                assert feature instanceof ClassHeaderDescription;
                List<InnerClassInfo> innerClasses = new ArrayList<>();
                InnerClasses_attribute innerClassesAttr = (InnerClasses_attribute) attr;
                for (int i = 0; i < innerClassesAttr.number_of_classes; i++) {
                    CONSTANT_Class_info outerClassInfo =
                            innerClassesAttr.classes[i].getOuterClassInfo(cf.constant_pool);
                    InnerClassInfo info = new InnerClassInfo();
                    CONSTANT_Class_info innerClassInfo =
                            innerClassesAttr.classes[i].getInnerClassInfo(cf.constant_pool);
                    info.innerClass = innerClassInfo != null ? innerClassInfo.getName() : null;
                    info.outerClass = outerClassInfo != null ? outerClassInfo.getName() : null;
                    info.innerClassName = innerClassesAttr.classes[i].getInnerName(cf.constant_pool);
                    info.innerClassFlags = innerClassesAttr.classes[i].inner_class_access_flags.flags;
                    innerClasses.add(info);
                }
                ((ClassHeaderDescription) feature).innerClasses = innerClasses;
                break;
            case "RuntimeInvisibleAnnotations":
                feature.classAnnotations = annotations2Description(cf.constant_pool, attr);
                break;
            case "RuntimeVisibleAnnotations":
                feature.runtimeAnnotations = annotations2Description(cf.constant_pool, attr);
                break;
            case "Signature":
                feature.signature = ((Signature_attribute) attr).getSignature(cf.constant_pool);
                break;
            case "ConstantValue":
                assert feature instanceof FieldDescription;
                Object value = convertConstantValue(cf.constant_pool.get(((ConstantValue_attribute) attr).constantvalue_index), ((FieldDescription) feature).descriptor);
                if (((FieldDescription) feature).descriptor.equals("C")) {
                    value = (char) (int) value;
                }
                ((FieldDescription) feature).constantValue = value;
                break;
            case "SourceFile":
                //ignore, not needed
                break;
            case "BootstrapMethods":
                //ignore, not needed
                break;
            case "Code":
                //ignore, not needed
                break;
            case "EnclosingMethod":
                return false;
            case "Synthetic":
                break;
            case "RuntimeVisibleParameterAnnotations":
                assert feature instanceof MethodDescription;
                ((MethodDescription) feature).runtimeParameterAnnotations =
                        parameterAnnotations2Description(cf.constant_pool, attr);
                break;
            case "RuntimeInvisibleParameterAnnotations":
                assert feature instanceof MethodDescription;
                ((MethodDescription) feature).classParameterAnnotations =
                        parameterAnnotations2Description(cf.constant_pool, attr);
                break;
            case Attribute.Module: {
                assert feature instanceof ModuleHeaderDescription;
                ModuleHeaderDescription header =
                        (ModuleHeaderDescription) feature;
                Module_attribute mod = (Module_attribute) attr;

                header.name = cf.constant_pool
                                .getModuleInfo(mod.module_name)
                                .getName();

                header.exports =
                        Arrays.stream(mod.exports)
                              .filter(ee -> ee.exports_to_count == 0)
                              .map(ee -> getPackageName(cf, ee.exports_index))
                              .collect(Collectors.toList());
                header.requires =
                        Arrays.stream(mod.requires)
                              .map(r -> RequiresDescription.create(cf, r))
                              .collect(Collectors.toList());
                header.uses = Arrays.stream(mod.uses_index)
                                    .mapToObj(use -> getClassName(cf, use))
                                    .collect(Collectors.toList());
                header.provides =
                        Arrays.stream(mod.provides)
                              .map(p -> ProvidesDescription.create(cf, p))
                              .collect(Collectors.toList());
                break;
            }
            case Attribute.ModuleTarget: {
                assert feature instanceof ModuleHeaderDescription;
                ModuleHeaderDescription header =
                        (ModuleHeaderDescription) feature;
                ModuleTarget_attribute mod = (ModuleTarget_attribute) attr;
                if (mod.target_platform_index != 0) {
                    header.moduleTarget =
                            cf.constant_pool
                              .getUTF8Value(mod.target_platform_index);
                }
                break;
            }
            case Attribute.ModuleResolution: {
                assert feature instanceof ModuleHeaderDescription;
                ModuleHeaderDescription header =
                        (ModuleHeaderDescription) feature;
                ModuleResolution_attribute mod =
                        (ModuleResolution_attribute) attr;
                header.moduleResolution = mod.resolution_flags;
                break;
            }
            case Attribute.ModulePackages:
            case Attribute.ModuleHashes:
                break;
            default:
                throw new IllegalStateException("Unhandled attribute: " +
                                                attrName);
        }

        return true;
    }

    private static String getClassName(ClassFile cf, int idx) {
        try {
            return cf.constant_pool.getClassInfo(idx).getName();
        } catch (InvalidIndex ex) {
            throw new IllegalStateException(ex);
        } catch (ConstantPool.UnexpectedEntry ex) {
            throw new IllegalStateException(ex);
        } catch (ConstantPoolException ex) {
            throw new IllegalStateException(ex);
        }
    }

    private static String getPackageName(ClassFile cf, int idx) {
        try {
            return cf.constant_pool.getPackageInfo(idx).getName();
        } catch (InvalidIndex ex) {
            throw new IllegalStateException(ex);
        } catch (ConstantPool.UnexpectedEntry ex) {
            throw new IllegalStateException(ex);
        } catch (ConstantPoolException ex) {
            throw new IllegalStateException(ex);
        }
    }

    private static String getModuleName(ClassFile cf, int idx) {
        try {
            return cf.constant_pool.getModuleInfo(idx).getName();
        } catch (InvalidIndex ex) {
            throw new IllegalStateException(ex);
        } catch (ConstantPool.UnexpectedEntry ex) {
            throw new IllegalStateException(ex);
        } catch (ConstantPoolException ex) {
            throw new IllegalStateException(ex);
        }
    }

    private static Integer getVersion(ClassFile cf, int idx) {
        if (idx == 0)
            return null;
        try {
            return ((CONSTANT_Integer_info) cf.constant_pool.get(idx)).value;
        } catch (InvalidIndex ex) {
            throw new IllegalStateException(ex);
        }
    }

    Object convertConstantValue(CPInfo info, String descriptor) throws ConstantPoolException {
        if (info instanceof CONSTANT_Integer_info) {
            if ("Z".equals(descriptor))
                return ((CONSTANT_Integer_info) info).value == 1;
            else
                return ((CONSTANT_Integer_info) info).value;
        } else if (info instanceof CONSTANT_Long_info) {
            return ((CONSTANT_Long_info) info).value;
        } else if (info instanceof CONSTANT_Float_info) {
            return ((CONSTANT_Float_info) info).value;
        } else if (info instanceof CONSTANT_Double_info) {
            return ((CONSTANT_Double_info) info).value;
        } else if (info instanceof CONSTANT_String_info) {
            return ((CONSTANT_String_info) info).getString();
        }
        throw new IllegalStateException(info.getClass().getName());
    }

    Object convertElementValue(ConstantPool cp, element_value val) throws InvalidIndex, ConstantPoolException {
        switch (val.tag) {
            case 'Z':
                return ((CONSTANT_Integer_info) cp.get(((Primitive_element_value) val).const_value_index)).value != 0;
            case 'B':
                return (byte) ((CONSTANT_Integer_info) cp.get(((Primitive_element_value) val).const_value_index)).value;
            case 'C':
                return (char) ((CONSTANT_Integer_info) cp.get(((Primitive_element_value) val).const_value_index)).value;
            case 'S':
                return (short) ((CONSTANT_Integer_info) cp.get(((Primitive_element_value) val).const_value_index)).value;
            case 'I':
                return ((CONSTANT_Integer_info) cp.get(((Primitive_element_value) val).const_value_index)).value;
            case 'J':
                return ((CONSTANT_Long_info) cp.get(((Primitive_element_value) val).const_value_index)).value;
            case 'F':
                return ((CONSTANT_Float_info) cp.get(((Primitive_element_value) val).const_value_index)).value;
            case 'D':
                return ((CONSTANT_Double_info) cp.get(((Primitive_element_value) val).const_value_index)).value;
            case 's':
                return ((CONSTANT_Utf8_info) cp.get(((Primitive_element_value) val).const_value_index)).value;

            case 'e':
                return new EnumConstant(cp.getUTF8Value(((Enum_element_value) val).type_name_index),
                        cp.getUTF8Value(((Enum_element_value) val).const_name_index));
            case 'c':
                return new ClassConstant(cp.getUTF8Value(((Class_element_value) val).class_info_index));

            case '@':
                return annotation2Description(cp, ((Annotation_element_value) val).annotation_value);

            case '[':
                List<Object> values = new ArrayList<>();
                for (element_value elem : ((Array_element_value) val).values) {
                    values.add(convertElementValue(cp, elem));
                }
                return values;
            default:
                throw new IllegalStateException("Currently unhandled tag: " + val.tag);
        }
    }

    private List<AnnotationDescription> annotations2Description(ConstantPool cp, Attribute attr) throws ConstantPoolException {
        RuntimeAnnotations_attribute annotationsAttr = (RuntimeAnnotations_attribute) attr;
        List<AnnotationDescription> descs = new ArrayList<>();
        for (Annotation a : annotationsAttr.annotations) {
            descs.add(annotation2Description(cp, a));
        }
        return descs;
    }

    private List<List<AnnotationDescription>> parameterAnnotations2Description(ConstantPool cp, Attribute attr) throws ConstantPoolException {
        RuntimeParameterAnnotations_attribute annotationsAttr =
                (RuntimeParameterAnnotations_attribute) attr;
        List<List<AnnotationDescription>> descs = new ArrayList<>();
        for (Annotation[] attrAnnos : annotationsAttr.parameter_annotations) {
            List<AnnotationDescription> paramDescs = new ArrayList<>();
            for (Annotation ann : attrAnnos) {
                paramDescs.add(annotation2Description(cp, ann));
            }
            descs.add(paramDescs);
        }
        return descs;
    }

    private AnnotationDescription annotation2Description(ConstantPool cp, Annotation a) throws ConstantPoolException {
        String annotationType = cp.getUTF8Value(a.type_index);
        Map<String, Object> values = new HashMap<>();

        for (element_value_pair e : a.element_value_pairs) {
            values.put(cp.getUTF8Value(e.element_name_index), convertElementValue(cp, e.value));
        }

        return new AnnotationDescription(annotationType, values);
    }
    //</editor-fold>

    protected boolean includeEffectiveAccess(ClassList classes, ClassDescription clazz) {
        if (!include(clazz.header.get(0).flags))
            return false;
        for (ClassDescription outer : classes.enclosingClasses(clazz)) {
            if (!include(outer.header.get(0).flags))
                return false;
        }
        return true;
    }

    boolean include(Set<String> includedClasses, ClassList classes, String clazzName) {
        if (clazzName == null)
            return false;

        boolean modified = includedClasses.add(clazzName);

        for (ClassDescription outer : classes.enclosingClasses(classes.find(clazzName, true))) {
            modified |= includedClasses.add(outer.name);
        }

        return modified;
    }

    <T extends FeatureDescription> boolean includeOutputType(Iterable<T> features,
                                                             Function<T, String> feature2Descriptor,
                                                             Set<String> includedClasses,
                                                             ClassList classes) {
        boolean modified = false;

        for (T feature : features) {
            CharSequence sig =
                    feature.signature != null ? feature.signature : feature2Descriptor.apply(feature);
            Matcher m = OUTPUT_TYPE_PATTERN.matcher(sig);
            while (m.find()) {
                modified |= include(includedClasses, classes, m.group(1));
            }
        }

        return modified;
    }

    static final Pattern OUTPUT_TYPE_PATTERN = Pattern.compile("L([^;<]+)(;|<)");

    public static class VersionDescription {
        public final String classes;
        public final String version;
        public final String primaryBaseline;

        public VersionDescription(String classes, String version, String primaryBaseline) {
            this.classes = classes;
            this.version = version;
            this.primaryBaseline = "<none>".equals(primaryBaseline) ? null : primaryBaseline;
        }

    }

    public static class ExcludeIncludeList {
        public final Set<String> includeList;
        public final Set<String> excludeList;

        protected ExcludeIncludeList(Set<String> includeList, Set<String> excludeList) {
            this.includeList = includeList;
            this.excludeList = excludeList;
        }

        public static ExcludeIncludeList create(String files) throws IOException {
            Set<String> includeList = new HashSet<>();
            Set<String> excludeList = new HashSet<>();
            for (String file : files.split(File.pathSeparator)) {
                try (Stream<String> lines = Files.lines(Paths.get(file))) {
                    lines.map(l -> l.substring(0, l.indexOf('#') != (-1) ? l.indexOf('#') : l.length()))
                         .filter(l -> !l.trim().isEmpty())
                         .forEach(l -> {
                             Set<String> target = l.startsWith("+") ? includeList : excludeList;
                             target.add(l.substring(1));
                         });
                }
            }
            return new ExcludeIncludeList(includeList, excludeList);
        }

        public boolean accepts(String className) {
            return matches(includeList, className) && !matches(excludeList, className);
        }

        private static boolean matches(Set<String> list, String className) {
            if (list.contains(className))
                return true;
            String pack = className.substring(0, className.lastIndexOf('/') + 1);
            return list.contains(pack);
        }
    }
    //</editor-fold>

    //<editor-fold defaultstate="collapsed" desc="Class Data Structures">
    static boolean checkChange(String versions, String version,
                               String baselineVersion) {
        return versions.contains(version) ^
               (baselineVersion != null &&
                versions.contains(baselineVersion));
    }

    static abstract class FeatureDescription {
        int flagsNormalization = ~0;
        int flags;
        boolean deprecated;
        String signature;
        String versions = "";
        List<AnnotationDescription> classAnnotations;
        List<AnnotationDescription> runtimeAnnotations;

        protected void writeAttributes(Appendable output) throws IOException {
            if (flags != 0)
                output.append(" flags " + Integer.toHexString(flags));
            if (deprecated) {
                output.append(" deprecated true");
            }
            if (signature != null) {
                output.append(" signature " + quote(signature, false));
            }
            if (classAnnotations != null && !classAnnotations.isEmpty()) {
                output.append(" classAnnotations ");
                for (AnnotationDescription a : classAnnotations) {
                    output.append(quote(a.toString(), false));
                }
            }
            if (runtimeAnnotations != null && !runtimeAnnotations.isEmpty()) {
                output.append(" runtimeAnnotations ");
                for (AnnotationDescription a : runtimeAnnotations) {
                    output.append(quote(a.toString(), false));
                }
            }
        }

        protected boolean shouldIgnore(String baselineVersion, String version) {
            return (!versions.contains(version) &&
                    (baselineVersion == null || !versions.contains(baselineVersion))) ||
                   (baselineVersion != null &&
                    versions.contains(baselineVersion) && versions.contains(version));
        }

        public abstract void write(Appendable output, String baselineVersion, String version) throws IOException;

        protected void readAttributes(LineBasedReader reader) {
            String inFlags = reader.attributes.get("flags");
            if (inFlags != null && !inFlags.isEmpty()) {
                flags = Integer.parseInt(inFlags, 16);
            }
            String inDeprecated = reader.attributes.get("deprecated");
            if ("true".equals(inDeprecated)) {
                deprecated = true;
            }
            signature = reader.attributes.get("signature");
            String inClassAnnotations = reader.attributes.get("classAnnotations");
            if (inClassAnnotations != null) {
                classAnnotations = parseAnnotations(inClassAnnotations, new int[1]);
            }
            String inRuntimeAnnotations = reader.attributes.get("runtimeAnnotations");
            if (inRuntimeAnnotations != null) {
                runtimeAnnotations = parseAnnotations(inRuntimeAnnotations, new int[1]);
            }
        }

        public abstract boolean read(LineBasedReader reader) throws IOException;

        @Override
        public int hashCode() {
            int hash = 3;
            hash = 89 * hash + (this.flags & flagsNormalization);
            hash = 89 * hash + (this.deprecated ? 1 : 0);
            hash = 89 * hash + Objects.hashCode(this.signature);
            hash = 89 * hash + listHashCode(this.classAnnotations);
            hash = 89 * hash + listHashCode(this.runtimeAnnotations);
            return hash;
        }

        @Override
        public boolean equals(Object obj) {
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            final FeatureDescription other = (FeatureDescription) obj;
            if ((this.flags & flagsNormalization) != (other.flags & flagsNormalization)) {
                return false;
            }
            if (this.deprecated != other.deprecated) {
                return false;
            }
            if (!Objects.equals(this.signature, other.signature)) {
                return false;
            }
            if (!listEquals(this.classAnnotations, other.classAnnotations)) {
                return false;
            }
            if (!listEquals(this.runtimeAnnotations, other.runtimeAnnotations)) {
                return false;
            }
            return true;
        }

    }

    public static class ModuleDescription {
        String name;
        List<ModuleHeaderDescription> header = new ArrayList<>();

        public void write(Appendable output, String baselineVersion,
                          String version) throws IOException {
            boolean inBaseline = false;
            boolean inVersion = false;
            for (ModuleHeaderDescription mhd : header) {
                if (baselineVersion != null &&
                    mhd.versions.contains(baselineVersion)) {
                    inBaseline = true;
                }
                if (mhd.versions.contains(version)) {
                    inVersion = true;
                }
            }
            if (!inVersion && !inBaseline)
                return ;
            if (!inVersion) {
                output.append("-module name " + name + "\n\n");
                return;
            }
            boolean hasChange = hasChange(header, version, baselineVersion);
            if (!hasChange)
                return;

            output.append("module name " + name + "\n");
            for (ModuleHeaderDescription header : header) {
                header.write(output, baselineVersion, version);
            }
            output.append("\n");
        }

        boolean hasChange(List<? extends FeatureDescription> hasChange,
                          String version, String baseline) {
            return hasChange.stream()
                            .map(fd -> fd.versions)
                            .anyMatch(versions -> checkChange(versions,
                                                              version,
                                                              baseline));
        }

        public void read(LineBasedReader reader, String baselineVersion,
                         String version) throws IOException {
            if (!"module".equals(reader.lineKey))
                return ;

            name = reader.attributes.get("name");

            reader.moveNext();

            OUTER: while (reader.hasNext()) {
                switch (reader.lineKey) {
                    case "header":
                        removeVersion(header, h -> true, version);
                        ModuleHeaderDescription mhd =
                                new ModuleHeaderDescription();
                        mhd.read(reader);
                        mhd.name = name;
                        mhd.versions = version;
                        header.add(mhd);
                        break;
                    case "class":
                    case "-class":
                    case "module":
                    case "-module":
                        break OUTER;
                    default:
                        throw new IllegalStateException(reader.lineKey);
                }
            }
        }
    }

    static class ModuleHeaderDescription extends HeaderDescription {
        String name;
        List<String> exports = new ArrayList<>();
        List<String> opens = new ArrayList<>();
        List<RequiresDescription> requires = new ArrayList<>();
        List<String> uses = new ArrayList<>();
        List<ProvidesDescription> provides = new ArrayList<>();
        Integer moduleResolution;
        String moduleTarget;

        @Override
        public int hashCode() {
            int hash = super.hashCode();
            hash = 83 * hash + Objects.hashCode(this.name);
            hash = 83 * hash + Objects.hashCode(this.exports);
            hash = 83 * hash + Objects.hashCode(this.opens);
            hash = 83 * hash + Objects.hashCode(this.requires);
            hash = 83 * hash + Objects.hashCode(this.uses);
            hash = 83 * hash + Objects.hashCode(this.provides);
            hash = 83 * hash + Objects.hashCode(this.moduleResolution);
            hash = 83 * hash + Objects.hashCode(this.moduleTarget);
            return hash;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (!super.equals(obj)) {
                return false;
            }
            final ModuleHeaderDescription other =
                    (ModuleHeaderDescription) obj;
            if (!Objects.equals(this.name, other.name)) {
                return false;
            }
            if (!listEquals(this.exports, other.exports)) {
                return false;
            }
            if (!listEquals(this.opens, other.opens)) {
                return false;
            }
            if (!listEquals(this.requires, other.requires)) {
                return false;
            }
            if (!listEquals(this.uses, other.uses)) {
                return false;
            }
            if (!listEquals(this.provides, other.provides)) {
                return false;
            }
            if (!Objects.equals(this.moduleTarget, other.moduleTarget)) {
                return false;
            }
            if (!Objects.equals(this.moduleResolution,
                                other.moduleResolution)) {
                return false;
            }
            return true;
        }

        @Override
        public void write(Appendable output, String baselineVersion,
                          String version) throws IOException {
            if (!versions.contains(version) ||
                (baselineVersion != null && versions.contains(baselineVersion)
                 && versions.contains(version)))
                return ;
            output.append("header");
            if (exports != null && !exports.isEmpty())
                output.append(" exports " + serializeList(exports));
            if (opens != null && !opens.isEmpty())
                output.append(" opens " + serializeList(opens));
            if (requires != null && !requires.isEmpty()) {
                List<String> requiresList =
                        requires.stream()
                                .map(req -> req.serialize())
                                .collect(Collectors.toList());
                output.append(" requires " + serializeList(requiresList));
            }
            if (uses != null && !uses.isEmpty())
                output.append(" uses " + serializeList(uses));
            if (provides != null && !provides.isEmpty()) {
                List<String> providesList =
                        provides.stream()
                                .map(p -> p.serialize())
                                .collect(Collectors.toList());
                output.append(" provides " + serializeList(providesList));
            }
            if (moduleTarget != null)
                output.append(" target " + quote(moduleTarget, true));
            if (moduleResolution != null)
                output.append(" resolution " +
                              quote(Integer.toHexString(moduleResolution),
                                    true));
            writeAttributes(output);
            output.append("\n");
            writeInnerClasses(output, baselineVersion, version);
        }

        private static Map<String, String> splitAttributes(String data) {
            String[] parts = data.split(" ");

            Map<String, String> attributes = new HashMap<>();

            for (int i = 0; i < parts.length; i += 2) {
                attributes.put(parts[i], unquote(parts[i + 1]));
            }

            return attributes;
        }

        @Override
        public boolean read(LineBasedReader reader) throws IOException {
            if (!"header".equals(reader.lineKey))
                return false;

            exports = deserializeList(reader.attributes.get("exports"));
            opens = deserializeList(reader.attributes.get("opens"));
            List<String> requiresList =
                    deserializeList(reader.attributes.get("requires"));
            requires = requiresList.stream()
                                   .map(RequiresDescription::deserialize)
                                   .collect(Collectors.toList());
            uses = deserializeList(reader.attributes.get("uses"));
            List<String> providesList =
                    deserializeList(reader.attributes.get("provides"), false);
            provides = providesList.stream()
                                   .map(ProvidesDescription::deserialize)
                                   .collect(Collectors.toList());

            moduleTarget = reader.attributes.get("target");

            if (reader.attributes.containsKey("resolution")) {
                final String resolutionFlags =
                        reader.attributes.get("resolution");
                moduleResolution = Integer.parseInt(resolutionFlags, 16);
            }

            readAttributes(reader);
            reader.moveNext();
            readInnerClasses(reader);

            return true;
        }

        static class RequiresDescription {
            final String moduleName;
            final int flags;
            final Integer version;

            public RequiresDescription(String moduleName, int flags,
                                       Integer version) {
                this.moduleName = moduleName;
                this.flags = flags;
                this.version = version;
            }

            public String serialize() {
                String versionKeyValue = version != null
                        ? " version " + quote(String.valueOf(version), true)
                        : "";
                return "name " + quote(moduleName, true) +
                       " flags " + quote(Integer.toHexString(flags), true) +
                       versionKeyValue;
            }

            public static RequiresDescription deserialize(String data) {
                Map<String, String> attributes = splitAttributes(data);

                Integer ver = attributes.containsKey("version")
                        ? Integer.parseInt(attributes.get("version"))
                        : null;
                int flags = Integer.parseInt(attributes.get("flags"), 16);
                return new RequiresDescription(attributes.get("name"),
                                               flags,
                                               ver);
            }

            public static RequiresDescription create(ClassFile cf,
                                                     RequiresEntry req) {
                String mod = getModuleName(cf, req.requires_index);
                Integer ver = getVersion(cf, req.requires_version_index);
                return new RequiresDescription(mod,
                                               req.requires_flags,
                                               ver);
            }
        }

        static class ProvidesDescription {
            final String interfaceName;
            final List<String> implNames;

            public ProvidesDescription(String interfaceName,
                                       List<String> implNames) {
                this.interfaceName = interfaceName;
                this.implNames = implNames;
            }

            public String serialize() {
                return "interface " + quote(interfaceName, true) +
                       " impls " + quote(serializeList(implNames), true, true);
            }

            public static ProvidesDescription deserialize(String data) {
                Map<String, String> attributes = splitAttributes(data);
                List<String> implsList =
                        deserializeList(attributes.get("impls"),
                                        false);
                return new ProvidesDescription(attributes.get("interface"),
                                               implsList);
            }

            public static ProvidesDescription create(ClassFile cf,
                                                     ProvidesEntry prov) {
                String api = getClassName(cf, prov.provides_index);
                List<String> impls =
                        Arrays.stream(prov.with_index)
                              .mapToObj(wi -> getClassName(cf, wi))
                              .collect(Collectors.toList());
                return new ProvidesDescription(api, impls);
            }
        }
    }

    public static class ClassDescription {
        String name;
        List<ClassHeaderDescription> header = new ArrayList<>();
        List<MethodDescription> methods = new ArrayList<>();
        List<FieldDescription> fields = new ArrayList<>();

        public void write(Appendable output, String baselineVersion,
                          String version) throws IOException {
            boolean inBaseline = false;
            boolean inVersion = false;
            for (ClassHeaderDescription chd : header) {
                if (baselineVersion != null &&
                    chd.versions.contains(baselineVersion)) {
                    inBaseline = true;
                }
                if (chd.versions.contains(version)) {
                    inVersion = true;
                }
            }
            if (!inVersion && !inBaseline)
                return ;
            if (!inVersion) {
                output.append("-class name " + name + "\n\n");
                return;
            }
            boolean hasChange = hasChange(header, version, baselineVersion) ||
                                hasChange(fields, version, baselineVersion) ||
                                hasChange(methods, version, baselineVersion);
            if (!hasChange)
                return;

            output.append("class name " + name + "\n");
            for (ClassHeaderDescription header : header) {
                header.write(output, baselineVersion, version);
            }
            for (FieldDescription field : fields) {
                field.write(output, baselineVersion, version);
            }
            for (MethodDescription method : methods) {
                method.write(output, baselineVersion, version);
            }
            output.append("\n");
        }

        boolean hasChange(List<? extends FeatureDescription> hasChange,
                          String version,
                          String baseline) {
            return hasChange.stream()
                            .map(fd -> fd.versions)
                            .anyMatch(versions -> checkChange(versions,
                                                              version,
                                                              baseline));
        }

        public void read(LineBasedReader reader, String baselineVersion,
                         String version) throws IOException {
            if (!"class".equals(reader.lineKey))
                return ;

            name = reader.attributes.get("name");

            reader.moveNext();

            OUTER: while (reader.hasNext()) {
                switch (reader.lineKey) {
                    case "header":
                        removeVersion(header, h -> true, version);
                        ClassHeaderDescription chd = new ClassHeaderDescription();
                        chd.read(reader);
                        chd.versions = version;
                        header.add(chd);
                        break;
                    case "field":
                        FieldDescription field = new FieldDescription();
                        field.read(reader);
                        field.versions += version;
                        fields.add(field);
                        break;
                    case "-field": {
                        removeVersion(fields,
                                      f -> Objects.equals(f.name, reader.attributes.get("name")) &&
                                           Objects.equals(f.descriptor, reader.attributes.get("descriptor")),
                                      version);
                        reader.moveNext();
                        break;
                    }
                    case "method":
                        MethodDescription method = new MethodDescription();
                        method.read(reader);
                        method.versions += version;
                        methods.add(method);
                        break;
                    case "-method": {
                        removeVersion(methods,
                                      m -> Objects.equals(m.name, reader.attributes.get("name")) &&
                                           Objects.equals(m.descriptor, reader.attributes.get("descriptor")),
                                      version);
                        reader.moveNext();
                        break;
                    }
                    case "class":
                    case "-class":
                    case "module":
                    case "-module":
                        break OUTER;
                    default:
                        throw new IllegalStateException(reader.lineKey);
                }
            }
        }

        public String packge() {
            String pack;
            int lastSlash = name.lastIndexOf('/');
            if (lastSlash != (-1)) {
                pack = name.substring(0, lastSlash).replace('/', '.');
            } else {
                pack = "";
            }

            return pack;
        }
    }

    static class ClassHeaderDescription extends HeaderDescription {
        String extendsAttr;
        List<String> implementsAttr;

        @Override
        public int hashCode() {
            int hash = super.hashCode();
            hash = 17 * hash + Objects.hashCode(this.extendsAttr);
            hash = 17 * hash + Objects.hashCode(this.implementsAttr);
            return hash;
        }

        @Override
        public boolean equals(Object obj) {
            if (obj == null) {
                return false;
            }
            if (!super.equals(obj)) {
                return false;
            }
            final ClassHeaderDescription other = (ClassHeaderDescription) obj;
            if (!Objects.equals(this.extendsAttr, other.extendsAttr)) {
                return false;
            }
            if (!Objects.equals(this.implementsAttr, other.implementsAttr)) {
                return false;
            }
            return true;
        }

        @Override
        public void write(Appendable output, String baselineVersion, String version) throws IOException {
            if (!versions.contains(version) ||
                (baselineVersion != null && versions.contains(baselineVersion) && versions.contains(version)))
                return ;
            output.append("header");
            if (extendsAttr != null)
                output.append(" extends " + extendsAttr);
            if (implementsAttr != null && !implementsAttr.isEmpty())
                output.append(" implements " + serializeList(implementsAttr));
            writeAttributes(output);
            output.append("\n");
            writeInnerClasses(output, baselineVersion, version);
        }

        @Override
        public boolean read(LineBasedReader reader) throws IOException {
            if (!"header".equals(reader.lineKey))
                return false;

            extendsAttr = reader.attributes.get("extends");
            String elementsList = reader.attributes.get("implements");
            implementsAttr = deserializeList(elementsList);

            readAttributes(reader);
            reader.moveNext();
            readInnerClasses(reader);

            return true;
        }

    }

    static abstract class HeaderDescription extends FeatureDescription {
        List<InnerClassInfo> innerClasses;

        @Override
        public int hashCode() {
            int hash = super.hashCode();
            hash = 19 * hash + Objects.hashCode(this.innerClasses);
            return hash;
        }

        @Override
        public boolean equals(Object obj) {
            if (obj == null) {
                return false;
            }
            if (!super.equals(obj)) {
                return false;
            }
            final HeaderDescription other = (HeaderDescription) obj;
            if (!listEquals(this.innerClasses, other.innerClasses)) {
                return false;
            }
            return true;
        }

        protected void writeInnerClasses(Appendable output,
                                         String baselineVersion,
                                         String version) throws IOException {
            if (innerClasses != null && !innerClasses.isEmpty()) {
                for (InnerClassInfo ici : innerClasses) {
                    output.append("innerclass");
                    output.append(" innerClass " + ici.innerClass);
                    output.append(" outerClass " + ici.outerClass);
                    output.append(" innerClassName " + ici.innerClassName);
                    output.append(" flags " + Integer.toHexString(ici.innerClassFlags));
                    output.append("\n");
                }
            }
        }

        protected void readInnerClasses(LineBasedReader reader) throws IOException {
            innerClasses = new ArrayList<>();

            while ("innerclass".equals(reader.lineKey)) {
                InnerClassInfo info = new InnerClassInfo();

                info.innerClass = reader.attributes.get("innerClass");
                info.outerClass = reader.attributes.get("outerClass");
                info.innerClassName = reader.attributes.get("innerClassName");

                String inFlags = reader.attributes.get("flags");
                if (inFlags != null && !inFlags.isEmpty())
                    info.innerClassFlags = Integer.parseInt(inFlags, 16);

                innerClasses.add(info);

                reader.moveNext();
            }
        }

    }

    static class MethodDescription extends FeatureDescription {
        static int METHODS_FLAGS_NORMALIZATION = ~0;
        String name;
        String descriptor;
        List<String> thrownTypes;
        Object annotationDefaultValue;
        List<List<AnnotationDescription>> classParameterAnnotations;
        List<List<AnnotationDescription>> runtimeParameterAnnotations;

        public MethodDescription() {
            flagsNormalization = METHODS_FLAGS_NORMALIZATION;
        }

        @Override
        public int hashCode() {
            int hash = super.hashCode();
            hash = 59 * hash + Objects.hashCode(this.name);
            hash = 59 * hash + Objects.hashCode(this.descriptor);
            hash = 59 * hash + Objects.hashCode(this.thrownTypes);
            hash = 59 * hash + Objects.hashCode(this.annotationDefaultValue);
            return hash;
        }

        @Override
        public boolean equals(Object obj) {
            if (obj == null) {
                return false;
            }
            if (!super.equals(obj)) {
                return false;
            }
            final MethodDescription other = (MethodDescription) obj;
            if (!Objects.equals(this.name, other.name)) {
                return false;
            }
            if (!Objects.equals(this.descriptor, other.descriptor)) {
                return false;
            }
            if (!Objects.equals(this.thrownTypes, other.thrownTypes)) {
                return false;
            }
            if (!Objects.equals(this.annotationDefaultValue, other.annotationDefaultValue)) {
                return false;
            }
            return true;
        }

        @Override
        public void write(Appendable output, String baselineVersion, String version) throws IOException {
            if (shouldIgnore(baselineVersion, version))
                return ;
            if (!versions.contains(version)) {
                output.append("-method");
                output.append(" name " + quote(name, false));
                output.append(" descriptor " + quote(descriptor, false));
                output.append("\n");
                return ;
            }
            output.append("method");
            output.append(" name " + quote(name, false));
            output.append(" descriptor " + quote(descriptor, false));
            if (thrownTypes != null)
                output.append(" thrownTypes " + serializeList(thrownTypes));
            if (annotationDefaultValue != null)
                output.append(" annotationDefaultValue " + quote(AnnotationDescription.dumpAnnotationValue(annotationDefaultValue), false));
            writeAttributes(output);
            if (classParameterAnnotations != null && !classParameterAnnotations.isEmpty()) {
                output.append(" classParameterAnnotations ");
                for (List<AnnotationDescription> pa : classParameterAnnotations) {
                    for (AnnotationDescription a : pa) {
                        output.append(quote(a.toString(), false));
                    }
                    output.append(";");
                }
            }
            if (runtimeParameterAnnotations != null && !runtimeParameterAnnotations.isEmpty()) {
                output.append(" runtimeParameterAnnotations ");
                for (List<AnnotationDescription> pa : runtimeParameterAnnotations) {
                    for (AnnotationDescription a : pa) {
                        output.append(quote(a.toString(), false));
                    }
                    output.append(";");
                }
            }
            output.append("\n");
        }

        @Override
        public boolean read(LineBasedReader reader) throws IOException {
            if (!"method".equals(reader.lineKey))
                return false;

            name = reader.attributes.get("name");
            descriptor = reader.attributes.get("descriptor");

            String thrownTypesValue = reader.attributes.get("thrownTypes");

            if (thrownTypesValue != null) {
                thrownTypes = deserializeList(thrownTypesValue);
            }

            String inAnnotationDefaultValue = reader.attributes.get("annotationDefaultValue");

            if (inAnnotationDefaultValue != null) {
                annotationDefaultValue = parseAnnotationValue(inAnnotationDefaultValue, new int[1]);
            }

            readAttributes(reader);

            String inClassParamAnnotations = reader.attributes.get("classParameterAnnotations");
            if (inClassParamAnnotations != null) {
                List<List<AnnotationDescription>> annos = new ArrayList<>();
                int[] pointer = new int[1];
                do {
                    annos.add(parseAnnotations(inClassParamAnnotations, pointer));
                    assert pointer[0] == inClassParamAnnotations.length() || inClassParamAnnotations.charAt(pointer[0]) == ';';
                } while (++pointer[0] < inClassParamAnnotations.length());
                classParameterAnnotations = annos;
            }

            String inRuntimeParamAnnotations = reader.attributes.get("runtimeParameterAnnotations");
            if (inRuntimeParamAnnotations != null) {
                List<List<AnnotationDescription>> annos = new ArrayList<>();
                int[] pointer = new int[1];
                do {
                    annos.add(parseAnnotations(inRuntimeParamAnnotations, pointer));
                    assert pointer[0] == inRuntimeParamAnnotations.length() || inRuntimeParamAnnotations.charAt(pointer[0]) == ';';
                } while (++pointer[0] < inRuntimeParamAnnotations.length());
                runtimeParameterAnnotations = annos;
            }

            reader.moveNext();

            return true;
        }

    }

    static class FieldDescription extends FeatureDescription {
        String name;
        String descriptor;
        Object constantValue;

        @Override
        public int hashCode() {
            int hash = super.hashCode();
            hash = 59 * hash + Objects.hashCode(this.name);
            hash = 59 * hash + Objects.hashCode(this.descriptor);
            hash = 59 * hash + Objects.hashCode(this.constantValue);
            return hash;
        }

        @Override
        public boolean equals(Object obj) {
            if (obj == null) {
                return false;
            }
            if (!super.equals(obj)) {
                return false;
            }
            final FieldDescription other = (FieldDescription) obj;
            if (!Objects.equals(this.name, other.name)) {
                return false;
            }
            if (!Objects.equals(this.descriptor, other.descriptor)) {
                return false;
            }
            if (!Objects.equals(this.constantValue, other.constantValue)) {
                return false;
            }
            return true;
        }

        @Override
        public void write(Appendable output, String baselineVersion, String version) throws IOException {
            if (shouldIgnore(baselineVersion, version))
                return ;
            if (!versions.contains(version)) {
                output.append("-field");
                output.append(" name " + quote(name, false));
                output.append(" descriptor " + quote(descriptor, false));
                output.append("\n");
                return ;
            }
            output.append("field");
            output.append(" name " + name);
            output.append(" descriptor " + descriptor);
            if (constantValue != null) {
                output.append(" constantValue " + quote(constantValue.toString(), false));
            }
            writeAttributes(output);
            output.append("\n");
        }

        @Override
        public boolean read(LineBasedReader reader) throws IOException {
            if (!"field".equals(reader.lineKey))
                return false;

            name = reader.attributes.get("name");
            descriptor = reader.attributes.get("descriptor");

            String inConstantValue = reader.attributes.get("constantValue");

            if (inConstantValue != null) {
                switch (descriptor) {
                    case "Z": constantValue = "true".equals(inConstantValue); break;
                    case "B": constantValue = Integer.parseInt(inConstantValue); break;
                    case "C": constantValue = inConstantValue.charAt(0); break;
                    case "S": constantValue = Integer.parseInt(inConstantValue); break;
                    case "I": constantValue = Integer.parseInt(inConstantValue); break;
                    case "J": constantValue = Long.parseLong(inConstantValue); break;
                    case "F": constantValue = Float.parseFloat(inConstantValue); break;
                    case "D": constantValue = Double.parseDouble(inConstantValue); break;
                    case "Ljava/lang/String;": constantValue = inConstantValue; break;
                    default:
                        throw new IllegalStateException("Unrecognized field type: " + descriptor);
                }
            }

            readAttributes(reader);

            reader.moveNext();

            return true;
        }

    }

    static final class AnnotationDescription {
        String annotationType;
        Map<String, Object> values;

        public AnnotationDescription(String annotationType, Map<String, Object> values) {
            this.annotationType = annotationType;
            this.values = values;
        }

        @Override
        public int hashCode() {
            int hash = 7;
            hash = 47 * hash + Objects.hashCode(this.annotationType);
            hash = 47 * hash + Objects.hashCode(this.values);
            return hash;
        }

        @Override
        public boolean equals(Object obj) {
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            final AnnotationDescription other = (AnnotationDescription) obj;
            if (!Objects.equals(this.annotationType, other.annotationType)) {
                return false;
            }
            if (!Objects.equals(this.values, other.values)) {
                return false;
            }
            return true;
        }

        @Override
        public String toString() {
            StringBuilder result = new StringBuilder();
            result.append("@" + annotationType);
            if (!values.isEmpty()) {
                result.append("(");
                boolean first = true;
                for (Entry<String, Object> e : values.entrySet()) {
                    if (!first) {
                        result.append(",");
                    }
                    first = false;
                    result.append(e.getKey());
                    result.append("=");
                    result.append(dumpAnnotationValue(e.getValue()));
                    result.append("");
                }
                result.append(")");
            }
            return result.toString();
        }

        private static String dumpAnnotationValue(Object value) {
            if (value instanceof List) {
                StringBuilder result = new StringBuilder();

                result.append("{");

                for (Object element : ((List) value)) {
                    result.append(dumpAnnotationValue(element));
                }

                result.append("}");

                return result.toString();
            }

            if (value instanceof String) {
                return "\"" + quote((String) value, true) + "\"";
            } else if (value instanceof Boolean) {
                return "Z" + value;
            } else if (value instanceof Byte) {
                return "B" + value;
            } if (value instanceof Character) {
                return "C" + value;
            } if (value instanceof Short) {
                return "S" + value;
            } if (value instanceof Integer) {
                return "I" + value;
            } if (value instanceof Long) {
                return "J" + value;
            } if (value instanceof Float) {
                return "F" + value;
            } if (value instanceof Double) {
                return "D" + value;
            } else {
                return value.toString();
            }
        }
    }

    static final class EnumConstant {
        String type;
        String constant;

        public EnumConstant(String type, String constant) {
            this.type = type;
            this.constant = constant;
        }

        @Override
        public String toString() {
            return "e" + type + constant + ";";
        }

        @Override
        public int hashCode() {
            int hash = 7;
            hash = 19 * hash + Objects.hashCode(this.type);
            hash = 19 * hash + Objects.hashCode(this.constant);
            return hash;
        }

        @Override
        public boolean equals(Object obj) {
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            final EnumConstant other = (EnumConstant) obj;
            if (!Objects.equals(this.type, other.type)) {
                return false;
            }
            if (!Objects.equals(this.constant, other.constant)) {
                return false;
            }
            return true;
        }

    }

    static final class ClassConstant {
        String type;

        public ClassConstant(String type) {
            this.type = type;
        }

        @Override
        public String toString() {
            return "c" + type;
        }

        @Override
        public int hashCode() {
            int hash = 3;
            hash = 53 * hash + Objects.hashCode(this.type);
            return hash;
        }

        @Override
        public boolean equals(Object obj) {
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            final ClassConstant other = (ClassConstant) obj;
            if (!Objects.equals(this.type, other.type)) {
                return false;
            }
            return true;
        }

    }

    static final class InnerClassInfo {
        String innerClass;
        String outerClass;
        String innerClassName;
        int    innerClassFlags;

        @Override
        public int hashCode() {
            int hash = 3;
            hash = 11 * hash + Objects.hashCode(this.innerClass);
            hash = 11 * hash + Objects.hashCode(this.outerClass);
            hash = 11 * hash + Objects.hashCode(this.innerClassName);
            hash = 11 * hash + Objects.hashCode(this.innerClassFlags);
            return hash;
        }

        @Override
        public boolean equals(Object obj) {
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            final InnerClassInfo other = (InnerClassInfo) obj;
            if (!Objects.equals(this.innerClass, other.innerClass)) {
                return false;
            }
            if (!Objects.equals(this.outerClass, other.outerClass)) {
                return false;
            }
            if (!Objects.equals(this.innerClassName, other.innerClassName)) {
                return false;
            }
            if (!Objects.equals(this.innerClassFlags, other.innerClassFlags)) {
                return false;
            }
            return true;
        }

    }

    public static final class ClassList implements Iterable<ClassDescription> {
        private final List<ClassDescription> classes = new ArrayList<>();
        private final Map<String, ClassDescription> name2Class = new HashMap<>();
        private final Map<ClassDescription, ClassDescription> inner2Outter = new HashMap<>();

        @Override
        public Iterator<ClassDescription> iterator() {
            return classes.iterator();
        }

        public void add(ClassDescription desc) {
            classes.add(desc);
            name2Class.put(desc.name, desc);
        }

        public ClassDescription find(String name) {
            return find(name, ALLOW_NON_EXISTING_CLASSES);
        }

        public ClassDescription find(String name, boolean allowNull) {
            ClassDescription desc = name2Class.get(name);

            if (desc != null || allowNull)
                return desc;

            throw new IllegalStateException("Cannot find: " + name);
        }

        private static final ClassDescription NONE = new ClassDescription();

        public ClassDescription enclosingClass(ClassDescription clazz) {
            if (clazz == null)
                return null;
            ClassDescription desc = inner2Outter.computeIfAbsent(clazz, c -> {
                ClassHeaderDescription header = clazz.header.get(0);

                if (header.innerClasses != null) {
                    for (InnerClassInfo ici : header.innerClasses) {
                        if (ici.innerClass.equals(clazz.name)) {
                            return find(ici.outerClass);
                        }
                    }
                }

                return NONE;
            });

            return desc != NONE ? desc : null;
        }

        public Iterable<ClassDescription> enclosingClasses(ClassDescription clazz) {
            List<ClassDescription> result = new ArrayList<>();
            ClassDescription outer = enclosingClass(clazz);

            while (outer != null) {
                result.add(outer);
                outer = enclosingClass(outer);
            }

            return result;
        }

        public void sort() {
            Collections.sort(classes, (cd1, cd2) -> cd1.name.compareTo(cd2.name));
        }
    }

    private static int listHashCode(Collection<?> c) {
        return c == null || c.isEmpty() ? 0 : c.hashCode();
    }

    private static boolean listEquals(Collection<?> c1, Collection<?> c2) {
        if (c1 == c2) return true;
        if (c1 == null && c2.isEmpty()) return true;
        if (c2 == null && c1.isEmpty()) return true;
        return Objects.equals(c1, c2);
    }

    private static String serializeList(List<String> list) {
        StringBuilder result = new StringBuilder();
        String sep = "";

        for (Object o : list) {
            result.append(sep);
            result.append(o);
            sep = ",";
        }

        return quote(result.toString(), false);
    }

    private static List<String> deserializeList(String serialized) {
        return deserializeList(serialized, true);
    }

    private static List<String> deserializeList(String serialized,
                                                boolean unquote) {
        serialized = unquote ? unquote(serialized) : serialized;
        if (serialized == null)
            return new ArrayList<>();
        return new ArrayList<>(List.of(serialized.split(",")));
    }

    private static String quote(String value, boolean quoteQuotes) {
        return quote(value, quoteQuotes, false);
    }

    private static String quote(String value, boolean quoteQuotes,
                                boolean quoteCommas) {
        StringBuilder result = new StringBuilder();

        for (char c : value.toCharArray()) {
            if (c <= 32 || c >= 127 || c == '\\' ||
                (quoteQuotes && c == '"') || (quoteCommas && c == ',')) {
                result.append("\\u" + String.format("%04X", (int) c) + ";");
            } else {
                result.append(c);
            }
        }

        return result.toString();
    }

    private static final Pattern unicodePattern =
            Pattern.compile("\\\\u([0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F])");

    private static String unquote(String value) {
        if (value == null)
            return null;

        StringBuilder result = new StringBuilder();
        Matcher m = unicodePattern.matcher(value);
        int lastStart = 0;

        while (m.find(lastStart)) {
            result.append(value.substring(lastStart, m.start()));
            result.append((char) Integer.parseInt(m.group(1), 16));
            lastStart = m.end() + 1;
        }

        result.append(value.substring(lastStart, value.length()));

        return result.toString();
    }

    private static String readDigits(String value, int[] valuePointer) {
        int start = valuePointer[0];

        if (value.charAt(valuePointer[0]) == '-')
            valuePointer[0]++;

        while (valuePointer[0] < value.length() && Character.isDigit(value.charAt(valuePointer[0])))
            valuePointer[0]++;

        return value.substring(start, valuePointer[0]);
    }

    private static String className(String value, int[] valuePointer) {
        int start = valuePointer[0];
        while (value.charAt(valuePointer[0]++) != ';')
            ;
        return value.substring(start, valuePointer[0]);
    }

    private static Object parseAnnotationValue(String value, int[] valuePointer) {
        switch (value.charAt(valuePointer[0]++)) {
            case 'Z':
                if ("true".equals(value.substring(valuePointer[0], valuePointer[0] + 4))) {
                    valuePointer[0] += 4;
                    return true;
                } else if ("false".equals(value.substring(valuePointer[0], valuePointer[0] + 5))) {
                    valuePointer[0] += 5;
                    return false;
                } else {
                    throw new IllegalStateException("Unrecognized boolean structure: " + value);
                }
            case 'B': return Byte.parseByte(readDigits(value, valuePointer));
            case 'C': return value.charAt(valuePointer[0]++);
            case 'S': return Short.parseShort(readDigits(value, valuePointer));
            case 'I': return Integer.parseInt(readDigits(value, valuePointer));
            case 'J': return Long.parseLong(readDigits(value, valuePointer));
            case 'F': return Float.parseFloat(readDigits(value, valuePointer));
            case 'D': return Double.parseDouble(readDigits(value, valuePointer));
            case 'c':
                return new ClassConstant(className(value, valuePointer));
            case 'e':
                return new EnumConstant(className(value, valuePointer), className(value, valuePointer).replaceFirst(";$", ""));
            case '{':
                List<Object> elements = new ArrayList<>(); //TODO: a good test for this would be highly desirable
                while (value.charAt(valuePointer[0]) != '}') {
                    elements.add(parseAnnotationValue(value, valuePointer));
                }
                valuePointer[0]++;
                return elements;
            case '"':
                int start = valuePointer[0];
                while (value.charAt(valuePointer[0]) != '"')
                    valuePointer[0]++;
                return unquote(value.substring(start, valuePointer[0]++));
            case '@':
                return parseAnnotation(value, valuePointer);
            default:
                throw new IllegalStateException("Unrecognized signature type: " + value.charAt(valuePointer[0] - 1) + "; value=" + value);
        }
    }

    public static List<AnnotationDescription> parseAnnotations(String encoded, int[] pointer) {
        ArrayList<AnnotationDescription> result = new ArrayList<>();

        while (pointer[0] < encoded.length() && encoded.charAt(pointer[0]) == '@') {
            pointer[0]++;
            result.add(parseAnnotation(encoded, pointer));
        }

        return result;
    }

    private static AnnotationDescription parseAnnotation(String value, int[] valuePointer) {
        String className = className(value, valuePointer);
        Map<String, Object> attribute2Value = new HashMap<>();

        if (valuePointer[0] < value.length() && value.charAt(valuePointer[0]) == '(') {
            while (value.charAt(valuePointer[0]) != ')') {
                int nameStart = ++valuePointer[0];

                while (value.charAt(valuePointer[0]++) != '=');

                String name = value.substring(nameStart, valuePointer[0] - 1);

                attribute2Value.put(name, parseAnnotationValue(value, valuePointer));
            }

            valuePointer[0]++;
        }

        return new AnnotationDescription(className, attribute2Value);
    }
    //</editor-fold>

    private static void help() {
        System.err.println("Help...");
    }

    public static void main(String... args) throws IOException {
        if (args.length < 1) {
            help();
            return ;
        }

        switch (args[0]) {
            case "build-description": {
                if (args.length < 3) {
                    help();
                    return ;
                }

                Path descDest = Paths.get(args[1]);
                List<VersionDescription> versions = new ArrayList<>();

                for (int i = 3; i + 2 < args.length; i += 3) {
                    versions.add(new VersionDescription(args[i + 1], args[i], args[i + 2]));
                }

                Files.walkFileTree(descDest, new FileVisitor<Path>() {
                    @Override
                    public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
                        return FileVisitResult.CONTINUE;
                    }
                    @Override
                    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                        Files.delete(file);
                        return FileVisitResult.CONTINUE;
                    }
                    @Override
                    public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
                        return FileVisitResult.CONTINUE;
                    }
                    @Override public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
                        Files.delete(dir);
                        return FileVisitResult.CONTINUE;
                    }
                });

                ExcludeIncludeList excludeList =
                        ExcludeIncludeList.create(args[2]);

                new CreateSymbols().createBaseLine(versions,
                                                   excludeList,
                                                   descDest,
                                                   args);
                break;
            }
            case "build-description-incremental-file": {
                if (args.length != 6 && args.length != 7) {
                    help();
                    return ;
                }

                if (args.length == 7) {
                    if ("--normalize-method-flags".equals(args[6])) {
                        MethodDescription.METHODS_FLAGS_NORMALIZATION = ~(0x100 | 0x20);
                    } else {
                        help();
                        return ;
                    }
                }

                new CreateSymbols().createIncrementalBaseLineFromDataFile(args[1], args[2], args[3], args[4], "<none>".equals(args[5]) ? null : args[5], args);
                break;
            }
            case "build-description-incremental": {
                if (args.length != 3) {
                    help();
                    return ;
                }

                new CreateSymbols().createIncrementalBaseLine(args[1], args[2], args);
                break;
            }
            case "build-ctsym":
                if (args.length != 3) {
                    help();
                    return ;
                }

                new CreateSymbols().createSymbols(args[1],
                                                  args[2],
                                                  CtSymKind.JOINED_VERSIONS);
                break;
        }
    }

}
