public class ObfConfigManager {

    private static final String HEADER = "Despector decompiler configuration:";

    private static HoconConfigurationLoader loader;
    private static CommentedConfigurationNode node;
    private static ObjectMapper<ObfConfig>.BoundInstance configMapper;

    private static ObfConfig config = null;

    /**
     * Gets the global configuration object.
     */
    public static ObfConfig getConfig() {
        if (config == null) {
            config = new ObfConfig();
        }
        return config;
    }

    /**
     * Loads the given configuration file.
     */
    public static void load(Path path) {
        System.out.println("Loading config from " + path.toString());
        try {
            Files.createDirectories(path.getParent());
            if (Files.notExists(path)) {
                Files.createFile(path);
            }

            loader = HoconConfigurationLoader.builder().setPath(path).build();
            configMapper = ObjectMapper.forClass(ObfConfig.class).bindToNew();
            node = loader.load(ConfigurationOptions.defaults().setHeader(HEADER));
            config = configMapper.populate(node);
            configMapper.serialize(node);
            loader.save(node);
        } catch (Exception e) {
            System.err.println("Error loading configuration:");
            e.printStackTrace();
        }
    }

    /**
     * Saves the config back to disk to persist and changes made.
     */
    public static void update() {
        try {
            configMapper.serialize(node);
            loader.save(node);
        } catch (Exception e) {
            System.err.println("Error saving configuration:");
            e.printStackTrace();
        }
    }

} public class MappingUsageFinder implements TypeVisitor {

    private final MappingsSet set;
    private Set<String> types = new HashSet<>();
    private Set<String> methods = new HashSet<>();
    private Set<String> fields = new HashSet<>();

    public MappingUsageFinder(MappingsSet set) {
        this.set = set;
    }

    public int getSeenTypes() {
        return this.types.size();
    }

    public boolean sawType(String obf) {
        return this.types.contains(obf);
    }

    public int getSeenMethods() {
        return this.methods.size();
    }

    public int getSeenFields() {
        return this.fields.size();
    }

    @Override
    public void visitClassEntry(ClassEntry type) {
        if (this.set.mapType(type.getName()) != null) {
            this.types.add(type.getName());
        }
    }

    @Override
    public void visitEnumEntry(EnumEntry type) {
        if (this.set.mapType(type.getName()) != null) {
            this.types.add(type.getName());
        }
    }

    @Override
    public void visitInterfaceEntry(InterfaceEntry type) {
        if (this.set.mapType(type.getName()) != null) {
            this.types.add(type.getName());
        }
    }

    @Override
    public void visitAnnotationEntry(AnnotationEntry type) {
        if (this.set.mapType(type.getName()) != null) {
            this.types.add(type.getName());
        }
    }

    @Override
    public void visitAnnotation(Annotation annotation) {
    }

    @Override
    public void visitMethod(MethodEntry mth) {
        if (this.set.mapMethod(mth.getOwnerName(), mth.getName(), mth.getDescription()) != null) {
            this.methods.add(mth.getOwnerName() + mth.getName() + mth.getDescription());
        }
    }

    @Override
    public void visitMethodEnd() {
    }

    @Override
    public void visitField(FieldEntry fld) {
        if (this.set.mapField(fld.getOwnerName(), fld.getName()) != null) {
            this.fields.add(fld.getOwnerName() + fld.getName());
        }
    }

    @Override
    public void visitTypeEnd() {
    }

} public final class MappingsIO {

    private static final String PACKAGE_LEADER = "PK: ";
    private static final String CLASS_LEADER = "CL: ";
    private static final String FIELD_LEADER = "FD: ";
    private static final String METHOD_LEADER = "MD: ";

    /**
     * Loads the given srg file and returns a {@link MappingsSet} containing the
     * mappings.
     */
    public static MappingsSet load(Path deobf_data) throws IOException {
        MappingsSet mappings = new MappingsSet();
        System.out.println("Loading mappings from " + deobf_data.toString());
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(deobf_data.toFile())))) {
            String line = null;
            outer: while ((line = reader.readLine()) != null) {
                if (line.startsWith(PACKAGE_LEADER)) {
                    line = line.substring(PACKAGE_LEADER.length());
                    String[] data = line.split(" ");
                    mappings.addPackageMapping(data[0], data[1]);
                } else if (line.startsWith(CLASS_LEADER)) {
                    line = line.substring(CLASS_LEADER.length());
                    String[] data = line.split(" ");
                    for (String ex : ObfConfigManager.getConfig().excluded_packages) {
                        if (data[1].startsWith(ex)) {
                            continue outer;
                        }
                    }
                    mappings.addTypeMapping(data[0], data[1]);
                } else if (line.startsWith(FIELD_LEADER)) {
                    line = line.substring(FIELD_LEADER.length());
                    String[] data = line.split(" ");
                    for (String ex : ObfConfigManager.getConfig().excluded_packages) {
                        if (data[1].startsWith(ex)) {
                            continue outer;
                        }
                    }
                    String obf_name = data[0].substring(0, data[0].lastIndexOf('/'));
                    String obf_fld = data[0].substring(data[0].lastIndexOf('/') + 1, data[0].length());
                    String map_fld = data[1].substring(data[1].lastIndexOf('/') + 1, data[1].length());
                    mappings.addFieldMapping(obf_name, obf_fld, map_fld);
                } else if (line.startsWith(METHOD_LEADER)) {
                    line = line.substring(METHOD_LEADER.length());
                    String[] data = line.split(" ");
                    for (String ex : ObfConfigManager.getConfig().excluded_packages) {
                        if (data[1].startsWith(ex)) {
                            continue outer;
                        }
                    }
                    String obf_name = data[0].substring(0, data[0].lastIndexOf('/'));
                    String obf_mth = data[0].substring(data[0].lastIndexOf('/') + 1, data[0].length());
                    String map_mth = data[2].substring(data[2].lastIndexOf('/') + 1, data[2].length());
                    mappings.addMethodMapping(obf_name, obf_mth, data[1], map_mth);
                }
            }
            System.out.println("Loaded " + mappings.packagesCount() + " packages");
            System.out.println("Loaded " + mappings.typeCount() + " classes");
            System.out.println("Loaded " + mappings.fieldCount() + " fields");
            System.out.println("Loaded " + mappings.methodCount() + " methods");
        }
        return mappings;
    }

    /**
     * Writes the given mappings set to the given file following the srg format.
     */
    public static void write(Path out, MappingsSet mappings, int next_member) throws IOException {
        Files.createDirectories(out.getParent());
        try (FileWriter writer = new FileWriter(out.toFile())) {
            // standard packages
            writer.write("PK: . net/minecraft/src\n"
                    + "PK: net net\n"
                    + "PK: net/minecraft net/minecraft\n"
                    + "PK: net/minecraft/client net/minecraft/client\n"
                    + "PK: net/minecraft/client/main net/minecraft/client/main\n"
                    + "PK: net/minecraft/realms net/minecraft/realms\n"
                    + "PK: net/minecraft/server net/minecraft/server\n");
            List<String> cls_keys = Lists.newArrayList(mappings.getMappedTypes());
            Collections.sort(cls_keys);
            for (String key : cls_keys) {
                writer.write("CL: " + key + " " + mappings.mapType(key) + "\n");
            }
            List<String> fd_keys = Lists.newArrayList(mappings.getMappedFields());
            Collections.sort(fd_keys);
            for (String key : fd_keys) {
                String owner = key.substring(0, key.lastIndexOf('/'));
                String obf_fld = key.substring(key.lastIndexOf('/') + 1, key.length());
                String mapped = mappings.mapType(owner);
                if (mapped == null) {
                    System.err.println("Skipping field mapping " + key + " owner not mapped");
                    continue;
                }
                writer.write("FD: " + key + " " + mapped + "/" + mappings.mapField(owner, obf_fld) + "\n");
            }

            List<String> mth_keys = Lists.newArrayList(mappings.getMappedMethods());
            Collections.sort(mth_keys);
            Set<String> seen_mth = new HashSet<>();
            for (String key : mth_keys) {
                Collection<MethodMapping> overloads = mappings.getMethods(key);
                for (MethodMapping overload : overloads) {
                    if (!overload.isMapped() && overload.getObf().startsWith("<")) {
                        overload.update(overload.getObf());
                    }
                    String mapped_owner = mappings.mapType(overload.getObfOwner());
                    if (mapped_owner == null) {
                        continue;
                    }
                    String mapped_key = mapped_owner + "/" + overload.getMapped() + " " + overload.getMappedSignature();
                    if (seen_mth.contains(mapped_key)) {
                        mapped_key = mapped_owner + "/mth_" + next_member + "_" + overload.getObf() + " " + overload.getMappedSignature();
                        next_member++;
                    }
                    seen_mth.add(mapped_key);
                    writer.write("MD: " + key + " " + overload.getObfSignature() + " " + mapped_key + "\n");
                }
            }
        }
    }

    private MappingsIO() {
    }

} public class MappingsSet {

    private final BiMap<String, String> packages = HashBiMap.create();
    private final BiMap<String, String> classes = HashBiMap.create();
    private final BiMap<String, String> fields = HashBiMap.create();
    private final Multimap<String, MethodMapping> methods = HashMultimap.create();
    private final Multimap<String, MethodMapping> inverse_methods = HashMultimap.create();

    private boolean modified = false;

    public MappingsSet() {
    }

    /**
     * Gets if this mapping set has been modified since the modification flag
     * was last cleared.
     */
    public boolean isModified() {
        return this.modified;
    }

    /**
     * Clears the modification flag to mark all previous modifications as
     * handled.
     */
    public void unmarkModified() {
        this.modified = false;
    }

    /**
     * Maps an obfuscated package name to plain, returns null if no mapping is
     * found.
     */
    public String mapPackage(String pkg) {
        return this.packages.get(pkg);
    }

    /**
     * Maps an obfuscated package name to plain returning the obfuscated name if
     * no mapping was found.
     */
    public String mapPackageSafe(String pkg) {
        String mapped = this.packages.get(pkg);
        if (mapped == null) {
            return pkg;
        }
        return mapped;
    }

    /**
     * Maps a plain package name to its obfuscated counterpart, returns null if
     * no mapping is found.
     */
    public String inversePackage(String mapped) {
        return this.packages.inverse().get(mapped);
    }

    /**
     * Adds a package mapping. If a mapping with the given obfuscated name
     * already exists then the mapping will fail.
     */
    public void addPackageMapping(String obfuscated_package, String mapped_package) {
        String existing = this.packages.get(obfuscated_package);
        if (existing != null) {
            System.out.println("Updating package mapping for " + obfuscated_package + " from " + existing + " to " + mapped_package);
            return;
        }
        this.packages.put(obfuscated_package, mapped_package);
        this.modified = true;
    }

    /**
     * Gets a set of the obfuscated names of all mapped packages.
     */
    public Set<String> getMappedPackages() {
        return this.packages.keySet();
    }

    /**
     * Gets a collection of the plain names of all mapped packages.
     */
    public Collection<String> getMappedPackageNames() {
        return this.packages.values();
    }

    /**
     * Gets how many packages are mapped.
     */
    public int packagesCount() {
        return this.packages.size();
    }

    public String mapType(String obfuscated_type) {
        return this.classes.get(obfuscated_type);
    }

    /**
     * Maps the given obfuscated type name to its mapped equivalent. Returns the
     * original obfuscated name if not such mapping is found.
     */
    public String mapTypeSafe(String obfuscated_type) {
        String mapped = this.classes.get(obfuscated_type);
        if (mapped == null) {
            return obfuscated_type;
        }
        return mapped;
    }

    /**
     * Gets the obfuscated name for the given plain name, or null if not found.
     */
    public String inverseType(String mapped_type) {
        return this.classes.inverse().get(mapped_type);
    }

    /**
     * Adds the given type mapping. If a mapping for the given obfuscated name
     * already exists then this mapping is ignored.
     */
    public void addTypeMapping(String obfuscated_type, String mapped_type) {
        String existing = this.classes.get(obfuscated_type);
        if (existing != null && !existing.equals(mapped_type)) {
            System.out.println("Existing class mapping for " + obfuscated_type + " from " + existing + " to " + mapped_type);
            return;
        }
        this.classes.put(obfuscated_type, mapped_type);
        this.modified = true;
    }

    public int typeCount() {
        return this.classes.size();
    }

    public Set<String> getMappedTypes() {
        return this.classes.keySet();
    }

    public Collection<String> getMappedTypeNames() {
        return this.classes.values();
    }

    /**
     * Gets the plain name for the given obfuscated field name for the given
     * owner. The owner name shoud be the obfuscated name of the owner. Returns
     * null if no mapping is found.
     */
    public String mapField(String owner, String fld) {
        String key = owner + "/" + fld;
        return mapField(key);
    }

    /**
     * Gets the mapped name for a field key. A field key is formed by the
     * obfuscated owner name and the obfuscated field name combined together
     * separated by a '/'.
     */
    public String mapField(String key) {
        String mapped = this.fields.get(key);
        if (mapped == null) {
            return null;
        }
        String mapped_fld = mapped.substring(mapped.lastIndexOf('/') + 1, mapped.length());
        return mapped_fld;
    }

    /**
     * Gets the plain name for the given obfuscated field name for the given
     * owner. The owner name should be the obfuscated name of the owner. If no
     * mapping is found then the obfuscated name is returned.
     */
    public String mapFieldSafe(String owner, String fld) {
        String mapped = mapField(owner, fld);
        return mapped == null ? fld : mapped;
    }

    /**
     * Returns the obfuscated field name given a mapped owner name and mapped
     * field name. Returns null if no such mapping is found.
     */
    public String inverseField(String owner, String mapped) {
        String key = owner + "/" + mapped;
        String inverse = this.fields.inverse().get(key);
        if (inverse == null) {
            return null;
        }
        String obf_fld = inverse.substring(inverse.lastIndexOf('/') + 1, inverse.length());
        return obf_fld;
    }

    /**
     * Adds a new field mapping to this mapping set. The owner name should be
     * given in obfuscated form.
     * 
     * <p>A mapping for the owner must exist before registering a field mapping
     * for it. If the owner is not obfuscated then a trivial mapping should be
     * inserted as a placeholder.</p>
     */
    public void addFieldMapping(String owner, String old, String mapped) {
        String key = owner + "/" + old;
        String existing = this.fields.get(key);
        if (existing != null) {
            System.out.println("Existing field mapping for " + key + " from " + existing + " to " + mapped);
            return;
        }
        String mapped_owner = mapType(owner);
        if (mapped_owner == null) {
            throw new IllegalStateException("Tried to map field before type");
        } else if(this.fields.containsValue(mapped_owner + "/" + mapped)) {
            System.out.println("Existing field mapping for value " + mapped_owner + "/" + mapped + " from " + existing + " to " + key);
            return;
        }
        this.fields.put(key, mapped_owner + "/" + mapped);
        this.modified = true;
    }

    public int fieldCount() {
        return this.fields.size();
    }

    /**
     * Gets the <strong>keys</strong> of all mapped fields. The key is made up
     * of `owner_name/field_name`.
     */
    public Set<String> getMappedFields() {
        return this.fields.keySet();
    }

    public Collection<String> getMappedFieldNames() {
        return this.fields.values();
    }

    /**
     * Gets the method mapping for the given obfuscated method name and
     * signature. If no such method is found then null is returned.
     */
    public MethodMapping getMethodMapping(String owner, String obfuscated_method, String obfuscated_signature) {
        String key = owner + "/" + obfuscated_method;
        for (MethodMapping pos : this.methods.get(key)) {
            if (pos.obfuscated_signature.equals(obfuscated_signature)) {
                return pos;
            }
        }
        return null;
    }

    /**
     * Maps the given obfuscated method name and signature to the mapped method
     * name, if no such method exists then null is returned.
     */
    public String mapMethod(String owner, String method, String sig) {
        String key = owner + "/" + method;
        for (MethodMapping pos : this.methods.get(key)) {
            if (pos.isMapped() && pos.obfuscated_signature.equals(sig)) {
                return pos.map_method;
            }
        }
        return null;
    }

    /**
     * Maps the given obfuscated method name and signature to the mapped method
     * name, if no such method exists then the obfuscated method name is
     * returned.
     */
    public String mapMethodSafe(String owner, String method, String sig) {
        String key = owner + "/" + method;
        for (MethodMapping pos : this.methods.get(key)) {
            if (pos.isMapped() && pos.obfuscated_signature.equals(sig)) {
                return pos.map_method;
            }
        }
        return method;
    }

    public String inverseMethod(String owner, String method, String obf_desc) {
        String key = owner + "/" + method;
        for (MethodMapping pos : this.inverse_methods.get(key)) {
            if (pos.obfuscated_signature.equals(obf_desc)) {
                return pos.obf_method;
            }
        }
        return null;
    }

    /**
     * Adds a new method mapping.
     */
    public void addMethodMapping(String obfuscated_owner, String obfuscated_method, String obfuscated_signature, String mapped_method) {
        String key = obfuscated_owner + "/" + obfuscated_method;
        MethodMapping existing = null;
        for (MethodMapping pos : this.methods.get(key)) {
            if (pos.obfuscated_signature.equals(obfuscated_signature)) {
                existing = pos;
                break;
            }
        }
        if (existing == null) {
            MethodMapping mapping = new MethodMapping(this, obfuscated_owner, obfuscated_method, obfuscated_signature, mapped_method);
            this.methods.put(key, mapping);
            String mapped_owner = mapTypeSafe(obfuscated_owner);
            this.inverse_methods.put(mapped_owner + "/" + mapped_method, mapping);
            this.modified = true;
        }
    }

    public int methodCount() {
        return this.methods.values().size();
    }

    public Collection<MethodMapping> getMethods(String obfuscated_owner, String obfuscated_method) {
        return this.methods.get(obfuscated_owner + "/" + obfuscated_method);
    }

    public Collection<MethodMapping> getMethods(String key) {
        return this.methods.get(key);
    }

    /**
     * Gets the <strong>keys</strong> of all mapped fields. The key is made up
     * of `owner_name/field_name`.
     */
    public Set<String> getMappedMethods() {
        return this.methods.keySet();
    }

    /**
     * Merges this mapping set with the given mapping set. The new mappings are
     * given preference over the existing mappings and will replace them if they
     * conflict.
     */
    public void merge(MappingsSet other) {
        final int initial_size = packagesCount() + typeCount() + methodCount() + fieldCount();
        for (Map.Entry<String, String> e : other.packages.entrySet()) {
            this.packages.put(e.getKey(), e.getValue());
        }
        for (Map.Entry<String, String> e : other.classes.entrySet()) {
            this.classes.put(e.getKey(), e.getValue());
        }
        for (Map.Entry<String, String> e : other.fields.entrySet()) {
            this.fields.put(e.getKey(), e.getValue());
        }
        for (Map.Entry<String, Collection<MethodMapping>> e : other.methods.asMap().entrySet()) {
            for (MethodMapping mth : e.getValue()) {
                this.methods.put(e.getKey(), new MethodMapping(mth));
            }
        }
        int final_size = packagesCount() + typeCount() + methodCount() + fieldCount();
        if (initial_size != final_size) {
            this.modified = true;
        }
    }

    /**
     * Represents a mapping of a method as a helper for mapping signature
     * information.
     */
    public static class MethodMapping {

        private final MappingsSet set;
        final String obf_cls;
        final String obf_method;
        final String obfuscated_signature;
        String map_method;
        String map_sig;

        /**
         * Creates a new method mapping with a mapped name specified.
         */
        public MethodMapping(MappingsSet set, String c, String m, String s, String mm) {
            this.set = set;
            this.obf_cls = c;
            this.obf_method = m;
            this.obfuscated_signature = s;
            this.map_method = mm;
            updateSig();
        }

        /**
         * Creates a new method mapping without a mapped name specified.
         */
        public MethodMapping(MappingsSet set, String c, String m, String s) {
            this.set = set;
            this.obf_cls = c;
            this.obf_method = m;
            this.obfuscated_signature = s;
        }

        /**
         * Creates a method mapping as a clone of the given mapping.
         */
        public MethodMapping(MethodMapping clone) {
            this.set = clone.set;
            this.obf_cls = clone.obf_cls;
            this.obf_method = clone.obf_method;
            this.obfuscated_signature = clone.obfuscated_signature;
            this.map_method = clone.map_method;
            this.map_sig = clone.map_sig;
        }

        public String getObfOwner() {
            return this.obf_cls;
        }

        public String getObf() {
            return this.obf_method;
        }

        public String getObfSignature() {
            return this.obfuscated_signature;
        }

        public String getMapped() {
            return this.map_method;
        }

        public String getMappedSignature() {
            updateSig();
            return this.map_sig;
        }

        public boolean isMapped() {
            return this.map_method != null;
        }

        public void update(String mth) {
            this.map_method = mth;
            updateSig();
        }

        public void updateSig() {
            this.map_sig = mapSig(this.obfuscated_signature, this.set);
        }

        public static String mapDesc(String desc, MappingsSet map) {
            if (desc.startsWith("[")) {
                return "[" + mapDesc(desc.substring(1), map);
            }
            if (desc.startsWith("L")) {
                String cls = desc.substring(1, desc.length() - 1);
                return "L" + map.mapTypeSafe(cls) + ";";
            }
            return desc;
        }

        public static String mapSig(String desc, MappingsSet map) {
            String out = "(";
            String accu = "";
            for (int i = 1; i < desc.length(); i++) {
                char next = desc.charAt(i);
                if (next == ')') {
                    break;
                }
                if (next == '[') {
                    accu += next;
                } else if (next == 'L') {
                    while (next != ';') {
                        accu += next;
                        next = desc.charAt(++i);
                    }
                    accu += ';';
                    out += mapDesc(accu, map);
                    accu = "";
                } else {
                    accu += next;
                    out += mapDesc(accu, map);
                    accu = "";
                }
            }
            String ret = desc.substring(desc.indexOf(')') + 1);
            out += ")" + mapDesc(ret, map);
            return out;
        }
    }
} public static class MethodMapping {

    private final MappingsSet set;
    final String obf_cls;
    final String obf_method;
    final String obfuscated_signature;
    String map_method;
    String map_sig;

    /**
     * Creates a new method mapping with a mapped name specified.
     */
    public MethodMapping(MappingsSet set, String c, String m, String s, String mm) {
        this.set = set;
        this.obf_cls = c;
        this.obf_method = m;
        this.obfuscated_signature = s;
        this.map_method = mm;
        updateSig();
    }

    /**
     * Creates a new method mapping without a mapped name specified.
     */
    public MethodMapping(MappingsSet set, String c, String m, String s) {
        this.set = set;
        this.obf_cls = c;
        this.obf_method = m;
        this.obfuscated_signature = s;
    }

    /**
     * Creates a method mapping as a clone of the given mapping.
     */
    public MethodMapping(MethodMapping clone) {
        this.set = clone.set;
        this.obf_cls = clone.obf_cls;
        this.obf_method = clone.obf_method;
        this.obfuscated_signature = clone.obfuscated_signature;
        this.map_method = clone.map_method;
        this.map_sig = clone.map_sig;
    }

    public String getObfOwner() {
        return this.obf_cls;
    }

    public String getObf() {
        return this.obf_method;
    }

    public String getObfSignature() {
        return this.obfuscated_signature;
    }

    public String getMapped() {
        return this.map_method;
    }

    public String getMappedSignature() {
        updateSig();
        return this.map_sig;
    }

    public boolean isMapped() {
        return this.map_method != null;
    }

    public void update(String mth) {
        this.map_method = mth;
        updateSig();
    }

    public void updateSig() {
        this.map_sig = mapSig(this.obfuscated_signature, this.set);
    }

    public static String mapDesc(String desc, MappingsSet map) {
        if (desc.startsWith("[")) {
            return "[" + mapDesc(desc.substring(1), map);
        }
        if (desc.startsWith("L")) {
            String cls = desc.substring(1, desc.length() - 1);
            return "L" + map.mapTypeSafe(cls) + ";";
        }
        return desc;
    }

    public static String mapSig(String desc, MappingsSet map) {
        String out = "(";
        String accu = "";
        for (int i = 1; i < desc.length(); i++) {
            char next = desc.charAt(i);
            if (next == ')') {
                break;
            }
            if (next == '[') {
                accu += next;
            } else if (next == 'L') {
                while (next != ';') {
                    accu += next;
                    next = desc.charAt(++i);
                }
                accu += ';';
                out += mapDesc(accu, map);
                accu = "";
            } else {
                accu += next;
                out += mapDesc(accu, map);
                accu = "";
            }
        }
        String ret = desc.substring(desc.indexOf(')') + 1);
        out += ")" + mapDesc(ret, map);
        return out;
    }
} public class UnknownMemberMapper implements TypeVisitor {

    private final MappingsSet mappings;
    private final MergeEngine engine;
    private final MappingsSet previous;

    private final Map<MethodEntry, MethodEntry> synthetic_overloads = new HashMap<>();

    private Map<FieldEntry, String> enum_names = new HashMap<>();
    private EnumEntry current_type;
    private int next_type = 0;

    public UnknownMemberMapper(MappingsSet set, MergeEngine engine, MappingsSet prev) {
        this.mappings = set;
        this.engine = engine;
        this.previous = prev;
    }

    public int getNext() {
        return this.next_type;
    }

    private void findSyntheticOverloads(TypeEntry type) {
        for (MethodEntry mth : type.getMethods()) {
            if (!mth.isSynthetic() || mth.getInstructions() == null) {
                continue;
            }
            StatementBlock insns = mth.getInstructions();
            MethodEntry target = null;
            if (mth.getReturnType() == VoidTypeSignature.VOID) {
                if (insns.getStatementCount() != 2) {
                    continue;
                }
                Statement a = insns.getStatement(0);
                if (!(a instanceof InvokeStatement) || !(((InvokeStatement) a).getInstruction() instanceof InstanceMethodInvoke)) {
                    continue;
                }
                InstanceMethodInvoke invoke = (InstanceMethodInvoke) ((InvokeStatement) a).getInstruction();
                if (!invoke.getOwnerName().equals(type.getName())) {
                    continue;
                }
                target = type.getMethod(invoke.getMethodName(), invoke.getMethodDescription());
            } else {
                if (insns.getStatementCount() != 1) {
                    continue;
                }
                Statement a = insns.getStatement(0);
                if (!(a instanceof Return) || !(((Return) a).getValue().get() instanceof InstanceMethodInvoke)) {
                    continue;
                }
                InstanceMethodInvoke invoke = (InstanceMethodInvoke) ((Return) a).getValue().get();
                if (!invoke.getOwnerName().equals(type.getName())) {
                    continue;
                }
                target = type.getMethod(invoke.getMethodName(), invoke.getMethodDescription());
            }
            if (target == null) {
                continue;
            }
            this.synthetic_overloads.put(target, mth);
        }
    }

    @Override
    public void visitClassEntry(ClassEntry type) {
        findSyntheticOverloads(type);
    }

    @Override
    public void visitEnumEntry(EnumEntry type) {
        findSyntheticOverloads(type);
        this.enum_names = null;
        this.current_type = type;
    }

    @Override
    public void visitInterfaceEntry(InterfaceEntry type) {
        findSyntheticOverloads(type);
    }

    @Override
    public void visitAnnotationEntry(AnnotationEntry type) {
        findSyntheticOverloads(type);
    }

    @Override
    public void visitAnnotation(Annotation annotation) {
    }

    private String getMapped(MethodEntry mth) {
        String mapped = this.mappings.mapMethod(mth.getOwnerName(), mth.getName(), mth.getDescription());
        if (mapped != null) {
            return mapped;
        }
        MethodGroup group = this.engine.getNewMethodGroup(mth);
        for (MethodEntry g : group.getMethods()) {
            mapped = this.mappings.mapMethod(g.getOwnerName(), g.getName(), g.getDescription());
            if (mapped != null) {
                return mapped;
            }
        }
        MethodMatchEntry match = this.engine.getMethodMatchInverse(mth);
        if (match != null && match.getOldMethod() instanceof MergeEngine.DummyMethod) {
            return match.getOldMethod().getName();
        }
        MethodEntry intr = this.synthetic_overloads.get(mth);
        if (intr != null) {
            return getMapped(intr);
        }
        if (mth.getName().length() > 2) {
            return null;
        }
        if (mapped == null && this.previous != null) {
            String prev_mapped_owner = this.previous.mapType(mth.getOwnerName());
            String mapped_owner = this.mappings.mapType(mth.getOwnerName());
            if (mapped_owner.equals(prev_mapped_owner)) {
                String prev = this.previous.mapMethod(mth.getOwnerName(), mth.getName(), mth.getDescription());
                String inv = this.mappings.inverseMethod(mapped_owner, prev, mth.getDescription());
                if (prev != null && inv == null) {
                    return prev;
                }
            }
        }
        mapped = String.format("mth_%04d_%s", this.next_type++, mth.getName());
        return mapped;
    }

    @Override
    public void visitMethod(MethodEntry mth) {
        if (mth.getName().equals("<init>") || mth.getName().equals("<clinit>")) {
            return;
        }
        String mapped = getMapped(mth);
        if (mapped != null) {
            this.mappings.addMethodMapping(mth.getOwnerName(), mth.getName(), mth.getDescription(), mapped);
        }
    }

    @Override
    public void visitMethodEnd() {
    }

    @Override
    public void visitField(FieldEntry fld) {
        String mapped = this.mappings.mapField(fld.getOwnerName(), fld.getName());
        if (mapped == null) {
            FieldMatchEntry match = this.engine.getFieldMatchInverse(fld);
            if (match != null && match.getOldField() instanceof MergeEngine.DummyField) {
                mapped = match.getOldField().getName();
            } else {
                if (this.current_type != null) {
                    checkEnumConstants();
                    mapped = this.enum_names.get(fld);
                }
                if (mapped == null && this.previous != null) {
                    String prev_mapped_owner = this.previous.mapField(fld.getOwnerName());
                    String mapped_owner = this.mappings.mapType(fld.getOwnerName());
                    if (mapped_owner.equals(prev_mapped_owner)) {
                        String prev = this.previous.mapField(fld.getOwnerName(), fld.getName());
                        String inv = this.mappings.inverseField(mapped_owner, prev);
                        if (prev != null && inv == null) {
                            mapped = prev;
                        }
                    }
                }
                if (mapped == null) {
                    mapped = String.format("fld_%04d_%s", this.next_type++, fld.getName());
                }
            }
            this.mappings.addFieldMapping(fld.getOwnerName(), fld.getName(), mapped);
        }
    }

    private void checkEnumConstants() {
        if (this.enum_names == null) {
            this.enum_names = new HashMap<>();
            MethodEntry clinit = this.current_type.getStaticMethod("<clinit>");
            if(clinit == null) {
                return;
            }
            for (Statement stmt : clinit.getInstructions()) {
                if (!(stmt instanceof StaticFieldAssignment)) {
                    break;
                }
                StaticFieldAssignment assign = (StaticFieldAssignment) stmt;
                if (assign.getFieldName().contains("$VALUES")) {
                    continue;
                }
                if (!TypeHelper.descToType(assign.getOwnerType()).equals(this.current_type.getName()) || !(assign.getValue() instanceof New)) {
                    break;
                }
                FieldEntry assigned = this.current_type.getStaticField(assign.getFieldName());
                if (assigned == null) {
                    continue;
                }
                New n = (New) assign.getValue();
                String name = ((StringConstant) n.getParameters()[0]).getConstant();
                this.enum_names.put(assigned, name);
            }
        }
    }

    @Override
    public void visitTypeEnd() {
        this.current_type = null;
    }

} public class UnknownTypeMapper implements TypeVisitor {

    private final MappingsSet mappings;
    private final UnknownPackageDiscovery packages;
    private final MappingsSet previous;

    private int next_type = 0;

    public UnknownTypeMapper(MergeEngine engine, MappingsSet set, MappingsSet previous) {
        this.mappings = set;
        this.packages = new UnknownPackageDiscovery(engine);
        this.packages.build();
        this.previous = previous;
    }

    private String mapName(TypeEntry typeentry) {
        String type = typeentry.getName();
        String mapped = this.mappings.mapType(type);
        String mapped_child = null;
        if (mapped != null && type.lastIndexOf('$') != -1) {
            mapped_child = mapped.substring(mapped.lastIndexOf('$') + 1, mapped.length());
            mapped = null;
        }
        if (mapped == null) {
            if (type.lastIndexOf('$') != -1) {
                String parent = type.substring(0, type.lastIndexOf('$'));
                TypeEntry parent_type = typeentry.getSource().get(parent);
                String child = type.substring(type.lastIndexOf('$') + 1, type.length());
                String mapped_parent = mapName(parent_type);
                if (mapped_child == null) {
                    try {
                        int index = Integer.valueOf(child);
                        mapped = mapped_parent + "$" + index;
                        while (this.mappings.inverseType(mapped) != null) {
                            index++;
                            mapped = mapped_parent + "$" + index;
                        }
                    } catch (NumberFormatException e) {
                        child = String.format("CL_%04d_%s", this.next_type++, child);
                        mapped = mapped_parent + "$" + child;
                    }
                } else {
                    mapped = mapped_parent + "$" + mapped_child;
                }
                this.mappings.addTypeMapping(type, mapped);
                return mapped;
            }
            String pkg = this.packages.getPackage(typeentry);
            mapped = String.format("%s/CL_%04d_%s", pkg, this.next_type++, type);
            this.mappings.addTypeMapping(type, mapped);
        }
        return mapped;
    }

    @Override
    public void visitClassEntry(ClassEntry type) {
        mapName(type);
    }

    @Override
    public void visitEnumEntry(EnumEntry type) {
        mapName(type);
    }

    @Override
    public void visitInterfaceEntry(InterfaceEntry type) {
        mapName(type);
    }

    @Override
    public void visitAnnotationEntry(AnnotationEntry type) {
        mapName(type);
    }

    @Override
    public void visitAnnotation(Annotation annotation) {
    }

    @Override
    public void visitMethod(MethodEntry mth) {
    }

    @Override
    public void visitMethodEnd() {
    }

    @Override
    public void visitField(FieldEntry fld) {
    }

    @Override
    public void visitTypeEnd() {
    }

} public class MergeEngine {

    public static MergeOperation jumpTo(int index, Predicate<MergeEngine> condition) {
        return new JumpOperation(index, condition);
    }

    private List<MergeOperation> operations = new ArrayList<>();

    private final Map<TypeEntry, MatchEntry> matches = new HashMap<>();
    private final Map<TypeEntry, MatchEntry> matches_inverse = new HashMap<>();
    private final Map<MethodEntry, MethodMatchEntry> method_matches = new HashMap<>();
    private final Map<MethodEntry, MethodMatchEntry> method_matches_inverse = new HashMap<>();
    private final Map<FieldEntry, FieldMatchEntry> field_matches = new HashMap<>();
    private final Map<FieldEntry, FieldMatchEntry> field_matches_inverse = new HashMap<>();

    private final Map<TypeEntry, MatchEntry> pending_matches = new HashMap<>();
    private final Map<MethodEntry, MethodMatchEntry> pending_method_matches = new HashMap<>();
    private final Map<FieldEntry, FieldMatchEntry> pending_field_matches = new HashMap<>();

    private final Map<MethodEntry, MethodGroup> old_method_groups = new HashMap<>();
    private final Map<MethodEntry, MethodGroup> new_method_groups = new HashMap<>();

    private final Multimap<TypeEntry, TypeEntry> old_subtypes = HashMultimap.create();
    private final Multimap<TypeEntry, TypeEntry> new_subtypes = HashMultimap.create();

    private final SourceSet old_src;
    private final SourceSet new_src;
    private final MappingsSet old_mappings;
    private final MappingsSet new_mappings;
    private MappingsSet validation_mappings;

    private int changes_last_cycle = 0;

    public MergeEngine(SourceSet oldsrc, MappingsSet oldmap, SourceSet newsrc, MappingsSet newmap) {
        this.old_src = oldsrc;
        this.new_src = newsrc;
        this.old_mappings = oldmap;
        this.new_mappings = newmap;
    }

    @SuppressWarnings("unchecked")
    public <T extends MergeOperation> T getOperation(Class<T> type) {
        for (MergeOperation op : this.operations) {
            if (type.isInstance(op)) {
                return (T) op;
            }
        }
        return null;
    }

    public SourceSet getOldSourceSet() {
        return this.old_src;
    }

    public SourceSet getNewSourceSet() {
        return this.new_src;
    }

    public MappingsSet getOldMappings() {
        return this.old_mappings;
    }

    public MappingsSet getNewMappings() {
        return this.new_mappings;
    }

    public MappingsSet getValidationMappings() {
        return this.validation_mappings;
    }

    public void setValidationMappings(MappingsSet mappings) {
        this.validation_mappings = mappings;
    }

    public int getChangesLastCycle() {
        return this.changes_last_cycle;
    }

    public void incrementChangeCount() {
        this.changes_last_cycle++;
    }

    public void resetChanges() {
        this.changes_last_cycle = 0;
    }

    public MatchEntry getMatch(TypeEntry t) {
        return this.matches.get(t);
    }

    public MatchEntry getMatchInverse(TypeEntry t) {
        return this.matches_inverse.get(t);
    }

    public MatchEntry getPendingMatch(TypeEntry t) {
        MatchEntry m = getMatch(t);
        if (m != null) {
            return m;
        }
        m = this.pending_matches.get(t);
        if (m == null) {
            m = new MatchEntry(t);
            this.pending_matches.put(t, m);
        }
        return m;
    }

    public boolean vote(TypeEntry old, TypeEntry n) {
        MatchEntry m = getPendingMatch(old);
        if (this.matches_inverse.containsKey(n)) {
            return m.getNewType() == n;
        }
        return m.vote(n);
    }

    public boolean isTypeMatched(TypeEntry n) {
        return this.matches_inverse.containsKey(n) || this.matches.containsKey(n);
    }

    public void setAsMatched(MatchEntry entry) {
        if (entry.getNewType() == null) {
            throw new IllegalStateException();
        }
        this.pending_matches.remove(entry.getOldType());
        this.matches.put(entry.getOldType(), entry);
        this.matches_inverse.put(entry.getNewType(), entry);
        for (MatchEntry match : this.pending_matches.values()) {
            match.removeVote(entry.getNewType());
        }
    }

    public Collection<MatchEntry> getAllMatches() {
        return this.matches.values();
    }

    public Collection<MatchEntry> getPendingMatches() {
        return this.pending_matches.values();
    }

    public MethodGroup getOldMethodGroup(MethodEntry entry) {
        MethodGroup group = this.old_method_groups.get(entry);
        if (group == null) {
            group = new MethodGroup(entry);
            this.old_method_groups.put(entry, group);
        }
        return group;
    }

    public MethodGroup getNewMethodGroup(MethodEntry entry) {
        MethodGroup group = this.new_method_groups.get(entry);
        if (group == null) {
            group = new MethodGroup(entry);
            this.new_method_groups.put(entry, group);
        }
        return group;
    }

    public MethodMatchEntry getMethodMatch(MethodEntry t) {
        return this.method_matches.get(t);
    }

    public MethodMatchEntry getMethodMatchInverse(MethodEntry t) {
        return this.method_matches_inverse.get(t);
    }

    public MethodMatchEntry getPendingMethodMatch(MethodEntry t) {
        MethodMatchEntry m = getMethodMatch(t);
        if (m != null) {
            return m;
        }
        m = this.pending_method_matches.get(t);
        if (m == null) {
            m = new MethodMatchEntry(t);
            this.pending_method_matches.put(t, m);
        }
        return m;
    }

    public boolean vote(MethodEntry old, MethodEntry n) {
        MethodMatchEntry m = getPendingMethodMatch(old);
        if (this.method_matches_inverse.containsKey(n)) {
            return m.getNewMethod() == n;
        }
        return m.vote(n);
    }

    public boolean isMethodMatched(MethodEntry n) {
        return this.method_matches_inverse.containsKey(n) || this.method_matches.containsKey(n);
    }

    public void setAsMatched(MethodMatchEntry entry) {
        if (entry.getNewMethod() == null) {
            throw new IllegalStateException();
        }
        this.pending_method_matches.remove(entry.getOldMethod());
        this.method_matches.put(entry.getOldMethod(), entry);
        this.method_matches_inverse.put(entry.getNewMethod(), entry);
        for (MethodMatchEntry match : this.pending_method_matches.values()) {
            match.removeVote(entry.getNewMethod());
        }
    }

    public Collection<MethodMatchEntry> getAllMethodMatches() {
        return this.method_matches.values();
    }

    public Collection<MethodMatchEntry> getPendingMethodMatches() {
        return this.pending_method_matches.values();
    }

    public FieldMatchEntry getFieldMatch(FieldEntry t) {
        return this.field_matches.get(t);
    }

    public FieldMatchEntry getFieldMatchInverse(FieldEntry t) {
        return this.field_matches_inverse.get(t);
    }

    public FieldMatchEntry getPendingFieldMatch(FieldEntry t) {
        FieldMatchEntry m = getFieldMatch(t);
        if (m != null) {
            return m;
        }
        m = this.pending_field_matches.get(t);
        if (m == null) {
            m = new FieldMatchEntry(t);
            this.pending_field_matches.put(t, m);
        }
        return m;
    }

    public boolean vote(FieldEntry old, FieldEntry n) {
        FieldMatchEntry m = getPendingFieldMatch(old);
        if (this.field_matches_inverse.containsKey(n)) {
            return m.getNewField() == n;
        }
        return m.vote(n);
    }

    public boolean isFieldMatched(FieldEntry n) {
        return this.field_matches_inverse.containsKey(n) || this.field_matches.containsKey(n);
    }

    public void setAsMatched(FieldMatchEntry entry) {
        if (entry.getNewField() == null) {
            throw new IllegalStateException();
        }
        this.pending_field_matches.remove(entry.getOldField());
        this.field_matches.put(entry.getOldField(), entry);
        this.field_matches_inverse.put(entry.getNewField(), entry);
        for (FieldMatchEntry match : this.pending_field_matches.values()) {
            match.removeVote(entry.getNewField());
        }
    }

    public Collection<FieldMatchEntry> getAllFieldMatches() {
        return this.field_matches.values();
    }

    public Collection<FieldMatchEntry> getPendingFieldMatches() {
        return this.pending_field_matches.values();
    }

    public void addOperation(int index, MergeOperation op) {
        this.operations.add(index, op);
    }

    public void addOperation(MergeOperation op) {
        this.operations.add(op);
    }

    public List<MergeOperation> getOperations() {
        return this.operations;
    }

    public void merge() {

        for (String type : this.new_mappings.getMappedTypes()) {
            TypeEntry new_type = this.new_src.get(type);
            if (new_type == null) {
                continue;
            }
            String mapped = this.new_mappings.mapType(type);
            String old = this.old_mappings.inverseType(mapped);
            if (old != null) {
                TypeEntry old_type = this.old_src.get(old);
                if (old_type != null) {
                    MatchEntry match = getPendingMatch(old_type);
                    match.setNewType(new_type);
                    setAsMatched(match);
                    continue;
                }
            }
            TypeEntry dummy = createDummyType(this.old_src, "dummy" + new_type.getName());
            MatchEntry match = getPendingMatch(dummy);
            match.setNewType(new_type);
            setAsMatched(match);
        }

        generateSubtypes();
        generateMethodGroups();

        for (int i = 0; i < this.operations.size(); i++) {
            MergeOperation op = this.operations.get(i);
            if (op instanceof JumpOperation) {
                JumpOperation jump = (JumpOperation) op;
                if (jump.condition.test(this)) {
                    i = jump.target - 1;
                }
                continue;
            }
            op.operate(this);
        }

        for (MatchEntry entry : this.matches.values()) {
            String mapped = this.old_mappings.mapType(entry.getOldType().getName());
            if (mapped != null) {
                this.new_mappings.addTypeMapping(entry.getNewType().getName(), mapped);
            }
        }

        for (TypeEntry type : this.new_src.getAllClasses()) {
            if (!type.isAnonType()) {
                continue;
            }
            if (this.new_mappings.mapType(type.getName()) != null) {
                continue;
            }
            String parent = type.getName().substring(0, type.getName().lastIndexOf('$'));
            String parent_mapped = this.new_mappings.mapType(parent);
            if (parent_mapped == null) {
                continue;
            }
            int index = Integer.parseInt(type.getName().substring(type.getName().lastIndexOf('$') + 1, type.getName().length()));
            String mapped_name = parent_mapped + "$" + index;
            while (this.new_mappings.inverseType(mapped_name) != null) {
                index++;
                mapped_name = parent_mapped + "$" + index;
            }
            this.new_mappings.addTypeMapping(type.getName(), mapped_name);
        }

        for (FieldMatchEntry entry : this.field_matches.values()) {
            if (entry.getOldField() instanceof DummyField) {
                FieldEntry fld = entry.getNewField();
                if (this.new_mappings.mapType(fld.getOwnerName()) == null) {
                    continue;
                }
                this.new_mappings.addFieldMapping(fld.getOwnerName(), fld.getName(), entry.getOldField().getName());
            } else {
                String owner = entry.getOldField().getOwnerName();
                String mapped = this.old_mappings.mapField(owner, entry.getOldField().getName());
                if (mapped != null) {
                    FieldEntry fld = entry.getNewField();
                    this.new_mappings.addFieldMapping(fld.getOwnerName(), fld.getName(), mapped);
                }
            }
        }

        for (MethodMatchEntry entry : this.method_matches.values()) {
            if (entry.getOldMethod().getName().startsWith("<")) {
                continue;
            }
            if (entry.getOldMethod() instanceof DummyMethod) {
                MethodEntry mth = entry.getNewMethod();
                if (this.new_mappings.mapType(mth.getOwnerName()) == null) {
                    continue;
                }
                this.new_mappings.addMethodMapping(mth.getOwnerName(), mth.getName(), mth.getDescription(), entry.getOldMethod().getName());
            } else {
                String owner = entry.getOldMethod().getOwnerName();
                String mapped = this.old_mappings.mapMethod(owner, entry.getOldMethod().getName(), entry.getOldMethod().getDescription());
                if (mapped != null) {
                    MethodEntry mth = entry.getNewMethod();
                    this.new_mappings.addMethodMapping(mth.getOwnerName(), mth.getName(), mth.getDescription(), mapped);
                }
            }
        }

    }

    private void generateSubtypes() {
        generateSubtypes(this.old_src, this.old_subtypes);
        generateSubtypes(this.new_src, this.new_subtypes);
    }

    private void generateSubtypes(SourceSet src, Multimap<TypeEntry, TypeEntry> subtypes) {
        for (TypeEntry type : src.getAllClasses()) {
            if (type instanceof ClassEntry) {
                ClassEntry cls = (ClassEntry) type;
                TypeEntry spr = src.get(cls.getSuperclassName());
                if (spr != null) {
                    subtypes.put(spr, type);
                }
            }
            for (String intr : type.getInterfaces()) {
                TypeEntry inter = src.get(intr);
                if (inter != null) {
                    subtypes.put(inter, type);
                }
            }
        }
    }

    private void generateMethodGroups() {
        MethodGroupBuilder old_builder = new MethodGroupBuilder(this.old_src, this.old_method_groups, this.old_subtypes);
        old_builder.build();
        MethodGroupBuilder new_builder = new MethodGroupBuilder(this.new_src, this.new_method_groups, this.new_subtypes);
        new_builder.build();
    }

    public static TypeEntry createDummyType(SourceSet set, String name) {
        TypeEntry type = new DummyType(set, Language.JAVA, name);
        return type;
    }

    public static FieldEntry createDummyField(SourceSet set, String name, TypeSignature desc, String owner) {
        FieldEntry fld = new DummyField(set);
        fld.setName(name);
        fld.setType(desc);
        fld.setOwner(owner);
        return fld;
    }

    public static MethodEntry createDummyMethod(SourceSet set, String name, String desc, String owner) {
        MethodEntry mth = new DummyMethod(set);
        MethodSignature sig = new MethodSignature(ClassTypeSignature.of(TypeHelper.getRet(desc)));
        List<String> params = TypeHelper.splitSig(desc);
        for (String param : params) {
            sig.getParameters().add(ClassTypeSignature.of(param));
        }
        mth.setMethodSignature(sig);
        mth.setName(name);
        mth.setDescription(desc);
        mth.setOwner(owner);
        return mth;
    }

    private static class JumpOperation implements MergeOperation {

        public int target;
        public Predicate<MergeEngine> condition;

        public JumpOperation(int target, Predicate<MergeEngine> condition) {
            this.target = target;
            this.condition = condition;
        }

        @Override
        public void operate(MergeEngine set) {
        }
    }

    public static class DummyType extends TypeEntry {

        public DummyType(SourceSet source, Language lang, String name) {
            super(source, lang, name);
        }

        @Override
        public void accept(AstVisitor visitor) {
        }

        @Override
        public void writeTo(MessagePacker pack) throws IOException {
        }

    }

    public static class DummyMethod extends MethodEntry {

        public DummyMethod(SourceSet source) {
            super(source);
        }

    }

    public static class DummyField extends FieldEntry {

        public DummyField(SourceSet source) {
            super(source);
        }

    }

} public class CustomMethodMergers implements MergeOperation {

    private static final Map<String, BiConsumer<MethodMatchEntry, MergeEngine>> custom_mergers = new HashMap<>();

    @Override
    public void operate(MergeEngine set) {

        for (MethodMatchEntry match : set.getAllMethodMatches()) {
            if (match.getNewMethod() == null || match.isMerged()) {
                continue;
            }
            MethodEntry old = match.getOldMethod();
            MappingsSet old_set = set.getOldMappings();
            String key = "L" + old_set.mapTypeSafe(old.getOwnerName()) + ";"
                    + old_set.mapMethodSafe(old.getOwnerName(), old.getName(), old.getDescription())
                    + MappingsSet.MethodMapping.mapSig(old.getDescription(), old_set);
            BiConsumer<MethodMatchEntry, MergeEngine> merger = custom_mergers.get(key);
            if (merger != null) {
                merger.accept(match, set);
                match.setMerged();
            }
        }

    }

    private static void noop(MethodMatchEntry match, MergeEngine set) {
    }

    private static void bootstrap_handler(MethodMatchEntry match, MergeEngine set) {

        if (match.getOldMethod().getInstructions() == null || match.getNewMethod().getInstructions() == null) {
            return;
        }
        MethodEntry old_reg = null;
        Map<String, FieldEntry> old_sounds = new HashMap<>();
        for (Statement stmt : match.getOldMethod().getInstructions()) {
            if (stmt instanceof StaticFieldAssignment) {
                StaticFieldAssignment assign = (StaticFieldAssignment) stmt;
                TypeEntry field_owner = set.getOldSourceSet().get(assign.getOwnerName());
                if (field_owner == null) {
                    continue;
                }
                FieldEntry fld = field_owner.getStaticField(assign.getFieldName());
                if (fld == null) {
                    continue;
                }
                Instruction val = assign.getValue();
                if (val instanceof Cast) {
                    val = ((Cast) val).getValue();
                }
                if (!(val instanceof StaticMethodInvoke)) {
                    continue;
                }
                StaticMethodInvoke invoke = (StaticMethodInvoke) val;
                TypeEntry mth_owner = set.getOldSourceSet().get(invoke.getOwnerName());
                if (mth_owner != null) {
                    MethodEntry reg = mth_owner.getStaticMethod(invoke.getMethodName(), invoke.getMethodDescription());
                    if (reg != null) {
                        old_reg = reg;
                    }
                }
                if (invoke.getParameters().length != 1 || !(invoke.getParameters()[0] instanceof StringConstant)) {
                    continue;
                }
                old_sounds.put(((StringConstant) invoke.getParameters()[0]).getConstant(), fld);
            }
        }
        for (Statement stmt : match.getNewMethod().getInstructions()) {
            if (stmt instanceof StaticFieldAssignment) {
                StaticFieldAssignment assign = (StaticFieldAssignment) stmt;
                TypeEntry field_owner = set.getNewSourceSet().get(assign.getOwnerName());
                if (field_owner == null) {
                    continue;
                }
                FieldEntry fld = field_owner.getStaticField(assign.getFieldName());
                if (fld == null) {
                    continue;
                }
                Instruction val = assign.getValue();
                if (val instanceof Cast) {
                    val = ((Cast) val).getValue();
                }
                if (!(val instanceof StaticMethodInvoke)) {
                    continue;
                }
                StaticMethodInvoke invoke = (StaticMethodInvoke) val;
                TypeEntry mth_owner = set.getOldSourceSet().get(invoke.getOwnerName());
                if (mth_owner != null) {
                    MethodEntry reg = mth_owner.getStaticMethod(invoke.getMethodName(), invoke.getMethodDescription());
                    if (reg != null) {
                        set.vote(old_reg, reg);
                    }
                }
                if (invoke.getParameters().length != 1 || !(invoke.getParameters()[0] instanceof StringConstant)) {
                    continue;
                }
                String key = ((StringConstant) invoke.getParameters()[0]).getConstant();
                FieldEntry old = old_sounds.get(key);
                if (old != null) {
                    set.vote(old, fld);
                }
            }
        }
    }

    private static void statlist_handler(MethodMatchEntry match, MergeEngine set) {
        if (match.getOldMethod().getInstructions() == null || match.getNewMethod().getInstructions() == null) {
            return;
        }
        Map<String, FieldEntry> old_fields = new HashMap<>();
        Map<String, Instruction> old_vals = new HashMap<>();
        for (Statement stmt : match.getOldMethod().getInstructions()) {
            if (stmt instanceof StaticFieldAssignment) {
                StaticFieldAssignment assign = (StaticFieldAssignment) stmt;
                TypeEntry field_owner = set.getOldSourceSet().get(assign.getOwnerName());
                if (field_owner == null) {
                    continue;
                }
                FieldEntry fld = field_owner.getStaticField(assign.getFieldName());
                if (fld == null) {
                    continue;
                }
                Instruction val = assign.getValue();
                if (val instanceof Cast) {
                    val = ((Cast) val).getValue();
                }
                String key = null;
                while (val instanceof InstanceMethodInvoke) {
                    val = ((InstanceMethodInvoke) val).getCallee();
                }
                if (!(val instanceof New)) {
                    continue;
                }
                New invoke = (New) val;
                if (invoke.getParameters().length == 0 || !(invoke.getParameters()[0] instanceof StringConstant)) {
                    continue;
                }
                key = ((StringConstant) invoke.getParameters()[0]).getConstant();
                old_fields.put(key, fld);
                old_vals.put(key, val);
            }
        }
        for (Statement stmt : match.getNewMethod().getInstructions()) {
            if (stmt instanceof StaticFieldAssignment) {
                StaticFieldAssignment assign = (StaticFieldAssignment) stmt;
                TypeEntry field_owner = set.getNewSourceSet().get(assign.getOwnerName());
                if (field_owner == null) {
                    continue;
                }
                FieldEntry fld = field_owner.getStaticField(assign.getFieldName());
                if (fld == null) {
                    continue;
                }
                Instruction val = assign.getValue();
                if (val instanceof Cast) {
                    val = ((Cast) val).getValue();
                }
                String key = null;
                while (val instanceof InstanceMethodInvoke) {
                    val = ((InstanceMethodInvoke) val).getCallee();
                }
                if (!(val instanceof New)) {
                    continue;
                }
                New invoke = (New) val;
                if (invoke.getParameters().length == 0 || !(invoke.getParameters()[0] instanceof StringConstant)) {
                    continue;
                }
                key = ((StringConstant) invoke.getParameters()[0]).getConstant();
                FieldEntry old = old_fields.get(key);
                if (old != null) {
                    set.vote(old, fld);
                    MergeUtil.merge(set, old_vals.get(key), assign.getValue());
                }
            }
        }
    }

    private static void register_items(MethodMatchEntry match, MergeEngine set) {
        if (match.getOldMethod().getInstructions() == null || match.getNewMethod().getInstructions() == null) {
            return;
        }
        Map<String, TypeEntry> old_types = new HashMap<>();
        Map<String, TypeEntry> new_types = new HashMap<>();
        findBlockTypes(match.getOldMethod().getInstructions(), set.getOldSourceSet(), old_types);
        findBlockItems(match.getOldMethod().getInstructions(), set.getOldSourceSet(), old_types);
        findBlockTypes(match.getNewMethod().getInstructions(), set.getNewSourceSet(), new_types);
        findBlockItems(match.getNewMethod().getInstructions(), set.getNewSourceSet(), new_types);
        for (Map.Entry<String, TypeEntry> e : new_types.entrySet()) {
            TypeEntry old = old_types.get(e.getKey());
            if (old != null) {
                set.vote(old, e.getValue());
            }
        }
    }

    private static String findBootstrapKey(TypeEntry owner, FieldEntry key) {

        MethodEntry clinit = owner.getStaticMethod("<clinit>");

        for (Statement stmt : clinit.getInstructions()) {
            if (stmt instanceof StaticFieldAssignment) {
                StaticFieldAssignment assign = (StaticFieldAssignment) stmt;
                if (assign.getFieldName().equals(key.getName())) {
                    Instruction val = assign.getValue();
                    if (val instanceof Cast) {
                        val = ((Cast) val).getValue();
                    }
                    if (!(val instanceof StaticMethodInvoke)) {
                        continue;
                    }
                    StaticMethodInvoke invoke = (StaticMethodInvoke) val;
                    if (invoke.getParameters().length != 1 || !(invoke.getParameters()[0] instanceof StringConstant)) {
                        continue;
                    }
                    return ((StringConstant) invoke.getParameters()[0]).getConstant();
                }
            }
        }

        return null;
    }

    private static void findBlockItems(StatementBlock block, SourceSet src, Map<String, TypeEntry> types) {
        for (Statement stmt : block) {
            if (stmt instanceof InvokeStatement) {
                Instruction inner = ((InvokeStatement) stmt).getInstruction();
                if (inner instanceof StaticMethodInvoke) {
                    StaticMethodInvoke reg = (StaticMethodInvoke) inner;
                    if (reg.getParameters().length != 2) {
                        continue;
                    }
                    Instruction val = reg.getParameters()[1];
                    if (val instanceof New) {
                        Instruction key_insn = reg.getParameters()[0];
                        if (!(key_insn instanceof StaticFieldAccess)) {
                            continue;
                        }
                        StaticFieldAccess key_acc = (StaticFieldAccess) key_insn;
                        TypeEntry key_owner = src.get(key_acc.getOwnerName());
                        FieldEntry key = key_owner.getStaticField(key_acc.getFieldName());
                        String type = TypeHelper.descToType(((New) val).getType().getDescriptor());
                        TypeEntry block_type = src.get(type);
                        if (block_type != null) {
                            String bkey = findBootstrapKey(key_owner, key);
                            types.put(bkey, block_type);
                        }
                        continue;
                    }
                    if (val instanceof Cast) {
                        val = ((Cast) val).getValue();
                    }
                    String key = null;
                    if (val instanceof InstanceMethodInvoke) {
                        InstanceMethodInvoke invoke = (InstanceMethodInvoke) val;
                        if (!invoke.getMethodDescription().startsWith("(Ljava/lang/String;)")
                                || !(invoke.getParameters()[0] instanceof StringConstant)) {
                            continue;
                        }
                        key = ((StringConstant) invoke.getParameters()[0]).getConstant();
                    } else {
                        continue;
                    }
                    while (val instanceof InstanceMethodInvoke) {
                        val = ((InstanceMethodInvoke) val).getCallee();
                    }
                    if (!(val instanceof New)) {
                        continue;
                    }
                    String type = TypeHelper.descToType(((New) val).getType().getDescriptor());
                    TypeEntry block_type = src.get(type);
                    if (block_type != null) {
                        types.put(key, block_type);
                    }
                }
            }
        }
    }

    private static void register_blocks(MethodMatchEntry match, MergeEngine set) {
        if (match.getOldMethod().getInstructions() == null || match.getNewMethod().getInstructions() == null) {
            return;
        }
        Map<String, TypeEntry> old_types = new HashMap<>();
        Map<String, TypeEntry> new_types = new HashMap<>();
        findBlockTypes(match.getOldMethod().getInstructions(), set.getOldSourceSet(), old_types);
        findBlockTypes(match.getNewMethod().getInstructions(), set.getNewSourceSet(), new_types);
        for (Map.Entry<String, TypeEntry> e : new_types.entrySet()) {
            TypeEntry old = old_types.get(e.getKey());
            if (old != null) {
                set.vote(old, e.getValue());
            }
        }
    }

    private static void findBlockTypes(StatementBlock block, SourceSet src, Map<String, TypeEntry> types) {
        Instruction last = null;
        for (Statement stmt : block) {
            if (stmt instanceof For) {
                break;
            }
            if (stmt instanceof LocalAssignment) {
                last = ((LocalAssignment) stmt).getValue();
            } else if (stmt instanceof InvokeStatement) {
                Instruction inner = ((InvokeStatement) stmt).getInstruction();
                if (inner instanceof StaticMethodInvoke) {
                    StaticMethodInvoke reg = (StaticMethodInvoke) inner;
                    if (reg.getParameters().length != 3) {
                        last = null;
                        continue;
                    }
                    if (!reg.getMethodDescription().startsWith("(I")) {
                        last = null;
                        continue;
                    }
                    Instruction val = reg.getParameters()[2];
                    if (val instanceof Cast) {
                        val = ((Cast) val).getValue();
                    }
                    String key = null;
                    if (!(reg.getParameters()[1] instanceof StringConstant)) {
                        if (val instanceof InstanceMethodInvoke) {
                            InstanceMethodInvoke invoke = (InstanceMethodInvoke) val;
                            if (!invoke.getMethodDescription().startsWith("(Ljava/lang/String;)")
                                    || !(invoke.getParameters()[0] instanceof StringConstant)) {
                                last = null;
                                continue;
                            }
                            key = ((StringConstant) invoke.getParameters()[0]).getConstant();
                        }
                    } else {
                        key = ((StringConstant) reg.getParameters()[1]).getConstant();
                    }
                    if (key == null) {
                        last = null;
                        continue;
                    }
                    if (val instanceof LocalAccess && last != null) {
                        val = last;
                    }
                    while (val instanceof InstanceMethodInvoke) {
                        val = ((InstanceMethodInvoke) val).getCallee();
                    }
                    if (!(val instanceof New)) {
                        last = null;
                        continue;
                    }
                    String type = TypeHelper.descToType(((New) val).getType().getDescriptor());
                    TypeEntry block_type = src.get(type);
                    if (block_type != null) {
                        types.put(key, block_type);
                    }
                    last = null;
                } else {
                    last = null;
                }
            }
        }
    }

    private static void register_enchantments(MethodMatchEntry match, MergeEngine set) {
        if (match.getOldMethod().getInstructions() == null || match.getNewMethod().getInstructions() == null) {
            return;
        }
        Map<String, TypeEntry> old_types = new HashMap<>();
        Map<String, TypeEntry> new_types = new HashMap<>();
        findEnchantments(match.getOldMethod().getInstructions(), set.getOldSourceSet(), old_types);
        findEnchantments(match.getNewMethod().getInstructions(), set.getNewSourceSet(), new_types);
        for (Map.Entry<String, TypeEntry> e : new_types.entrySet()) {
            TypeEntry old = old_types.get(e.getKey());
            if (old != null) {
                set.vote(old, e.getValue());
            }
        }
    }

    private static void findEnchantments(StatementBlock block, SourceSet src, Map<String, TypeEntry> types) {
        for (Statement stmt : block) {
            if (stmt instanceof InvokeStatement) {
                Instruction inner = ((InvokeStatement) stmt).getInstruction();
                if (inner instanceof InstanceMethodInvoke) {
                    InstanceMethodInvoke reg = (InstanceMethodInvoke) inner;
                    if (reg.getParameters().length != 3) {
                        continue;
                    }
                    Instruction key_val = reg.getParameters()[1];
                    String key = null;
                    if (key_val instanceof New) {
                        New n = (New) key_val;
                        if (n.getParameters().length == 1 && n.getParameters()[0] instanceof StringConstant) {
                            key = ((StringConstant) n.getParameters()[0]).getConstant();
                        }
                    }
                    if (key == null) {
                        continue;
                    }
                    Instruction val = reg.getParameters()[2];
                    if (val instanceof Cast) {
                        val = ((Cast) val).getValue();
                    }
                    while (val instanceof InstanceMethodInvoke) {
                        val = ((InstanceMethodInvoke) val).getCallee();
                    }
                    if (!(val instanceof New)) {
                        continue;
                    }
                    String type = TypeHelper.descToType(((New) val).getType().getDescriptor());
                    TypeEntry block_type = src.get(type);
                    if (block_type != null) {
                        types.put(key, block_type);
                    }
                }
            }
        }
    }

    static {
        custom_mergers.put("Lnet/minecraft/init/SoundEvents;<clinit>()V", CustomMethodMergers::bootstrap_handler);
        custom_mergers.put("Lnet/minecraft/init/Blocks;<clinit>()V", CustomMethodMergers::bootstrap_handler);
        custom_mergers.put("Lnet/minecraft/init/Items;<clinit>()V", CustomMethodMergers::bootstrap_handler);
        custom_mergers.put("Lnet/minecraft/init/Biomes;<clinit>()V", CustomMethodMergers::bootstrap_handler);
        custom_mergers.put("Lnet/minecraft/init/MobEffects;<clinit>()V", CustomMethodMergers::bootstrap_handler);
        custom_mergers.put("Lnet/minecraft/init/PotionTypes;<clinit>()V", CustomMethodMergers::bootstrap_handler);
        custom_mergers.put("Lnet/minecraft/init/Enchantments;<clinit>()V", CustomMethodMergers::bootstrap_handler);
        custom_mergers.put("Lnet/minecraft/world/storage/loot/LootTableList;<clinit>()V", CustomMethodMergers::bootstrap_handler);

        custom_mergers.put("Lnet/minecraft/stats/StatList;<clinit>()V", CustomMethodMergers::statlist_handler);

        custom_mergers.put("Lnet/minecraft/util/datafix/DataFixesManager;func_188276_a(Lnet/minecraft/util/datafix/DataFixer;)V",
                CustomMethodMergers::noop);
        custom_mergers.put("Lnet/minecraft/util/datafix/DataFixesManager;func_188279_a()Lnet/minecraft/util/datafix/DataFixer;",
                CustomMethodMergers::noop);

        custom_mergers.put("Lnet/minecraft/block/Block;func_149671_p()V", CustomMethodMergers::register_blocks);
        custom_mergers.put("Lnet/minecraft/item/Item;func_150900_l()V", CustomMethodMergers::register_items);
        custom_mergers.put("Lnet/minecraft/world/biome/Biome;func_185358_q()V", CustomMethodMergers::register_blocks);
        custom_mergers.put("Lnet/minecraft/enchantment/Enchantment;func_185257_f()V", CustomMethodMergers::register_enchantments);
    }

} public class MatchDiscreteFields implements MergeOperation {

    @Override
    public void operate(MergeEngine set) {

        for (MatchEntry type_match : set.getAllMatches()) {
            {
                Map<String, FieldEntry> new_discrete = new HashMap<>();
                Set<String> invalid = new HashSet<>();

                for (FieldEntry mth : type_match.getNewType().getFields()) {
                    if (set.isFieldMatched(mth)) {
                        continue;
                    }
                    String key = mapDesc(mth.getType().getDescriptor(), set.getNewMappings());
                    if (invalid.contains(key)) {
                        continue;
                    }
                    if (new_discrete.containsKey(key)) {
                        invalid.add(key);
                        new_discrete.remove(key);
                    } else {
                        new_discrete.put(key, mth);
                    }
                }

                invalid.clear();
                Map<String, FieldEntry> old_discrete = new HashMap<>();

                for (FieldEntry mth : type_match.getOldType().getFields()) {
                    if (set.isFieldMatched(mth)) {
                        continue;
                    }
                    String key = mapOldDesc(mth.getType().getDescriptor(), set.getOldMappings(), set.getNewMappings());
                    if (invalid.contains(key)) {
                        continue;
                    }
                    if (old_discrete.containsKey(key)) {
                        invalid.add(key);
                        old_discrete.remove(key);
                    } else {
                        old_discrete.put(key, mth);
                    }
                }

                for (Map.Entry<String, FieldEntry> e : new_discrete.entrySet()) {
                    FieldEntry old = old_discrete.get(e.getKey());
                    if (old == null) {
                        continue;
                    }
                    set.vote(old, e.getValue());
                }
            }
            {
                Map<String, FieldEntry> new_discrete = new HashMap<>();
                Set<String> invalid = new HashSet<>();

                for (FieldEntry mth : type_match.getNewType().getStaticFields()) {
                    if (set.isFieldMatched(mth)) {
                        continue;
                    }
                    String key = mapDesc(mth.getType().getDescriptor(), set.getNewMappings());
                    if (invalid.contains(key)) {
                        continue;
                    }
                    if (new_discrete.containsKey(key)) {
                        invalid.add(key);
                        new_discrete.remove(key);
                    } else {
                        new_discrete.put(key, mth);
                    }
                }

                invalid.clear();
                Map<String, FieldEntry> old_discrete = new HashMap<>();

                for (FieldEntry mth : type_match.getOldType().getStaticFields()) {
                    if (set.isFieldMatched(mth)) {
                        continue;
                    }
                    String key = mapOldDesc(mth.getType().getDescriptor(), set.getOldMappings(), set.getNewMappings());
                    if (invalid.contains(key)) {
                        continue;
                    }
                    if (old_discrete.containsKey(key)) {
                        invalid.add(key);
                        old_discrete.remove(key);
                    } else {
                        old_discrete.put(key, mth);
                    }
                }

                for (Map.Entry<String, FieldEntry> e : new_discrete.entrySet()) {
                    FieldEntry old = old_discrete.get(e.getKey());
                    if (old == null) {
                        continue;
                    }
                    set.vote(old, e.getValue());
                }
            }
        }
    }

    private static String mapDesc(String desc, MappingsSet map) {
        if (desc.startsWith("[")) {
            return "[" + mapDesc(desc.substring(1), map);
        }
        if (desc.startsWith("L")) {
            String cls = desc.substring(1, desc.length() - 1);
            String m = map.mapType(cls);
            if (m == null) {
                return null;
            }
            return "L" + m + ";";
        }
        return desc;
    }

    private static String mapOldDesc(String desc, MappingsSet old_map, MappingsSet new_map) {
        if (desc.startsWith("[")) {
            return "[" + mapOldDesc(desc.substring(1), old_map, new_map);
        }
        if (desc.startsWith("L")) {
            String cls = desc.substring(1, desc.length() - 1);
            String m = old_map.mapType(cls);
            if (new_map.inverseType(m) == null) {
                return null;
            }
            return "L" + m + ";";
        }
        return desc;
    }

} public class MatchDiscreteMethods implements MergeOperation {

    @Override
    public void operate(MergeEngine set) {
        for (MatchEntry type_match : set.getAllMatches()) {
            matchDiscrete(set, type_match.getOldType().getMethods(), type_match.getNewType().getMethods());
            matchDiscrete(set, type_match.getOldType().getStaticMethods(), type_match.getNewType().getStaticMethods());
        }
    }

    public static void matchDiscrete(MergeEngine set, Collection<MethodEntry> old_methods, Collection<MethodEntry> new_methods) {

        Map<String, MethodEntry> new_discrete = new HashMap<>();
        Set<String> invalid = new HashSet<>();

        for (MethodEntry mth : new_methods) {
            if (set.isMethodMatched(mth)) {
                continue;
            }
            String key = makeKey(mth.getDescription(), set.getNewMappings());
            if (invalid.contains(key)) {
                continue;
            }
            if (new_discrete.containsKey(key)) {
                invalid.add(key);
                new_discrete.remove(key);
            } else {
                new_discrete.put(key, mth);
            }
        }

        invalid.clear();
        Map<String, MethodEntry> old_discrete = new HashMap<>();

        for (MethodEntry mth : old_methods) {
            if (set.isMethodMatched(mth)) {
                continue;
            }
            String key = makeOldKey(mth.getDescription(), set.getOldMappings(), set.getNewMappings());
            if (invalid.contains(key)) {
                continue;
            }
            if (old_discrete.containsKey(key)) {
                invalid.add(key);
                old_discrete.remove(key);
            } else {
                old_discrete.put(key, mth);
            }
        }

        for (Map.Entry<String, MethodEntry> e : new_discrete.entrySet()) {
            MethodEntry old = old_discrete.get(e.getKey());
            if (old == null) {
                continue;
            }
            set.vote(old, e.getValue());
        }
    }

    public static String mapDesc(String desc, MappingsSet map) {
        if (desc.startsWith("[")) {
            return "[" + mapDesc(desc.substring(1), map);
        }
        if (desc.startsWith("L")) {
            String cls = desc.substring(1, desc.length() - 1);
            String m = map.mapType(cls);
            if (m == null) {
                return null;
            }
            return "L" + m + ";";
        }
        return desc;
    }

    public static String makeKey(String desc, MappingsSet map) {
        String out = "(";
        for (int i = 1; i < desc.length(); i++) {
            char next = desc.charAt(i);
            if (next == ')') {
                break;
            }
            String accu = "";
            if (next == '[') {
                accu += next;
            } else if (next == 'L') {
                while (next != ';') {
                    accu += next;
                    next = desc.charAt(++i);
                }
                accu += ';';
                String m = mapDesc(accu, map);
                if (m == null) {
                    out += "*";
                } else {
                    out += m;
                }
                accu = "";
            } else {
                accu += next;
                String m = mapDesc(accu, map);
                if (m == null) {
                    out += "*";
                } else {
                    out += m;
                }
                accu = "";
            }
        }
        String ret = desc.substring(desc.indexOf(')') + 1);
        out += ")";
        String m = mapDesc(ret, map);
        if (m == null) {
            out += "*";
        } else {
            out += m;
        }
        return out;
    }

    public static String mapOldDesc(String desc, MappingsSet old_map, MappingsSet new_map) {
        if (desc.startsWith("[")) {
            return "[" + mapOldDesc(desc.substring(1), old_map, new_map);
        }
        if (desc.startsWith("L")) {
            String cls = desc.substring(1, desc.length() - 1);
            String m = old_map.mapType(cls);
            if (new_map.inverseType(m) == null) {
                return null;
            }
            return "L" + m + ";";
        }
        return desc;
    }

    public static String makeOldKey(String desc, MappingsSet old_map, MappingsSet new_map) {
        String out = "(";
        for (int i = 1; i < desc.length(); i++) {
            char next = desc.charAt(i);
            if (next == ')') {
                break;
            }
            String accu = "";
            if (next == '[') {
                accu += next;
            } else if (next == 'L') {
                while (next != ';') {
                    accu += next;
                    next = desc.charAt(++i);
                }
                accu += ';';
                String m = mapOldDesc(accu, old_map, new_map);
                if (m == null) {
                    out += "*";
                } else {
                    out += m;
                }
                accu = "";
            } else {
                accu += next;
                String m = mapOldDesc(accu, old_map, new_map);
                if (m == null) {
                    out += "*";
                } else {
                    out += m;
                }
                accu = "";
            }
        }
        String ret = desc.substring(desc.indexOf(')') + 1);
        out += ")";
        String m = mapOldDesc(ret, old_map, new_map);
        if (m == null) {
            out += "*";
        } else {
            out += m;
        }
        return out;
    }

} public class MatchEnums implements MergeOperation {

    @Override
    public void operate(MergeEngine set) {
        for (EnumEntry n : set.getNewSourceSet().getAllEnums()) {
            if (n.isAnonType() || n.getEnumConstants().isEmpty() || n.getName().contains("$")) {
                continue;
            }

            search: for (EnumEntry m : set.getOldSourceSet().getAllEnums()) {
                if (m.isAnonType() || m.getEnumConstants().isEmpty() || n.getEnumConstants().size() < m.getEnumConstants().size()
                        || m.getName().contains("$")) {
                    continue;
                }
                for (String cst : m.getEnumConstants()) {
                    if (!n.getEnumConstants().contains(cst)) {
                        continue search;
                    }
                }
                set.vote(m, n);
            }
        }

    }

} public class MatchInnerClasses implements MergeOperation {

    private boolean prepared = false;

    private final Multimap<TypeEntry, TypeEntry> old_inners = HashMultimap.create();
    private final Multimap<TypeEntry, TypeEntry> new_inners = HashMultimap.create();

    private void prep(MergeEngine set) {
        for (TypeEntry type : set.getOldSourceSet().getAllClasses()) {
            if (!type.getName().contains("$")) {
                continue;
            }
            String parent_name = type.getName().substring(0, type.getName().lastIndexOf('$'));
            TypeEntry parent = set.getOldSourceSet().get(parent_name);
            if (parent == null) {
                throw new IllegalStateException(parent_name + " not found as parent type");
            }
            this.old_inners.put(parent, type);
        }
        for (TypeEntry type : set.getNewSourceSet().getAllClasses()) {
            if (!type.getName().contains("$")) {
                continue;
            }
            String parent_name = type.getName().substring(0, type.getName().lastIndexOf('$'));
            TypeEntry parent = set.getNewSourceSet().get(parent_name);
            if (parent == null) {
                throw new IllegalStateException(parent_name + " not found as parent type");
            }
            this.new_inners.put(parent, type);
        }
    }

    @Override
    public void operate(MergeEngine set) {
        if (!this.prepared) {
            this.prepared = true;
            prep(set);
        }

        for (TypeEntry type : set.getOldSourceSet().getAllClasses()) {
            MatchEntry match = set.getMatch(type);
            if (match == null) {
                continue;
            }
            if (type.getName().contains("$") && match.getNewType().getName().contains("$")) {
                String parent_name = type.getName().substring(0, type.getName().lastIndexOf('$'));
                TypeEntry parent = set.getOldSourceSet().get(parent_name);
                MatchEntry parent_match = set.getPendingMatch(parent);
                if (parent != null && parent_match.getNewType() == null) {
                    String new_parent_name = match.getNewType().getName().substring(0, match.getNewType().getName().lastIndexOf('$'));
                    TypeEntry new_parent = set.getNewSourceSet().get(new_parent_name);
                    if (new_parent != null) {
                        set.vote(parent, new_parent);
                    }
                }
            }
            TypeEntry new_parent = match.getNewType();
            Collection<TypeEntry> old_all_inners = this.old_inners.get(type);
            if (old_all_inners == null) {
                continue;
            }
            Collection<TypeEntry> new_all_inners = this.new_inners.get(new_parent);
            if (new_all_inners == null) {
                continue;
            }
            mergeDistinct(set, old_all_inners, new_all_inners);
        }

    }

    private void mergeDistinct(MergeEngine set, Collection<TypeEntry> old_inners, Collection<TypeEntry> new_inners) {
        List<TypeEntry> old_anon = new ArrayList<>();
        List<TypeEntry> new_anon = new ArrayList<>();
        List<TypeEntry> old_named = new ArrayList<>();
        List<TypeEntry> new_named = new ArrayList<>();

        for (TypeEntry type : old_inners) {
            if (type.isAnonType()) {
                old_anon.add(type);
            } else {
                old_named.add(type);
            }
        }

        for (TypeEntry type : new_inners) {
            if (type.isAnonType()) {
                new_anon.add(type);
            } else {
                new_named.add(type);
            }
        }

        if (old_anon.size() == 1 && new_anon.size() == 1) {
            set.vote(old_anon.get(0), new_anon.get(0));
        } else {
        }

        if (old_named.size() == 1 && new_named.size() == 1) {
            set.vote(old_named.get(0), new_named.get(0));
        } else {
            for (TypeEntry type : old_named) {
                if (set.isTypeMatched(type)) {
                    continue;
                }
                if (type instanceof EnumEntry) {
                    TypeEntry possible = null;
                    int matches = 0;

                    for (TypeEntry pos : new_named) {
                        if (!(pos instanceof EnumEntry)) {
                            continue;
                        }
                        EnumEntry pos_enum = (EnumEntry) pos;
                        int m = 0;
                        for (String cst : ((EnumEntry) type).getEnumConstants()) {
                            if (pos_enum.getEnumConstants().contains(cst)) {
                                m++;
                            }
                        }
                        if (m > matches) {
                            possible = pos;
                        } else if (m == matches) {
                            possible = null;
                        }
                    }
                    if (possible != null) {
                        set.vote(type, possible);
                    }
                    continue;
                }
            }
        }
    }

} public class MatchMethodGroups implements MergeOperation {

    @Override
    public void operate(MergeEngine set) {

        Set<MethodGroup> handled = new HashSet<>();

        for (MethodMatchEntry match : set.getAllMethodMatches()) {
            MethodGroup old_group = set.getOldMethodGroup(match.getOldMethod());
            if (old_group.getMethods().size() == 1) {
                continue;
            }
            if (handled.contains(old_group)) {
                continue;
            }
            handled.add(old_group);
            MethodGroup new_group = set.getNewMethodGroup(match.getNewMethod());

            for (MethodEntry old : old_group.getMethods()) {
                MethodMatchEntry m = set.getMethodMatch(old);
                if (m != null) {
                    continue;
                }
                TypeEntry old_owner = set.getOldSourceSet().get(old.getOwnerName());
                MatchEntry type_match = set.getMatch(old_owner);
                if (type_match == null) {
                    continue;
                }
                MethodEntry n = new_group.getOverride(type_match.getNewType());
                if (n != null) {
                    set.vote(old, n);
                }
            }
        }
    }

} public class MatchReferences implements MergeOperation {

    private boolean prepared = false;
    public final Multimap<FieldEntry, MethodEntry> old_field_accesses = HashMultimap.create();
    public final Multimap<FieldEntry, MethodEntry> old_field_assignments = HashMultimap.create();
    public final Multimap<FieldEntry, MethodEntry> new_field_accesses = HashMultimap.create();
    public final Multimap<FieldEntry, MethodEntry> new_field_assignments = HashMultimap.create();

    public final Multimap<String, MethodEntry> old_ext_accesses = HashMultimap.create();
    public final Multimap<String, MethodEntry> old_ext_assignments = HashMultimap.create();
    public final Multimap<String, MethodEntry> new_ext_accesses = HashMultimap.create();
    public final Multimap<String, MethodEntry> new_ext_assignments = HashMultimap.create();

    public final Multimap<MethodEntry, MethodEntry> old_method_invokes = HashMultimap.create();
    public final Multimap<MethodEntry, MethodEntry> new_method_invokes = HashMultimap.create();

    public final Multimap<String, MethodEntry> old_ext_invokes = HashMultimap.create();
    public final Multimap<String, MethodEntry> new_ext_invokes = HashMultimap.create();

    public final Multimap<TypeEntry, MethodEntry> old_inits = HashMultimap.create();
    public final Multimap<TypeEntry, MethodEntry> new_inits = HashMultimap.create();
    public final Multimap<String, MethodEntry> old_ext_inits = HashMultimap.create();
    public final Multimap<String, MethodEntry> new_ext_inits = HashMultimap.create();

    public final Multimap<TypeEntry, MethodEntry> old_array_inits = HashMultimap.create();
    public final Multimap<TypeEntry, MethodEntry> new_array_inits = HashMultimap.create();
    public final Multimap<String, MethodEntry> old_ext_array_inits = HashMultimap.create();
    public final Multimap<String, MethodEntry> new_ext_array_inits = HashMultimap.create();

    public final Map<MethodEntry, TypeEntry> old_anons = new HashMap<>();
    public final Map<MethodEntry, TypeEntry> new_anons = new HashMap<>();

    private void prep(MergeEngine set) {
        RefFinder new_finder = new RefFinder(set.getNewSourceSet(), this.new_field_accesses, this.new_field_assignments,
                this.new_ext_accesses, this.new_ext_assignments, this.new_method_invokes, this.new_ext_invokes, this.new_inits, this.new_ext_inits,
                this.new_array_inits, this.new_ext_array_inits, this.new_anons);
        for (TypeEntry type : set.getNewSourceSet().getAllClasses()) {
            type.accept(new_finder);
        }
        RefFinder old_finder = new RefFinder(set.getOldSourceSet(), this.old_field_accesses, this.old_field_assignments,
                this.old_ext_accesses, this.old_ext_assignments, this.old_method_invokes, this.old_ext_invokes, this.old_inits, this.old_ext_inits,
                this.old_array_inits, this.old_ext_array_inits, this.old_anons);
        for (TypeEntry type : set.getOldSourceSet().getAllClasses()) {
            type.accept(old_finder);
        }
    }

    @Override
    public void operate(MergeEngine set) {
        if (!this.prepared) {
            this.prepared = true;
            prep(set);
        }

        for (String old_ext : this.old_ext_accesses.keySet()) {
            Collection<MethodEntry> old = this.old_ext_accesses.get(old_ext);
            Collection<MethodEntry> new_ = this.new_ext_accesses.get(old_ext);
            matchDiscreteByType(set, old, new_);
        }

        for (String old_ext : this.old_ext_assignments.keySet()) {
            Collection<MethodEntry> old = this.old_ext_assignments.get(old_ext);
            Collection<MethodEntry> new_ = this.new_ext_assignments.get(old_ext);
            matchDiscreteByType(set, old, new_);
        }

        for (String old_ext : this.old_ext_invokes.keySet()) {
            Collection<MethodEntry> old = this.old_ext_invokes.get(old_ext);
            Collection<MethodEntry> new_ = this.new_ext_invokes.get(old_ext);
            matchDiscreteByType(set, old, new_);
        }

        for (String old_ext : this.old_ext_inits.keySet()) {
            Collection<MethodEntry> old = this.old_ext_inits.get(old_ext);
            Collection<MethodEntry> new_ = this.new_ext_inits.get(old_ext);
            matchDiscreteByType(set, old, new_);
        }

        for (String old_ext : this.old_ext_array_inits.keySet()) {
            Collection<MethodEntry> old = this.old_ext_array_inits.get(old_ext);
            Collection<MethodEntry> new_ = this.new_ext_array_inits.get(old_ext);
            matchDiscreteByType(set, old, new_);
        }

        for (MatchEntry match : set.getAllMatches()) {
            {
                Collection<MethodEntry> old = this.old_inits.get(match.getOldType());
                Collection<MethodEntry> new_ = this.new_inits.get(match.getNewType());
                if (old != null && !old.isEmpty() && new_ != null && !new_.isEmpty()) {
                    matchDiscreteByType(set, old, new_);
                }
            }
            {
                Collection<MethodEntry> old = this.old_array_inits.get(match.getOldType());
                Collection<MethodEntry> new_ = this.new_array_inits.get(match.getNewType());
                if (old != null && !old.isEmpty() && new_ != null && !new_.isEmpty()) {
                    matchDiscreteByType(set, old, new_);
                }
            }
            for (FieldEntry fld : match.getOldType().getFields()) {
                FieldMatchEntry fld_match = set.getFieldMatch(fld);
                if (fld_match == null) {
                    continue;
                }
                FieldEntry n = fld_match.getNewField();

                matchDiscreteByType(set, this.old_field_accesses.get(fld), this.new_field_accesses.get(n));
                matchDiscreteByType(set, this.old_field_assignments.get(fld), this.new_field_assignments.get(n));
            }
            for (FieldEntry fld : match.getOldType().getStaticFields()) {
                FieldMatchEntry fld_match = set.getFieldMatch(fld);
                if (fld_match == null) {
                    continue;
                }
                FieldEntry n = fld_match.getNewField();

                matchDiscreteByType(set, this.old_field_accesses.get(fld), this.new_field_accesses.get(n));
                matchDiscreteByType(set, this.old_field_assignments.get(fld), this.new_field_assignments.get(n));
            }
            for (MethodEntry fld : match.getOldType().getMethods()) {
                MethodMatchEntry fld_match = set.getMethodMatch(fld);
                if (fld_match == null) {
                    continue;
                }
                MethodEntry n = fld_match.getNewMethod();

                TypeEntry oanon = this.old_anons.get(fld);
                TypeEntry nanon = this.new_anons.get(n);
                if (oanon != null && nanon != null) {
                    set.vote(oanon, nanon);
                }

                matchDiscreteByType(set, this.old_method_invokes.get(fld), this.new_method_invokes.get(n));
            }
            for (MethodEntry fld : match.getOldType().getStaticMethods()) {
                MethodMatchEntry fld_match = set.getMethodMatch(fld);
                if (fld_match == null) {
                    continue;
                }
                MethodEntry n = fld_match.getNewMethod();

                TypeEntry oanon = this.old_anons.get(fld);
                TypeEntry nanon = this.new_anons.get(n);
                if (oanon != null && nanon != null) {
                    set.vote(oanon, nanon);
                }

                matchDiscreteByType(set, this.old_method_invokes.get(fld), this.new_method_invokes.get(n));
            }
        }
    }

    private static void matchDiscreteByType(MergeEngine set, Collection<MethodEntry> old_accesses, Collection<MethodEntry> new_accesses) {
        Multimap<TypeEntry, MethodEntry> new_by_type = HashMultimap.create();
        for (MethodEntry mth : new_accesses) {
            TypeEntry mth_owner = set.getNewSourceSet().get(mth.getOwnerName());
            new_by_type.put(mth_owner, mth);
        }
        Multimap<TypeEntry, MethodEntry> old_by_type = HashMultimap.create();
        for (MethodEntry mth : old_accesses) {
            TypeEntry mth_owner = set.getOldSourceSet().get(mth.getOwnerName());
            old_by_type.put(mth_owner, mth);
        }

        if (new_by_type.size() == 1 && old_by_type.size() == 1) {
            TypeEntry old_type = old_by_type.keySet().iterator().next();
            TypeEntry new_type = new_by_type.keySet().iterator().next();
            set.vote(old_type, new_type);
        }

        for (TypeEntry old_type : old_by_type.keySet()) {
            Collection<MethodEntry> old_methods = old_by_type.get(old_type);
            MatchEntry type_match = set.getMatch(old_type);
            if (type_match == null) {
                continue;
            }
            TypeEntry new_type = type_match.getNewType();
            Collection<MethodEntry> new_methods = new_by_type.get(new_type);
            if (new_methods == null) {
                continue;
            }
            MatchDiscreteMethods.matchDiscrete(set, old_methods, new_methods);
        }
    }

    private static class RefFinder implements InstructionVisitor, StatementVisitor, TypeVisitor {

        private SourceSet set;
        private MethodEntry current_method;
        private final Multimap<FieldEntry, MethodEntry> field_accesses;
        private final Multimap<FieldEntry, MethodEntry> field_assignments;
        private final Multimap<String, MethodEntry> ext_accesses;
        private final Multimap<String, MethodEntry> ext_assignments;
        private final Multimap<MethodEntry, MethodEntry> method_invokes;
        private final Multimap<String, MethodEntry> ext_invokes;
        private final Multimap<TypeEntry, MethodEntry> inits;
        private final Multimap<String, MethodEntry> ext_inits;
        private final Multimap<TypeEntry, MethodEntry> array_inits;
        private final Multimap<String, MethodEntry> ext_array_inits;
        private final Map<MethodEntry, TypeEntry> anons;
        private boolean store_anons = false;

        public RefFinder(SourceSet set, Multimap<FieldEntry, MethodEntry> field_accesses, Multimap<FieldEntry, MethodEntry> field_assignments,
                Multimap<String, MethodEntry> ext_accesses, Multimap<String, MethodEntry> ext_assignments,
                Multimap<MethodEntry, MethodEntry> method_invokes, Multimap<String, MethodEntry> ext_invokes,
                Multimap<TypeEntry, MethodEntry> inits, Multimap<String, MethodEntry> ext_inits, Multimap<TypeEntry, MethodEntry> array_inits,
                Multimap<String, MethodEntry> ext_array_inits, Map<MethodEntry, TypeEntry> anons) {
            this.set = set;
            this.field_accesses = field_accesses;
            this.field_assignments = field_assignments;
            this.ext_accesses = ext_accesses;
            this.ext_assignments = ext_assignments;
            this.method_invokes = method_invokes;
            this.ext_invokes = ext_invokes;
            this.inits = inits;
            this.ext_inits = ext_inits;
            this.array_inits = array_inits;
            this.ext_array_inits = ext_array_inits;
            this.anons = anons;
        }

        @Override
        public void visitInstanceFieldAssignment(InstanceFieldAssignment stmt) {
            TypeEntry owner = this.set.get(stmt.getOwnerName());
            if (owner != null) {
                FieldEntry fld = MergeUtil.findField(owner, stmt.getFieldName());
                if (fld != null) {
                    this.field_assignments.put(fld, this.current_method);
                    return;
                }
            }
            this.ext_assignments.put(stmt.getOwnerType() + stmt.getOwnerName(), this.current_method);
        }

        @Override
        public void visitStaticFieldAssignment(StaticFieldAssignment stmt) {
            TypeEntry owner = this.set.get(stmt.getOwnerName());
            if (owner != null) {
                FieldEntry fld = MergeUtil.findStaticField(owner, stmt.getFieldName());
                if (fld != null) {
                    this.field_assignments.put(fld, this.current_method);
                    return;
                }
            }
            this.ext_assignments.put(stmt.getOwnerType() + stmt.getOwnerName(), this.current_method);
        }

        @Override
        public void visitInstanceFieldAccess(InstanceFieldAccess insn) {
            TypeEntry owner = this.set.get(insn.getOwnerName());
            if (owner != null) {
                FieldEntry fld = MergeUtil.findField(owner, insn.getFieldName());
                if (fld != null) {
                    this.field_accesses.put(fld, this.current_method);
                    return;
                }
            }
            this.ext_accesses.put(insn.getOwnerType() + insn.getOwnerName(), this.current_method);
        }

        @Override
        public void visitStaticFieldAccess(StaticFieldAccess insn) {
            TypeEntry owner = this.set.get(insn.getOwnerName());
            if (owner != null) {
                FieldEntry fld = MergeUtil.findStaticField(owner, insn.getFieldName());
                if (fld != null) {
                    this.field_accesses.put(fld, this.current_method);
                    return;
                }
            }
            this.ext_accesses.put(insn.getOwnerType() + insn.getOwnerName(), this.current_method);
        }

        @Override
        public void visitInstanceMethodInvoke(InstanceMethodInvoke insn) {
            TypeEntry owner = this.set.get(insn.getOwnerName());
            if (owner != null) {
                MethodEntry mth = MergeUtil.findMethod(owner, insn.getMethodName(), insn.getMethodDescription());
                if (mth != null) {
                    this.method_invokes.put(mth, this.current_method);
                    return;
                }
            }
            this.ext_invokes.put(insn.getOwner() + insn.getMethodName() + insn.getMethodDescription(), this.current_method);
        }

        @Override
        public void visitStaticMethodInvoke(StaticMethodInvoke insn) {
            TypeEntry owner = this.set.get(insn.getOwnerName());
            if (owner != null) {
                MethodEntry mth = MergeUtil.findStaticMethod(owner, insn.getMethodName(), insn.getMethodDescription());
                if (mth != null) {
                    this.method_invokes.put(mth, this.current_method);
                    return;
                }
            }
            this.ext_invokes.put(insn.getOwner() + insn.getMethodName() + insn.getMethodDescription(), this.current_method);
        }

        @Override
        public void visitNewArray(NewArray insn) {
            TypeEntry array = this.set.get(insn.getType().getName());
            if (array != null) {
                this.array_inits.put(array, this.current_method);
            } else {
                this.ext_array_inits.put(insn.getType().getName(), this.current_method);
            }
        }

        @Override
        public void visitMultiNewArray(MultiNewArray insn) {
            TypeEntry array = this.set.get(insn.getType().getName());
            if (array != null) {
                this.array_inits.put(array, this.current_method);
            } else {
                this.ext_array_inits.put(insn.getType().getName(), this.current_method);
            }
        }

        @Override
        public void visitNew(New insn) {
            TypeEntry type = this.set.get(insn.getType().getName());
            if (type != null) {
                this.inits.put(type, this.current_method);
                if (type.isAnonType()) {
                    if (this.store_anons) {
                        this.store_anons = false;
                        this.anons.put(this.current_method, type);
                    } else {
                        this.anons.remove(this.current_method);
                    }
                }
            } else {
                this.ext_inits.put(insn.getType().getName(), this.current_method);
            }
        }

        @Override
        public void visitMethod(MethodEntry mth) {
            this.current_method = mth;
            this.store_anons = true;
        }

        @Override
        public void visitMethodEnd() {
            this.current_method = null;
        }

        @Override
        public void visitStringConstant(StringConstant cst) {
        }

        @Override
        public void visitArrayAccess(ArrayAccess insn) {
        }

        @Override
        public void visitCast(Cast insn) {
        }

        @Override
        public void visitDoubleConstant(DoubleConstant insn) {
        }

        @Override
        public void visitDynamicInvoke(Lambda insn) {
        }

        @Override
        public void visitFloatConstant(FloatConstant insn) {
        }

        @Override
        public void visitInstanceOf(InstanceOf insn) {
        }

        @Override
        public void visitIntConstant(IntConstant insn) {
        }

        @Override
        public void visitLocalAccess(LocalAccess insn) {
        }

        @Override
        public void visitLocalInstance(LocalInstance local) {
        }

        @Override
        public void visitLongConstant(LongConstant insn) {
        }

        @Override
        public void visitNegativeOperator(NegativeOperator insn) {
        }

        @Override
        public void visitNullConstant(NullConstant insn) {
        }

        @Override
        public void visitNumberCompare(NumberCompare insn) {
        }

        @Override
        public void visitOperator(Operator insn) {
        }

        @Override
        public void visitTernary(Ternary insn) {
        }

        @Override
        public void visitTypeConstant(TypeConstant insn) {
        }

        @Override
        public void visitArrayAssignment(ArrayAssignment arrayAssign) {
        }

        @Override
        public void visitBreak(Break breakStatement) {
        }

        @Override
        public void visitCatchBlock(CatchBlock catchBlock) {
        }

        @Override
        public void visitComment(Comment comment) {
        }

        @Override
        public void visitDoWhile(DoWhile doWhileLoop) {
        }

        @Override
        public void visitElif(Elif elseBlock) {
        }

        @Override
        public void visitElse(Else elseBlock) {
        }

        @Override
        public void visitFor(For forLoop) {
        }

        @Override
        public void visitForEach(ForEach forLoop) {
        }

        @Override
        public void visitIf(If ifBlock) {
        }

        @Override
        public void visitIncrement(Increment incrementStatement) {
        }

        @Override
        public void visitInvoke(InvokeStatement stmt) {
        }

        @Override
        public void visitLocalAssignment(LocalAssignment localAssign) {
        }

        @Override
        public void visitReturn(Return returnValue) {
        }

        @Override
        public void visitSwitch(Switch tableSwitch) {
        }

        @Override
        public void visitSwitchCase(Case case1) {
        }

        @Override
        public void visitThrow(Throw throwException) {
        }

        @Override
        public void visitTryCatch(TryCatch tryBlock) {
        }

        @Override
        public void visitWhile(While whileLoop) {
        }

        @Override
        public void visitClassEntry(ClassEntry type) {
        }

        @Override
        public void visitEnumEntry(EnumEntry type) {
        }

        @Override
        public void visitInterfaceEntry(InterfaceEntry type) {
        }

        @Override
        public void visitAnnotationEntry(AnnotationEntry type) {
        }

        @Override
        public void visitAnnotation(Annotation annotation) {
        }

        @Override
        public void visitField(FieldEntry fld) {
        }

        @Override
        public void visitTypeEnd() {
        }

        @Override
        public void visitMethodReference(MethodReference methodReference) {
        }

    }

} public class MatchStringConstants implements MergeOperation {

    @Override
    public void operate(MergeEngine set) {
        Walker walker = new Walker();
        for (TypeEntry type : set.getOldSourceSet().getAllClasses()) {
            walker.setType(type);
            for (MethodEntry mth : type.getMethods()) {
                if (mth.getInstructions() != null) {
                    mth.getInstructions().accept(walker);
                }
            }
            for (MethodEntry mth : type.getStaticMethods()) {
                if (mth.getInstructions() != null) {
                    mth.getInstructions().accept(walker);
                }
            }
        }
        Map<String, TypeEntry> old_unique = walker.getUniqueStringConstants();
        walker = new Walker();
        for (TypeEntry type : set.getNewSourceSet().getAllClasses()) {
            walker.setType(type);
            for (MethodEntry mth : type.getMethods()) {
                if (mth.getInstructions() != null) {
                    mth.getInstructions().accept(walker);
                }
            }
            for (MethodEntry mth : type.getStaticMethods()) {
                if (mth.getInstructions() != null) {
                    mth.getInstructions().accept(walker);
                }
            }
        }
        Map<String, TypeEntry> new_unique = walker.getUniqueStringConstants();
        walker = null;
        for (Map.Entry<String, TypeEntry> e : old_unique.entrySet()) {
            TypeEntry old = e.getValue();
            TypeEntry n = new_unique.get(e.getKey());
            if (n != null) {
                int old_complexity = old.getFieldCount() + old.getStaticFieldCount() + old.getMethodCount() + old.getStaticMethodCount();
                int new_complexity = n.getFieldCount() + n.getStaticFieldCount() + n.getMethodCount() + n.getStaticMethodCount();
                if (new_complexity < old_complexity * 0.1 || old_complexity < new_complexity * 0.1) {
                    continue;
                }
                set.vote(old, n);
            }
        }
    }

    private static class Walker implements InstructionVisitor {

        private final Map<String, TypeEntry> unique = new HashMap<>();
        private final Set<String> non_unique = new HashSet<>();
        private TypeEntry current;

        public Walker() {

        }

        public Map<String, TypeEntry> getUniqueStringConstants() {
            return this.unique;
        }

        public void setType(TypeEntry cur) {
            this.current = cur;
        }

        @Override
        public void visitStringConstant(StringConstant cst) {
            if (this.non_unique.contains(cst.getConstant())) {
                return;
            }
            TypeEntry type = this.unique.get(cst.getConstant());
            if (type == null) {
                this.unique.put(cst.getConstant(), this.current);
            } else if (type != this.current) {
                this.non_unique.add(cst.getConstant());
                this.unique.remove(cst.getConstant());
            }
        }

        @Override
        public void visitArrayAccess(ArrayAccess insn) {
        }

        @Override
        public void visitCast(Cast insn) {
        }

        @Override
        public void visitDoubleConstant(DoubleConstant insn) {
        }

        @Override
        public void visitDynamicInvoke(Lambda insn) {
        }

        @Override
        public void visitFloatConstant(FloatConstant insn) {
        }

        @Override
        public void visitInstanceFieldAccess(InstanceFieldAccess insn) {
        }

        @Override
        public void visitInstanceMethodInvoke(InstanceMethodInvoke insn) {
        }

        @Override
        public void visitInstanceOf(InstanceOf insn) {
        }

        @Override
        public void visitIntConstant(IntConstant insn) {
        }

        @Override
        public void visitLocalAccess(LocalAccess insn) {
        }

        @Override
        public void visitLocalInstance(LocalInstance local) {
        }

        @Override
        public void visitLongConstant(LongConstant insn) {
        }

        @Override
        public void visitNegativeOperator(NegativeOperator insn) {
        }

        @Override
        public void visitNew(New insn) {
        }

        @Override
        public void visitNewArray(NewArray insn) {
        }

        @Override
        public void visitNullConstant(NullConstant insn) {
        }

        @Override
        public void visitNumberCompare(NumberCompare insn) {
        }

        @Override
        public void visitOperator(Operator insn) {
        }

        @Override
        public void visitStaticFieldAccess(StaticFieldAccess insn) {
        }

        @Override
        public void visitStaticMethodInvoke(StaticMethodInvoke insn) {
        }

        @Override
        public void visitTernary(Ternary insn) {
        }

        @Override
        public void visitTypeConstant(TypeConstant insn) {
        }

        @Override
        public void visitMultiNewArray(MultiNewArray insn) {
        }

        @Override
        public void visitMethodReference(MethodReference methodReference) {
        }

    }

} public class MergeInitializers implements MergeOperation {

    private Set<String> handled = new HashSet<>();

    @Override
    public void operate(MergeEngine set) {

        for (MatchEntry match : set.getAllMatches()) {
            if (match.getNewType() == null || this.handled.contains(match.getNewType().getName())) {
                continue;
            }
            this.handled.add(match.getNewType().getName());

            MethodEntry old_clinit = match.getOldType().getStaticMethod("<clinit>");
            MethodEntry new_clinit = match.getNewType().getStaticMethod("<clinit>");
            if (old_clinit != null && new_clinit != null) {
                MethodMatchEntry m = set.getPendingMethodMatch(old_clinit);
                m.setNewMethod(new_clinit);
                set.setAsMatched(m);
            }
        }
    }

} public class MergeMatchedFields implements MergeOperation {

    @Override
    public void operate(MergeEngine set) {

        for (FieldMatchEntry match : set.getAllFieldMatches()) {
            if (match.isMerged()) {
                continue;
            }
            MergeUtil.merge(set, match.getOldField(), match.getNewField());
            match.setMerged();
        }

    }

} public class MergeMatchedMethods implements MergeOperation {

    @Override
    public void operate(MergeEngine set) {

        for (MethodMatchEntry match : set.getAllMethodMatches()) {
            if (match.getNewMethod() == null || match.isMerged()) {
                continue;
            }
            MergeUtil.merge(set, match.getOldMethod(), match.getNewMethod());
            match.setMerged();
        }

    }

} public class MergeMatchedTypes implements MergeOperation {

    @Override
    public void operate(MergeEngine set) {

        for (MatchEntry match : set.getAllMatches()) {
            if (match.isMerged()) {
                continue;
            }
            match.setAsMerged();
            TypeEntry old = match.getOldType();
            TypeEntry new_ = match.getNewType();

            if (old instanceof ClassEntry) {
                TypeEntry old_super = set.getOldSourceSet().get(((ClassEntry) old).getSuperclassName());
                TypeEntry new_super = set.getNewSourceSet().get(((ClassEntry) new_).getSuperclassName());
                if (old_super != null && new_super != null) {
                    set.vote(old_super, new_super);
                }
            }

            {
                Map<TypeEntry, Integer> old_interface_complexities = new HashMap<>();
                for (String inter : old.getInterfaces()) {
                    TypeEntry intr = set.getOldSourceSet().get(inter);
                    if (intr == null) {
                        continue;
                    }
                    old_interface_complexities.put(intr, intr.getMethodCount());
                }
                Map<TypeEntry, Integer> new_interface_complexities = new HashMap<>();
                for (String inter : new_.getInterfaces()) {
                    TypeEntry intr = set.getNewSourceSet().get(inter);
                    if (intr == null) {
                        continue;
                    }
                    new_interface_complexities.put(intr, intr.getMethodCount());
                }

                for (TypeEntry old_inter : old_interface_complexities.keySet()) {
                    if (set.isTypeMatched(old_inter)) {
                        continue;
                    }
                    int cmp = old_interface_complexities.get(old_inter);
                    TypeEntry inter_match = null;
                    for (Map.Entry<TypeEntry, Integer> e : new_interface_complexities.entrySet()) {
                        if (set.isTypeMatched(e.getKey())) {
                            continue;
                        }
                        int delta = Math.abs(cmp - e.getValue());
                        if (delta <= 2) {
                            if (inter_match != null) {
                                inter_match = null;
                                break;
                            }
                            inter_match = e.getKey();
                        } else if (delta < 6) {
                            inter_match = null;
                            break;
                        }
                    }
                    if (inter_match != null) {
                        set.vote(old_inter, inter_match);
                    }
                }
            }

            {
                // This is looking for types (particularily functional
                // interfaces) that only have a single non-synthetic method as
                // we can safely match those together
                MethodEntry old_non_syn = null;
                for (MethodEntry mth : old.getMethods()) {
                    if (mth.isSynthetic()) {
                        continue;
                    }
                    if (old_non_syn != null) {
                        old_non_syn = null;
                        break;
                    }
                    old_non_syn = mth;
                }
                MethodEntry new_non_syn = null;
                for (MethodEntry mth : new_.getMethods()) {
                    if (mth.isSynthetic()) {
                        continue;
                    }
                    if (new_non_syn != null) {
                        new_non_syn = null;
                        break;
                    }
                    new_non_syn = mth;
                }

                if (old_non_syn != null && new_non_syn != null) {
                    MethodMatchEntry m = set.getPendingMethodMatch(old_non_syn);
                    m.setNewMethod(new_non_syn);
                    set.setAsMatched(m);
                }
            }

            {
                // this is finding unique string constants on a by-method basis
                // to attempt matching using them
                StringConstantMethodWalker walker = new StringConstantMethodWalker();
                old.accept(walker);
                Map<String, MethodEntry> old_unique = walker.getUnique();
                walker = new StringConstantMethodWalker();
                new_.accept(walker);
                Map<String, MethodEntry> new_unique = walker.getUnique();

                for (Map.Entry<String, MethodEntry> e : old_unique.entrySet()) {
                    MethodEntry n = new_unique.get(e.getKey());
                    if (n != null) {
                        set.vote(e.getValue(), n);
                    }
                }
            }

        }

    }

    private static final class StringConstantMethodWalker implements InstructionVisitor, TypeVisitor {

        private Map<String, MethodEntry> unique = new HashMap<>();
        private Set<String> non_unique = new HashSet<>();
        private MethodEntry current_method = null;

        public StringConstantMethodWalker() {

        }

        public Map<String, MethodEntry> getUnique() {
            return this.unique;
        }

        @Override
        public void visitClassEntry(ClassEntry type) {
        }

        @Override
        public void visitEnumEntry(EnumEntry type) {
        }

        @Override
        public void visitInterfaceEntry(InterfaceEntry type) {
        }

        @Override
        public void visitAnnotationEntry(AnnotationEntry type) {
        }

        @Override
        public void visitAnnotation(Annotation annotation) {
        }

        @Override
        public void visitMethod(MethodEntry mth) {
            this.current_method = mth;
        }

        @Override
        public void visitMethodEnd() {
            this.current_method = null;
        }

        @Override
        public void visitField(FieldEntry fld) {
        }

        @Override
        public void visitTypeEnd() {
        }

        @Override
        public void visitArrayAccess(ArrayAccess insn) {
        }

        @Override
        public void visitCast(Cast insn) {
        }

        @Override
        public void visitDoubleConstant(DoubleConstant insn) {
        }

        @Override
        public void visitDynamicInvoke(Lambda insn) {
        }

        @Override
        public void visitFloatConstant(FloatConstant insn) {
        }

        @Override
        public void visitInstanceFieldAccess(InstanceFieldAccess insn) {
        }

        @Override
        public void visitInstanceMethodInvoke(InstanceMethodInvoke insn) {
        }

        @Override
        public void visitInstanceOf(InstanceOf insn) {
        }

        @Override
        public void visitIntConstant(IntConstant insn) {
        }

        @Override
        public void visitLocalAccess(LocalAccess insn) {
        }

        @Override
        public void visitLocalInstance(LocalInstance local) {
        }

        @Override
        public void visitLongConstant(LongConstant insn) {
        }

        @Override
        public void visitMultiNewArray(MultiNewArray insn) {
        }

        @Override
        public void visitNegativeOperator(NegativeOperator insn) {
        }

        @Override
        public void visitNew(New insn) {
        }

        @Override
        public void visitNewArray(NewArray insn) {
        }

        @Override
        public void visitNullConstant(NullConstant insn) {
        }

        @Override
        public void visitNumberCompare(NumberCompare insn) {
        }

        @Override
        public void visitOperator(Operator insn) {
        }

        @Override
        public void visitStaticFieldAccess(StaticFieldAccess insn) {
        }

        @Override
        public void visitStaticMethodInvoke(StaticMethodInvoke insn) {
        }

        @Override
        public void visitStringConstant(StringConstant cst) {
            if (this.non_unique.contains(cst.getConstant())) {
                return;
            }
            MethodEntry mth = this.unique.get(cst.getConstant());
            if (mth == null) {
                this.unique.put(cst.getConstant(), this.current_method);
            } else if (mth != this.current_method) {
                this.non_unique.add(cst.getConstant());
                this.unique.remove(cst.getConstant());
            }
        }

        @Override
        public void visitTernary(Ternary insn) {
        }

        @Override
        public void visitTypeConstant(TypeConstant insn) {
        }

        @Override
        public void visitMethodReference(MethodReference methodReference) {
        }

    }

} public class MergeSyntheticOverloads implements MergeOperation {

    @Override
    public void operate(MergeEngine set) {
        for (TypeEntry type : set.getNewSourceSet().getAllClasses()) {
            if (type instanceof EnumEntry) {
                if (!type.isAnonType()) {
                    MethodEntry clinit = type.getStaticMethod("<clinit>");
                    if (clinit != null && clinit.getInstructions() != null) {
                        for (Statement stmt : clinit.getInstructions()) {
                            if (stmt instanceof StaticFieldAssignment) {
                                StaticFieldAssignment assign = (StaticFieldAssignment) stmt;
                                if (!assign.getOwnerName().equals(type.getName())) {
                                    continue;
                                }
                                if (assign.getValue() instanceof NewArray) {
                                    FieldEntry fld = type.getStaticField(assign.getFieldName());
                                    if (fld != null && fld.getType().getDescriptor().startsWith("[")) {
                                        FieldEntry dummy =
                                                MergeEngine.createDummyField(set.getOldSourceSet(), "$VALUES", fld.getType(), type.getName());
                                        FieldMatchEntry match = set.getPendingFieldMatch(dummy);
                                        match.setNewField(fld);
                                        set.setAsMatched(match);
                                        set.incrementChangeCount();
                                        break;
                                    }
                                }
                            }
                        }
                    }
                }
            }
            for (MethodEntry mth : type.getMethods()) {
                if (!mth.isSynthetic() || mth.getInstructions() == null || mth.getName().length() <= 2) {
                    continue;
                }
                StatementBlock insns = mth.getInstructions();
                MethodEntry target = null;
                if (mth.getReturnType() == VoidTypeSignature.VOID) {
                    if (insns.getStatementCount() != 2) {
                        continue;
                    }
                    Statement a = insns.getStatement(0);
                    if (!(a instanceof InvokeStatement) || !(((InvokeStatement) a).getInstruction() instanceof InstanceMethodInvoke)) {
                        continue;
                    }
                    InstanceMethodInvoke invoke = (InstanceMethodInvoke) ((InvokeStatement) a).getInstruction();
                    if (!invoke.getOwnerName().equals(type.getName())) {
                        continue;
                    }
                    target = type.getMethod(invoke.getMethodName(), invoke.getMethodDescription());
                } else {
                    if (insns.getStatementCount() != 1) {
                        continue;
                    }
                    Statement a = insns.getStatement(0);
                    if (!(a instanceof Return) || !(((Return) a).getValue().get() instanceof InstanceMethodInvoke)) {
                        continue;
                    }
                    InstanceMethodInvoke invoke = (InstanceMethodInvoke) ((Return) a).getValue().get();
                    if (!invoke.getOwnerName().equals(type.getName())) {
                        continue;
                    }
                    target = type.getMethod(invoke.getMethodName(), invoke.getMethodDescription());
                }
                if (target == null) {
                    continue;
                }
                MethodEntry dummy = MergeEngine.createDummyMethod(set.getOldSourceSet(), mth.getName(), target.getDescription(), type.getName());
                MethodMatchEntry match = set.getPendingMethodMatch(dummy);
                match.setNewMethod(target);
                set.setAsMatched(match);
                set.incrementChangeCount();
            }

        }

    }

} public class VoteCollector implements MergeOperation {

    @Override
    public void operate(MergeEngine set) {
        List<MatchEntry> matches = new ArrayList<>(set.getPendingMatches());
        int target_type_count = Math.min(matches.size(), Math.max(20, set.getPendingMatches().size() / 10));
        Collections.sort(matches, (a, b) -> b.getVoteDifference() - a.getVoteDifference());
        for (int i = 0; i < target_type_count; i++) {
            MatchEntry m = matches.get(i);
            if (m.getHighest() == null) {
                continue;
            }
            m.setNewType(m.getHighest());
            set.setAsMatched(m);
            set.incrementChangeCount();
            cleanup(set, m.getOldType(), m.getNewType());
        }

        List<MethodMatchEntry> method_matches = new ArrayList<>(set.getPendingMethodMatches());
        int target_method_count = Math.min(method_matches.size(), Math.max(50, set.getPendingMethodMatches().size() / 10));
        Collections.sort(method_matches, (a, b) -> b.getVoteDifference() - a.getVoteDifference());
        for (int i = 0; i < target_method_count; i++) {
            MethodMatchEntry m = method_matches.get(i);
            if (m.getHighest() == null) {
                continue;
            }
            TypeEntry old_owner = set.getOldSourceSet().get(m.getOldMethod().getOwnerName());
            TypeEntry new_owner = set.getNewSourceSet().get(m.getHighest().getOwnerName());
            MatchEntry owner_match = set.getPendingMatch(old_owner);
            if (owner_match.getNewType() == null) {
                if (set.isTypeMatched(new_owner)) {
                    continue;
                }
                owner_match.setNewType(new_owner);
                set.setAsMatched(owner_match);
                set.incrementChangeCount();
                cleanup(set, old_owner, new_owner);
            }
            m.setNewMethod(m.getHighest());
            set.setAsMatched(m);
            set.incrementChangeCount();
        }

        List<FieldMatchEntry> field_matches = new ArrayList<>(set.getPendingFieldMatches());
        int target_field_count = Math.min(field_matches.size(), Math.max(20, set.getPendingFieldMatches().size() / 10));
        Collections.sort(field_matches, (a, b) -> b.getVoteDifference() - a.getVoteDifference());
        for (int i = 0; i < target_field_count; i++) {
            FieldMatchEntry m = field_matches.get(i);
            if (m.getHighest() == null) {
                continue;
            }
            TypeEntry old_owner = set.getOldSourceSet().get(m.getOldField().getOwnerName());
            TypeEntry new_owner = set.getNewSourceSet().get(m.getHighest().getOwnerName());
            MatchEntry owner_match = set.getPendingMatch(old_owner);
            if (owner_match.getNewType() == null) {
                if (set.isTypeMatched(new_owner)) {
                    continue;
                }
                owner_match.setNewType(new_owner);
                set.setAsMatched(owner_match);
                set.incrementChangeCount();
                cleanup(set, old_owner, new_owner);
            }
            m.setNewField(m.getHighest());
            set.setAsMatched(m);
            set.incrementChangeCount();
        }
    }

    private void cleanup(MergeEngine set, TypeEntry type, TypeEntry new_type) {
        for (MethodEntry mth : type.getMethods()) {
            MethodMatchEntry mth_match = set.getPendingMethodMatch(mth);
            if (mth_match.getNewMethod() == null) {
                List<MethodEntry> to_remove = new ArrayList<>();
                for (Map.Entry<MethodEntry, Integer> vote : mth_match.getVotes().entrySet()) {
                    if (!vote.getKey().getOwnerName().equals(new_type.getName())) {
                        to_remove.add(vote.getKey());
                    }
                }
                for (MethodEntry m : to_remove) {
                    mth_match.removeVote(m);
                }
            }
            mth_match.setOwnerMatch(new_type);
        }
        for (MethodEntry mth : type.getStaticMethods()) {
            MethodMatchEntry mth_match = set.getPendingMethodMatch(mth);
            if (mth_match.getNewMethod() == null) {
                List<MethodEntry> to_remove = new ArrayList<>();
                for (Map.Entry<MethodEntry, Integer> vote : mth_match.getVotes().entrySet()) {
                    if (!vote.getKey().getOwnerName().equals(new_type.getName())) {
                        to_remove.add(vote.getKey());
                    }
                }
                for (MethodEntry m : to_remove) {
                    mth_match.removeVote(m);
                }
            }
            mth_match.setOwnerMatch(new_type);
        }
        for (FieldEntry fld : type.getFields()) {
            FieldMatchEntry fld_match = set.getPendingFieldMatch(fld);
            if (fld_match.getNewField() == null) {
                List<FieldEntry> to_remove = new ArrayList<>();
                for (Map.Entry<FieldEntry, Integer> vote : fld_match.getVotes().entrySet()) {
                    if (!vote.getKey().getOwnerName().equals(new_type.getName())) {
                        to_remove.add(vote.getKey());
                    }
                }
                for (FieldEntry m : to_remove) {
                    fld_match.removeVote(m);
                }
            }
            fld_match.setOwnerMatch(new_type);
        }
        for (FieldEntry fld : type.getStaticFields()) {
            FieldMatchEntry fld_match = set.getPendingFieldMatch(fld);
            if (fld_match.getNewField() == null) {
                List<FieldEntry> to_remove = new ArrayList<>();
                for (Map.Entry<FieldEntry, Integer> vote : fld_match.getVotes().entrySet()) {
                    if (!vote.getKey().getOwnerName().equals(new_type.getName())) {
                        to_remove.add(vote.getKey());
                    }
                }
                for (FieldEntry m : to_remove) {
                    fld_match.removeVote(m);
                }
            }
            fld_match.setOwnerMatch(new_type);
        }
    }

}
import org.spongepowered.despector.ast.SourceSet;
import org.spongepowered.despector.config.LibraryConfiguration;
import org.spongepowered.despector.decompiler.BaseDecompiler;
import org.spongepowered.despector.decompiler.Decompilers;
import org.spongepowered.despector.decompiler.DirectoryWalker;
import org.spongepowered.despector.util.TypeHelper;
import org.spongepowered.despector.util.serialization.AstLoader;
import org.spongepowered.despector.util.serialization.MessagePacker;
import org.spongepowered.obfuscation.config.ObfConfigManager;
import org.spongepowered.obfuscation.data.MappingUsageFinder;
import org.spongepowered.obfuscation.data.MappingsIO;
import org.spongepowered.obfuscation.data.MappingsSet;
import org.spongepowered.obfuscation.data.MappingsSet.MethodMapping;
import org.spongepowered.obfuscation.data.UnknownMemberMapper;
import org.spongepowered.obfuscation.data.UnknownTypeMapper;
import org.spongepowered.obfuscation.merge.MergeEngine;
import org.spongepowered.obfuscation.merge.operation.CustomMethodMergers;
import org.spongepowered.obfuscation.merge.operation.MatchDiscreteFields;
import org.spongepowered.obfuscation.merge.operation.MatchDiscreteMethods;
import org.spongepowered.obfuscation.merge.operation.MatchEnums;
import org.spongepowered.obfuscation.merge.operation.MatchInnerClasses;
import org.spongepowered.obfuscation.merge.operation.MatchMethodGroups;
import org.spongepowered.obfuscation.merge.operation.MatchReferences;
import org.spongepowered.obfuscation.merge.operation.MatchStringConstants;
import org.spongepowered.obfuscation.merge.operation.MergeInitializers;
import org.spongepowered.obfuscation.merge.operation.MergeMatchedFields;
import org.spongepowered.obfuscation.merge.operation.MergeMatchedMethods;
import org.spongepowered.obfuscation.merge.operation.MergeMatchedTypes;
import org.spongepowered.obfuscation.merge.operation.MergeSyntheticOverloads;
import org.spongepowered.obfuscation.merge.operation.VoteCollector;
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
            System.out.println("Missing some args");
            System.out.println("Usage: java -jar ObfuscationMapper.jar old.jar old_mappings_dir new.jar");
            return;
        }
        if (output_mappings == null) {
            output_mappings = "output.srg";
        }

        Path root = Paths.get("");
        Path old_mappings_root = root.resolve(old_mappings_dir);

        if (!Files.exists(old_mappings_root)) {
            System.err.println("Unknown mappings: " + old_mappings_root.toAbsolutePath().toString());
            return;
        }

        MappingsSet old_mappings = MappingsIO.load(old_mappings_root);
        MappingsSet new_mappings = null;
        if (seed_mappings != null) {
            Path seed = root.resolve(seed_mappings);
            if (!Files.exists(seed)) {
                System.err.println("Unknown seed mappings: " + seed.toAbsolutePath().toString());
                return;
            }
            new_mappings = MappingsIO.load(seed);
        } else {
            new_mappings = new MappingsSet();
        }

        MappingsSet validation = null;
        if (validation_mappings != null) {
            Path validation_mappings_path = root.resolve(validation_mappings);
            if (!Files.exists(validation_mappings_path)) {
                System.err.println("Validation mappings " + validation_mappings + " not found");
            } else {
                System.out.println("Loading validation mappings");
                validation = MappingsIO.load(validation_mappings_path);
            }
        }

        MappingsSet previous = null;
        if (prev_mappings != null) {
            Path prev_mappings_path = root.resolve(prev_mappings);
            if (!Files.exists(prev_mappings_path)) {
                System.err.println("Previous mappings " + prev_mappings + " not found");
            } else {
                System.out.println("Loading previous mappings");
                previous = MappingsIO.load(prev_mappings_path);
            }
        }

        SourceSet old_sourceset = new SourceSet();
        SourceSet new_sourceset = new SourceSet();

        BaseDecompiler decompiler = Decompilers.JAVA;

        Path old_serialized = root.resolve(old_jar.replace('/', '_').replace('\\', '_') + ".ast");
        if (is_cached && Files.exists(old_serialized)) {
            long start = System.nanoTime();
            AstLoader.loadSources(old_sourceset, new BufferedInputStream(new FileInputStream(old_serialized.toFile())));
            long end = System.nanoTime();
            System.out.println("Loaded cached ast with " + old_sourceset.getAllClasses().size() + " classes from the older version");
            System.out.println("Loaded in " + ((end - start) / 1000000) + "ms");
        } else {
            long start = System.nanoTime();
            Path old_jar_path = root.resolve(old_jar);
            DirectoryWalker walker = new DirectoryWalker(old_jar_path);
            walker.walk(old_sourceset, decompiler);
            decompiler.flushTasks();
            long end = System.nanoTime();
            System.out.println("Loaded and decompiled " + old_sourceset.getAllClasses().size() + " classes from the older version");
            System.out.println("Loaded in " + ((end - start) / 1000000) + "ms");
            int failed = LibraryConfiguration.failed_method_count;
            int total = LibraryConfiguration.total_method_count;
            System.out.println("Failed to decompile " + failed + "/" + total + " (" + ((failed / (float) total) * 100) + "%)");
            LibraryConfiguration.failed_method_count = 0;
            LibraryConfiguration.total_method_count = 0;
            if (is_cached) {
                try (MessagePacker packer = new MessagePacker(new FileOutputStream(old_serialized.toFile()))) {
                    old_sourceset.writeTo(packer);
                }
            }
        }
        Path new_serialized = root.resolve(new_jar.replace('/', '_').replace('\\', '_') + ".ast");
        if (is_cached && Files.exists(new_serialized)) {
            long start = System.nanoTime();
            AstLoader.loadSources(new_sourceset, new BufferedInputStream(new FileInputStream(new_serialized.toFile())));
            long end = System.nanoTime();
            System.out.println("Loaded cached ast with " + new_sourceset.getAllClasses().size() + " classes from the newer version");
            System.out.println("Loaded in " + ((end - start) / 1000000) + "ms");
        } else {
            long start = System.nanoTime();
            Path new_jar_path = root.resolve(new_jar);
            DirectoryWalker walker = new DirectoryWalker(new_jar_path);
            walker.walk(new_sourceset, decompiler);
            decompiler.flushTasks();
            long end = System.nanoTime();
            System.out.println("Loaded and decompiled " + new_sourceset.getAllClasses().size() + " classes from the newer version");
            System.out.println("Loaded in " + ((end - start) / 1000000) + "ms");
            int failed = LibraryConfiguration.failed_method_count;
            int total = LibraryConfiguration.total_method_count;
            System.out.println("Failed to decompile " + failed + "/" + total + " (" + ((failed / (float) total) * 100) + "%)");
            LibraryConfiguration.failed_method_count = 0;
            LibraryConfiguration.total_method_count = 0;
            if (is_cached) {
                try (MessagePacker packer = new MessagePacker(new FileOutputStream(new_serialized.toFile()))) {
                    new_sourceset.writeTo(packer);
                }
            }
        }

        MergeEngine engine = new MergeEngine(old_sourceset, old_mappings, new_sourceset, new_mappings);

        engine.addOperation(new MergeSyntheticOverloads());
        engine.addOperation(new MatchStringConstants());
        engine.addOperation(new MatchEnums());
        engine.addOperation(new MergeInitializers());
        engine.addOperation(new MatchReferences());
        engine.addOperation(new MatchDiscreteFields());
        engine.addOperation(new MatchMethodGroups());
        engine.addOperation(new MatchDiscreteMethods());