public enum NameType {
	PLAIN(true, false, false, 0),
	MAPPED(false, true, false, 0),
	AUX(false, false, false, 1),
	AUX2(false, false, false, 2),

	MAPPED_PLAIN(true, true, false, 0),
	MAPPED_AUX_PLAIN(true, true, false, 1),
	MAPPED_AUX2_PLAIN(true, true, false, 2),
	MAPPED_TMP_PLAIN(true, true, true, 0),
	MAPPED_LOCTMP_PLAIN(true, true, false, 0),

	UID_PLAIN(true, false, false, 0),
	TMP_PLAIN(true, false, true, 0),
	LOCTMP_PLAIN(true, false, false, 0),
	AUX_PLAIN(true, false, false, 1),
	AUX2_PLAIN(true, false, false, 2);

	NameType(boolean plain, boolean mapped, boolean tmp, int aux) {
		this.plain = plain;
		this.mapped = mapped;
		this.tmp = tmp;
		this.aux = aux;
	}

	public NameType withPlain(boolean value) {
		if (plain == value) return this;

		if (value) {
			if (mapped) return MAPPED_PLAIN;

			return VALUES[AUX_PLAIN.ordinal() + aux - 1];
		} else if (this == MAPPED_PLAIN) {
			return MAPPED;
		} else if (aux > 0 && !mapped && !tmp) {
			return VALUES[AUX.ordinal() + aux - 1];
		} else {
			throw new UnsupportedOperationException();
		}
	}

	public NameType withMapped(boolean value) {
		if (mapped == value) return this;

		if (value) {
			if (aux > 0) return VALUES[MAPPED_AUX_PLAIN.ordinal() + aux - 1];
			if (tmp) return MAPPED_TMP_PLAIN;
			if (plain) return MAPPED_PLAIN;

			return MAPPED;
		} else {
			if (aux > 0) return VALUES[(plain ? AUX_PLAIN : AUX).ordinal() + aux - 1];
			if (tmp) return TMP_PLAIN;
			if (this == MAPPED_LOCTMP_PLAIN) return LOCTMP_PLAIN;

			return PLAIN;
		}
	}

	public NameType withAux(int index, boolean value) {
		if ((aux - 1 == index) == value) return this;

		if (value) {
			if (mapped) return VALUES[MAPPED_AUX_PLAIN.ordinal() + index];
			if (plain) return VALUES[AUX_PLAIN.ordinal() + index];

			return VALUES[AUX.ordinal() + index];
		} else {
			if (mapped) return MAPPED_PLAIN;

			return PLAIN;
		}
	}

	public NameType withTmp(boolean value) {
		if (tmp == value) return this;

		if (value) {
			if (mapped) return MAPPED_TMP_PLAIN;

			return TMP_PLAIN;
		} else {
			if (mapped) return MAPPED_PLAIN;

			return PLAIN;
		}
	}

	// transform between tmp <-> loctmp
	public NameType withUnmatchedTmp(boolean value) {
		boolean locTmp = this == MAPPED_LOCTMP_PLAIN || this == LOCTMP_PLAIN;

		if (value == locTmp || !tmp && !locTmp) return this;

		if (value) {
			if (mapped) return MAPPED_LOCTMP_PLAIN;

			return LOCTMP_PLAIN;
		} else {
			if (mapped) return MAPPED_TMP_PLAIN;

			return TMP_PLAIN;
		}
	}

	public boolean isAux() {
		return aux > 0;
	}

	public int getAuxIndex() {
		if (aux == 0) throw new NoSuchElementException();

		return aux - 1;
	}

	public static NameType getAux(int index) {
		Objects.checkIndex(index, AUX_COUNT);

		return VALUES[NameType.AUX.ordinal() + index];
	}

	public static final int AUX_COUNT = 2;

	private static final NameType[] VALUES = values();

	public final boolean plain;
	public final boolean mapped;
	public final boolean tmp;
	private final int aux;
} public class ClassClassifier {
	public static void init() {
		addClassifier(classTypeCheck, 20);
		addClassifier(signature, 5);
		addClassifier(hierarchyDepth, 1);
		addClassifier(parentClass, 4);
		addClassifier(childClasses, 3);
		addClassifier(interfaces, 3);
		addClassifier(implementers, 2);
		addClassifier(outerClass, 6);
		addClassifier(innerClasses, 5);
		addClassifier(methodCount, 3);
		addClassifier(fieldCount, 3);
		addClassifier(hierarchySiblings, 2);
		addClassifier(similarMethods, 10);
		addClassifier(outReferences, 6);
		addClassifier(inReferences, 6);
		addClassifier(stringConstants, 8);
		addClassifier(numericConstants, 6);
		addClassifier(methodOutReferences, 5, ClassifierLevel.Intermediate, ClassifierLevel.Full, ClassifierLevel.Extra);
		addClassifier(methodInReferences, 6, ClassifierLevel.Intermediate, ClassifierLevel.Full, ClassifierLevel.Extra);
		addClassifier(fieldReadReferences, 5, ClassifierLevel.Intermediate, ClassifierLevel.Full, ClassifierLevel.Extra);
		addClassifier(fieldWriteReferences, 5, ClassifierLevel.Intermediate, ClassifierLevel.Full, ClassifierLevel.Extra);
		addClassifier(membersFull, 10, ClassifierLevel.Full, ClassifierLevel.Extra);
		addClassifier(inRefsBci, 6, ClassifierLevel.Extra);
	}

	public static void addClassifier(AbstractClassifier classifier, double weight, ClassifierLevel... levels) {
		if (levels.length == 0) levels = ClassifierLevel.ALL;

		classifier.weight = weight;

		for (ClassifierLevel level : levels) {
			classifiers.computeIfAbsent(level, ignore -> new ArrayList<>()).add(classifier);
			maxScore.put(level, getMaxScore(level) + weight);
		}
	}

	public static double getMaxScore(ClassifierLevel level) {
		return maxScore.getOrDefault(level, 0.);
	}

	public static List<RankResult<ClassInstance>> rank(ClassInstance src, ClassInstance[] dsts, ClassifierLevel level, ClassEnvironment env, double maxMismatch) {
		return ClassifierUtil.rank(src, dsts, classifiers.getOrDefault(level, Collections.emptyList()), ClassifierUtil::checkPotentialEquality, env, maxMismatch);
	}

	public static List<RankResult<ClassInstance>> rankParallel(ClassInstance src, ClassInstance[] dsts, ClassifierLevel level, ClassEnvironment env, double maxMismatch) {
		return ClassifierUtil.rankParallel(src, dsts, classifiers.getOrDefault(level, Collections.emptyList()), ClassifierUtil::checkPotentialEquality, env, maxMismatch);
	}

	private static final Map<ClassifierLevel, List<IClassifier<ClassInstance>>> classifiers = new EnumMap<>(ClassifierLevel.class);
	private static final Map<ClassifierLevel, Double> maxScore = new EnumMap<>(ClassifierLevel.class);

	private static AbstractClassifier classTypeCheck = new AbstractClassifier("class type check") {
		@Override
		public double getScore(ClassInstance clsA, ClassInstance clsB, ClassEnvironment env) {
			int mask = Opcodes.ACC_ENUM | Opcodes.ACC_INTERFACE | Opcodes.ACC_ANNOTATION | Opcodes.ACC_RECORD | Opcodes.ACC_ABSTRACT;
			int resultA = clsA.getAccess() & mask;
			int resultB = clsB.getAccess() & mask;

			//assert Integer.bitCount(resultA) <= 3 && Integer.bitCount(resultB) <= 3;

			return 1 - Integer.bitCount(resultA ^ resultB) / 5.;
		}
	};

	private static AbstractClassifier signature = new AbstractClassifier("signature") {
		@Override
		public double getScore(ClassInstance clsA, ClassInstance clsB, ClassEnvironment env) {
			ClassSignature sigA = clsA.getSignature();
			ClassSignature sigB = clsB.getSignature();

			if (sigA == null && sigB == null) return 1;
			if (sigA == null || sigB == null) return 0;

			return sigA.isPotentiallyEqual(sigB) ? 1 : 0;
		}
	};

	private static AbstractClassifier hierarchyDepth = new AbstractClassifier("hierarchy depth") {
		@Override
		public double getScore(ClassInstance clsA, ClassInstance clsB, ClassEnvironment env) {
			int countA = 0;
			int countB = 0;

			while (clsA.getSuperClass() != null) {
				clsA = clsA.getSuperClass();
				countA++;
			}

			while (clsB.getSuperClass() != null) {
				clsB = clsB.getSuperClass();
				countB++;
			}

			return ClassifierUtil.compareCounts(countA, countB);
		}
	};


	private static AbstractClassifier hierarchySiblings = new AbstractClassifier("hierarchy siblings") {
		@Override
		public double getScore(ClassInstance clsA, ClassInstance clsB, ClassEnvironment env) {
			return ClassifierUtil.compareCounts(clsA.getSuperClass().getChildClasses().size(), clsB.getSuperClass().getChildClasses().size());
		}
	};

	private static AbstractClassifier parentClass = new AbstractClassifier("parent class") {
		@Override
		public double getScore(ClassInstance clsA, ClassInstance clsB, ClassEnvironment env) {
			if (clsA.getSuperClass() == null && clsB.getSuperClass() == null) return 1;
			if (clsA.getSuperClass() == null || clsB.getSuperClass() == null) return 0;

			return ClassifierUtil.checkPotentialEquality(clsA.getSuperClass(), clsB.getSuperClass()) ? 1 : 0;
		}
	};

	private static AbstractClassifier childClasses = new AbstractClassifier("child classes") {
		@Override
		public double getScore(ClassInstance clsA, ClassInstance clsB, ClassEnvironment env) {
			return ClassifierUtil.compareClassSets(clsA.getChildClasses(), clsB.getChildClasses(), true);
		}
	};

	private static AbstractClassifier interfaces = new AbstractClassifier("interfaces") {
		@Override
		public double getScore(ClassInstance clsA, ClassInstance clsB, ClassEnvironment env) {
			return ClassifierUtil.compareClassSets(clsA.getInterfaces(), clsB.getInterfaces(), true);
		}
	};

	private static AbstractClassifier implementers = new AbstractClassifier("implementers") {
		@Override
		public double getScore(ClassInstance clsA, ClassInstance clsB, ClassEnvironment env) {
			return ClassifierUtil.compareClassSets(clsA.getImplementers(), clsB.getImplementers(), true);
		}
	};

	private static AbstractClassifier outerClass = new AbstractClassifier("outer class") {
		@Override
		public double getScore(ClassInstance clsA, ClassInstance clsB, ClassEnvironment env) {
			ClassInstance outerA = clsA.getOuterClass();
			ClassInstance outerB = clsB.getOuterClass();

			if (outerA == null && outerB == null) return 1;
			if (outerA == null || outerB == null) return 0;

			return ClassifierUtil.checkPotentialEquality(outerA, outerB) ? 1 : 0;
		}
	};

	private static AbstractClassifier innerClasses = new AbstractClassifier("inner classes") {
		@Override
		public double getScore(ClassInstance clsA, ClassInstance clsB, ClassEnvironment env) {
			Set<ClassInstance> innerA = clsA.getInnerClasses();
			Set<ClassInstance> innerB = clsB.getInnerClasses();

			if (innerA.isEmpty() && innerB.isEmpty()) return 1;
			if (innerA.isEmpty() || innerB.isEmpty()) return 0;

			return ClassifierUtil.compareClassSets(innerA, innerB, true);
		}
	};

	private static AbstractClassifier methodCount = new AbstractClassifier("method count") {
		@Override
		public double getScore(ClassInstance clsA, ClassInstance clsB, ClassEnvironment env) {
			return ClassifierUtil.compareCounts(clsA.getMethods().length, clsB.getMethods().length);
		}
	};

	private static AbstractClassifier fieldCount = new AbstractClassifier("field count") {
		@Override
		public double getScore(ClassInstance clsA, ClassInstance clsB, ClassEnvironment env) {
			return ClassifierUtil.compareCounts(clsA.getFields().length, clsB.getFields().length);
		}
	};

	private static AbstractClassifier similarMethods = new AbstractClassifier("similar methods") {
		@Override
		public double getScore(ClassInstance clsA, ClassInstance clsB, ClassEnvironment env) {
			if (clsA.getMethods().length == 0 && clsB.getMethods().length == 0) return 1;
			if (clsA.getMethods().length == 0 || clsB.getMethods().length == 0) return 0;

			Set<MethodInstance> methodsB = Util.newIdentityHashSet(Arrays.asList(clsB.getMethods()));
			double totalScore = 0;
			MethodInstance bestMatch = null;
			double bestScore = 0;

			for (MethodInstance methodA : clsA.getMethods()) {
				{
					mBLoop: for (MethodInstance methodB : methodsB) {
						if (!ClassifierUtil.checkPotentialEquality(methodA, methodB)) continue;
						if (!ClassifierUtil.checkPotentialEquality(methodA.getRetType(), methodB.getRetType())) continue;

						MethodVarInstance[] argsA = methodA.getArgs();
						MethodVarInstance[] argsB = methodB.getArgs();
						if (argsA.length != argsB.length) continue;

						for (int i = 0; i < argsA.length; i++) {
							ClassInstance argA = argsA[i].getType();
							ClassInstance argB = argsB[i].getType();

							if (!ClassifierUtil.checkPotentialEquality(argA, argB)) {
								continue mBLoop;
							}
						}

						MethodNode asmNodeA = methodA.getAsmNode();
						MethodNode asmNodeB = methodA.getAsmNode();
						double score;

						if (asmNodeA == null || asmNodeB == null) {
							score = asmNodeA == null && asmNodeB == null ? 1 : 0;
						} else {
							score = ClassifierUtil.compareCounts(asmNodeA.instructions.size(), asmNodeB.instructions.size());
						}

						if (score > bestScore) {
							bestScore = score;
							bestMatch = methodB;
						}
					}
				}

				if (bestMatch != null) {
					totalScore += bestScore;
					methodsB.remove(bestMatch);
				}
			}

			return totalScore / Math.max(clsA.getMethods().length, clsB.getMethods().length);
		}
	};

	private static AbstractClassifier outReferences = new AbstractClassifier("out references") {
		@Override
		public double getScore(ClassInstance clsA, ClassInstance clsB, ClassEnvironment env) {
			Set<ClassInstance> refsA = getOutRefs(clsA);
			Set<ClassInstance> refsB = getOutRefs(clsB);

			return ClassifierUtil.compareClassSets(refsA, refsB, false);
		}
	};

	private static Set<ClassInstance> getOutRefs(ClassInstance cls) {
		Set<ClassInstance> ret = Util.newIdentityHashSet();

		for (MethodInstance method : cls.getMethods()) {
			ret.addAll(method.getClassRefs());
		}

		for (FieldInstance field : cls.getFields()) {
			ret.add(field.getType());
		}

		return ret;
	}

	private static AbstractClassifier inReferences = new AbstractClassifier("in references") {
		@Override
		public double getScore(ClassInstance clsA, ClassInstance clsB, ClassEnvironment env) {
			Set<ClassInstance> refsA = getInRefs(clsA);
			Set<ClassInstance> refsB = getInRefs(clsB);

			return ClassifierUtil.compareClassSets(refsA, refsB, false);
		}
	};

	private static Set<ClassInstance> getInRefs(ClassInstance cls) {
		Set<ClassInstance> ret = Util.newIdentityHashSet();

		for (MethodInstance method : cls.getMethodTypeRefs()) {
			ret.add(method.getCls());
		}

		for (FieldInstance field : cls.getFieldTypeRefs()) {
			ret.add(field.getCls());
		}

		return ret;
	}

	private static AbstractClassifier methodOutReferences = new AbstractClassifier("method out references") {
		@Override
		public double getScore(ClassInstance clsA, ClassInstance clsB, ClassEnvironment env) {
			Set<MethodInstance> refsA = getMethodOutRefs(clsA);
			Set<MethodInstance> refsB = getMethodOutRefs(clsB);

			return ClassifierUtil.compareMethodSets(refsA, refsB, false);
		}
	};

	private static Set<MethodInstance> getMethodOutRefs(ClassInstance cls) {
		Set<MethodInstance> ret = Util.newIdentityHashSet();

		for (MethodInstance method : cls.getMethods()) {
			ret.addAll(method.getRefsOut());
		}

		return ret;
	}

	private static AbstractClassifier methodInReferences = new AbstractClassifier("method in references") {
		@Override
		public double getScore(ClassInstance clsA, ClassInstance clsB, ClassEnvironment env) {
			Set<MethodInstance> refsA = getMethodInRefs(clsA);
			Set<MethodInstance> refsB = getMethodInRefs(clsB);

			return ClassifierUtil.compareMethodSets(refsA, refsB, false);
		}
	};

	private static Set<MethodInstance> getMethodInRefs(ClassInstance cls) {
		Set<MethodInstance> ret = Util.newIdentityHashSet();

		for (MethodInstance method : cls.getMethods()) {
			ret.addAll(method.getRefsIn());
		}

		return ret;
	}

	private static AbstractClassifier fieldReadReferences = new AbstractClassifier("field read references") {
		@Override
		public double getScore(ClassInstance clsA, ClassInstance clsB, ClassEnvironment env) {
			Set<FieldInstance> refsA = getFieldReadRefs(clsA);
			Set<FieldInstance> refsB = getFieldReadRefs(clsB);

			return ClassifierUtil.compareFieldSets(refsA, refsB, false);
		}
	};

	private static Set<FieldInstance> getFieldReadRefs(ClassInstance cls) {
		Set<FieldInstance> ret = Util.newIdentityHashSet();

		for (MethodInstance method : cls.getMethods()) {
			ret.addAll(method.getFieldReadRefs());
		}

		return ret;
	}

	private static AbstractClassifier fieldWriteReferences = new AbstractClassifier("field write references") {
		@Override
		public double getScore(ClassInstance clsA, ClassInstance clsB, ClassEnvironment env) {
			Set<FieldInstance> refsA = getFieldWriteRefs(clsA);
			Set<FieldInstance> refsB = getFieldWriteRefs(clsB);

			return ClassifierUtil.compareFieldSets(refsA, refsB, false);
		}
	};

	private static Set<FieldInstance> getFieldWriteRefs(ClassInstance cls) {
		Set<FieldInstance> ret = Util.newIdentityHashSet();

		for (MethodInstance method : cls.getMethods()) {
			ret.addAll(method.getFieldWriteRefs());
		}

		return ret;
	}

	private static AbstractClassifier stringConstants = new AbstractClassifier("string constants") {
		@Override
		public double getScore(ClassInstance clsA, ClassInstance clsB, ClassEnvironment env) {
			return ClassifierUtil.compareSets(clsA.getStrings(), clsB.getStrings(), true);
		}
	};

	private static AbstractClassifier numericConstants = new AbstractClassifier("numeric constants") {
		@Override
		public double getScore(ClassInstance clsA, ClassInstance clsB, ClassEnvironment env) {
			Set<Integer> intsA = new HashSet<>();
			Set<Integer> intsB = new HashSet<>();
			Set<Long> longsA = new HashSet<>();
			Set<Long> longsB = new HashSet<>();
			Set<Float> floatsA = new HashSet<>();
			Set<Float> floatsB = new HashSet<>();
			Set<Double> doublesA = new HashSet<>();
			Set<Double> doublesB = new HashSet<>();

			extractNumbers(clsA, intsA, longsA, floatsA, doublesA);
			extractNumbers(clsB, intsB, longsB, floatsB, doublesB);

			return (ClassifierUtil.compareSets(intsA, intsB, false)
					+ ClassifierUtil.compareSets(longsA, longsB, false)
					+ ClassifierUtil.compareSets(floatsA, floatsB, false)
					+ ClassifierUtil.compareSets(doublesA, doublesB, false)) / 4;
		}
	};

	private static AbstractClassifier membersFull = new AbstractClassifier("members full") {
		@Override
		public double getScore(ClassInstance clsA, ClassInstance clsB, ClassEnvironment env) {
			/*if (clsA.getName().equals("agl") && clsB.getName().equals("aht")) {
				System.out.println();
			}*/

			final double absThreshold = 0.8;
			final double relThreshold = 0.08;
			final ClassifierLevel level = ClassifierLevel.Full;
			double match = 0;

			if (clsA.getMethods().length > 0 && clsB.getMethods().length > 0) {
				double maxScore = MethodClassifier.getMaxScore(level);

				for (MethodInstance method : clsA.getMethods()) {
					if (!method.isMatchable()) continue;

					List<RankResult<MethodInstance>> ranking = MethodClassifier.rank(method, clsB.getMethods(), level, env);
					if (Matcher.checkRank(ranking, absThreshold, relThreshold, maxScore)) match += Matcher.getScore(ranking.get(0).getScore(), maxScore);
				}
			}

			if (clsA.getFields().length > 0 && clsB.getFields().length > 0) {
				double maxScore = FieldClassifier.getMaxScore(level);

				for (FieldInstance field : clsA.getFields()) {
					if (!field.isMatchable()) continue;

					List<RankResult<FieldInstance>> ranking = FieldClassifier.rank(field, clsB.getFields(), level, env);
					if (Matcher.checkRank(ranking, absThreshold, relThreshold, maxScore)) match += Matcher.getScore(ranking.get(0).getScore(), maxScore);
				}
			}

			int methods = Math.max(clsA.getMethods().length, clsB.getMethods().length);
			int fields = Math.max(clsA.getFields().length, clsB.getFields().length);

			if (methods == 0 && fields == 0) {
				return 1;
			} else {
				assert match <= methods + fields;

				return match / (methods + fields);
			}
		}
	};

	private static AbstractClassifier inRefsBci = new AbstractClassifier("in refs (bci)") {
		@Override
		public double getScore(ClassInstance clsA, ClassInstance clsB, ClassEnvironment env) {
			int matched = 0;
			int mismatched = 0;

			for (MethodInstance src : clsA.getMethodTypeRefs()) {
				if (src.getCls() == clsA) continue;

				MethodInstance dst = src.getMatch();

				if (dst == null || !clsB.getMethodTypeRefs().contains(dst)) {
					mismatched++;
					continue;
				}

				int[] map = ClassifierUtil.mapInsns(src, dst);
				if (map == null) continue;

				InsnList ilA = src.getAsmNode().instructions;
				InsnList ilB = dst.getAsmNode().instructions;

				for (int srcIdx = 0; srcIdx < map.length; srcIdx++) {
					if (map[srcIdx] < 0) continue;

					AbstractInsnNode in = ilA.get(srcIdx);
					if (in.getType() != AbstractInsnNode.METHOD_INSN) continue;

					MethodInsnNode min = (MethodInsnNode) in;
					ClassInstance owner = env.getClsByNameA(min.owner);

					if (owner != clsA) continue;

					in = ilB.get(map[srcIdx]);
					min = (MethodInsnNode) in;
					owner = env.getClsByNameB(min.owner);

					if (owner != clsB) {
						mismatched++;
					} else {
						matched++;
					}
				}
			}

			if (matched == 0 && mismatched == 0) {
				return 1;
			} else {
				return (double) matched / (matched + mismatched);
			}
		}
	};

	private static void extractNumbers(ClassInstance cls, Set<Integer> ints, Set<Long> longs, Set<Float> floats, Set<Double> doubles) {
		for (MethodInstance method : cls.getMethods()) {
			MethodNode asmNode = method.getAsmNode();
			if (asmNode == null) continue;

			ClassifierUtil.extractNumbers(asmNode, ints, longs, floats, doubles);
		}

		for (FieldInstance field : cls.getFields()) {
			FieldNode asmNode = field.getAsmNode();
			if (asmNode == null) continue;

			ClassifierUtil.handleNumberValue(asmNode.value, ints, longs, floats, doubles);
		}
	}

	public static abstract class AbstractClassifier implements IClassifier<ClassInstance> {
		public AbstractClassifier(String name) {
			this.name = name;
		}

		@Override
		public String getName() {
			return name;
		}

		@Override
		public double getWeight() {
			return weight;
		}

		private final String name;
		private double weight;
	}
} public enum ClassifierLevel {
	Initial,
	Intermediate,
	Full,
	Extra;

	public static final ClassifierLevel[] ALL = values();
} public class FieldClassifier {
	public static void init() {
		addClassifier(fieldTypeCheck, 10);
		addClassifier(accessFlags, 4);
		addClassifier(type, 10);
		addClassifier(signature, 5);
		addClassifier(readReferences, 6);
		addClassifier(writeReferences, 6);
		addClassifier(position, 3);
		addClassifier(initValue, 7);
		addClassifier(initStrings, 8);
		addClassifier(initCode, 10, ClassifierLevel.Intermediate, ClassifierLevel.Full, ClassifierLevel.Extra);
		addClassifier(readRefsBci, 6, ClassifierLevel.Extra);
		addClassifier(writeRefsBci, 6, ClassifierLevel.Extra);
	}

	public static void addClassifier(AbstractClassifier classifier, double weight, ClassifierLevel... levels) {
		if (levels.length == 0) levels = ClassifierLevel.ALL;

		classifier.weight = weight;

		for (ClassifierLevel level : levels) {
			classifiers.computeIfAbsent(level, ignore -> new ArrayList<>()).add(classifier);
			maxScore.put(level, getMaxScore(level) + weight);
		}
	}

	public static double getMaxScore(ClassifierLevel level) {
		return maxScore.getOrDefault(level, 0.);
	}

	public static List<RankResult<FieldInstance>> rank(FieldInstance src, FieldInstance[] dsts, ClassifierLevel level, ClassEnvironment env) {
		return rank(src, dsts, level, env, Double.POSITIVE_INFINITY);
	}

	public static List<RankResult<FieldInstance>> rank(FieldInstance src, FieldInstance[] dsts, ClassifierLevel level, ClassEnvironment env, double maxMismatch) {
		return ClassifierUtil.rank(src, dsts, classifiers.getOrDefault(level, Collections.emptyList()), ClassifierUtil::checkPotentialEquality, env, maxMismatch);
	}

	private static final Map<ClassifierLevel, List<IClassifier<FieldInstance>>> classifiers = new IdentityHashMap<>();
	private static final Map<ClassifierLevel, Double> maxScore = new EnumMap<>(ClassifierLevel.class);

	private static AbstractClassifier fieldTypeCheck = new AbstractClassifier("field type check") {
		@Override
		public double getScore(FieldInstance fieldA, FieldInstance fieldB, ClassEnvironment env) {
			if (!checkAsmNodes(fieldA, fieldB)) return compareAsmNodes(fieldA, fieldB);

			int mask = Opcodes.ACC_STATIC;
			int resultA = fieldA.getAsmNode().access & mask;
			int resultB = fieldB.getAsmNode().access & mask;

			return 1 - Integer.bitCount(resultA ^ resultB);
		}
	};

	private static AbstractClassifier accessFlags = new AbstractClassifier("access flags") {
		@Override
		public double getScore(FieldInstance fieldA, FieldInstance fieldB, ClassEnvironment env) {
			if (!checkAsmNodes(fieldA, fieldB)) return compareAsmNodes(fieldA, fieldB);

			int mask = (Opcodes.ACC_PUBLIC | Opcodes.ACC_PROTECTED | Opcodes.ACC_PRIVATE) | Opcodes.ACC_FINAL | Opcodes.ACC_VOLATILE | Opcodes.ACC_TRANSIENT | Opcodes.ACC_SYNTHETIC;
			int resultA = fieldA.getAsmNode().access & mask;
			int resultB = fieldB.getAsmNode().access & mask;

			return 1 - Integer.bitCount(resultA ^ resultB) / 6.;
		}
	};

	private static AbstractClassifier type = new AbstractClassifier("types") {
		@Override
		public double getScore(FieldInstance fieldA, FieldInstance fieldB, ClassEnvironment env) {
			return ClassifierUtil.checkPotentialEquality(fieldA.getType(), fieldB.getType()) ? 1 : 0;
		}
	};

	private static AbstractClassifier signature = new AbstractClassifier("signature") {
		@Override
		public double getScore(FieldInstance fieldA, FieldInstance fieldB, ClassEnvironment env) {
			FieldSignature sigA = fieldA.getSignature();
			FieldSignature sigB = fieldB.getSignature();

			if (sigA == null && sigB == null) return 1;
			if (sigA == null || sigB == null) return 0;

			return sigA.isPotentiallyEqual(sigB) ? 1 : 0;
		}
	};

	private static AbstractClassifier readReferences = new AbstractClassifier("read references") {
		@Override
		public double getScore(FieldInstance fieldA, FieldInstance fieldB, ClassEnvironment env) {
			return ClassifierUtil.compareMethodSets(fieldA.getReadRefs(), fieldB.getReadRefs(), true);
		}
	};

	private static AbstractClassifier writeReferences = new AbstractClassifier("write references") {
		@Override
		public double getScore(FieldInstance fieldA, FieldInstance fieldB, ClassEnvironment env) {
			return ClassifierUtil.compareMethodSets(fieldA.getWriteRefs(), fieldB.getWriteRefs(), true);
		}
	};

	private static AbstractClassifier position = new AbstractClassifier("position") {
		@Override
		public double getScore(FieldInstance fieldA, FieldInstance fieldB, ClassEnvironment env) {
			/*if (fieldA.position == fieldB.position) return 1;

			double relPosA = ClassifierUtil.getRelativePosition(fieldA.position, fieldA.cls.fields.size());
			double relPosB = ClassifierUtil.getRelativePosition(fieldB.position, fieldB.cls.fields.size());

			return 1 - Math.abs(relPosA - relPosB);*/
			return ClassifierUtil.classifyPosition(fieldA, fieldB, MemberInstance::getPosition, (f, idx) -> f.getCls().getField(idx), f -> f.getCls().getFields());
		}
	};

	private static AbstractClassifier initValue = new AbstractClassifier("init value") {
		@Override
		public double getScore(FieldInstance fieldA, FieldInstance fieldB, ClassEnvironment env) {
			if (!checkAsmNodes(fieldA, fieldB)) return compareAsmNodes(fieldA, fieldB);

			Object valA = fieldA.getAsmNode().value;
			Object valB = fieldB.getAsmNode().value;

			if (valA == null && valB == null) return 1;
			if (valA == null || valB == null) return 0;

			return valA.equals(valB) ? 1 : 0;
		}
	};

	private static AbstractClassifier initStrings = new AbstractClassifier("init strings") {
		@Override
		public double getScore(FieldInstance fieldA, FieldInstance fieldB, ClassEnvironment env) {
			List<AbstractInsnNode> initA = fieldA.getInitializer();
			List<AbstractInsnNode> initB = fieldB.getInitializer();

			if (initA == null && initB == null) return 1;
			if (initA == null || initB == null) return 0;

			Set<String> stringsA = new HashSet<>();
			ClassifierUtil.extractStrings(initA, stringsA);
			Set<String> stringsB = new HashSet<>();
			ClassifierUtil.extractStrings(initB, stringsB);

			return ClassifierUtil.compareSets(stringsA, stringsB, false);
		}
	};

	private static AbstractClassifier initCode = new AbstractClassifier("init code") {
		@Override
		public double getScore(FieldInstance fieldA, FieldInstance fieldB, ClassEnvironment env) {
			List<AbstractInsnNode> initA = fieldA.getInitializer();
			List<AbstractInsnNode> initB = fieldB.getInitializer();

			if (initA == null && initB == null) return 1;
			if (initA == null || initB == null) return 0;

			return ClassifierUtil.compareInsns(initA, initB, env);
		}
	};

	private static AbstractClassifier readRefsBci = new AbstractClassifier("read refs (bci)") {
		@Override
		public double getScore(FieldInstance fieldA, FieldInstance fieldB, ClassEnvironment env) {
			String ownerA = fieldA.getCls().getName();
			String nameA = fieldA.getName();
			String descA = fieldA.getDesc();
			String ownerB = fieldB.getCls().getName();
			String nameB = fieldB.getName();
			String descB = fieldB.getDesc();

			int matched = 0;
			int mismatched = 0;

			for (MethodInstance src : fieldA.getReadRefs()) {
				MethodInstance dst = src.getMatch();

				if (dst == null || !fieldB.getReadRefs().contains(dst)) {
					mismatched++;
					continue;
				}

				int[] map = ClassifierUtil.mapInsns(src, dst);
				if (map == null) continue;

				InsnList ilA = src.getAsmNode().instructions;
				InsnList ilB = dst.getAsmNode().instructions;

				for (int srcIdx = 0; srcIdx < map.length; srcIdx++) {
					if (map[srcIdx] < 0) continue;

					AbstractInsnNode in = ilA.get(srcIdx);
					if (in.getOpcode() != Opcodes.GETFIELD && in.getOpcode() != Opcodes.GETSTATIC) continue;

					FieldInsnNode fin = (FieldInsnNode) in;
					if (!isSameField(fin, ownerA, nameA, descA, fieldA)) continue;

					in = ilB.get(map[srcIdx]);
					fin = (FieldInsnNode) in;

					if (!isSameField(fin, ownerB, nameB, descB, fieldB)) {
						mismatched++;
					} else {
						matched++;
					}
				}
			}

			if (matched == 0 && mismatched == 0) {
				return 1;
			} else {
				return (double) matched / (matched + mismatched);
			}
		}
	};

	private static AbstractClassifier writeRefsBci = new AbstractClassifier("write refs (bci)") {
		@Override
		public double getScore(FieldInstance fieldA, FieldInstance fieldB, ClassEnvironment env) {
			String ownerA = fieldA.getCls().getName();
			String nameA = fieldA.getName();
			String descA = fieldA.getDesc();
			String ownerB = fieldB.getCls().getName();
			String nameB = fieldB.getName();
			String descB = fieldB.getDesc();

			int matched = 0;
			int mismatched = 0;

			for (MethodInstance src : fieldA.getWriteRefs()) {
				MethodInstance dst = src.getMatch();

				if (dst == null || !fieldB.getWriteRefs().contains(dst)) {
					mismatched++;
					continue;
				}

				int[] map = ClassifierUtil.mapInsns(src, dst);
				if (map == null) continue;

				InsnList ilA = src.getAsmNode().instructions;
				InsnList ilB = dst.getAsmNode().instructions;

				for (int srcIdx = 0; srcIdx < map.length; srcIdx++) {
					if (map[srcIdx] < 0) continue;

					AbstractInsnNode in = ilA.get(srcIdx);
					if (in.getOpcode() != Opcodes.PUTFIELD && in.getOpcode() != Opcodes.PUTSTATIC) continue;

					FieldInsnNode fin = (FieldInsnNode) in;
					if (!isSameField(fin, ownerA, nameA, descA, fieldA)) continue;

					in = ilB.get(map[srcIdx]);
					fin = (FieldInsnNode) in;

					if (!isSameField(fin, ownerB, nameB, descB, fieldB)) {
						mismatched++;
					} else {
						matched++;
					}
				}
			}

			if (matched == 0 && mismatched == 0) {
				return 1;
			} else {
				return (double) matched / (matched + mismatched);
			}
		}
	};

	private static boolean isSameField(FieldInsnNode fin, String owner, String name, String desc, FieldInstance field) {
		ClassInstance target;

		return fin.name.equals(name)
				&& fin.desc.equals(desc)
				&& (fin.owner.equals(owner) || (target = field.getEnv().getClsByName(fin.owner)) != null && target.resolveField(name, desc) == field);
	}

	private static boolean checkAsmNodes(FieldInstance a, FieldInstance b) {
		return a.getAsmNode() != null && b.getAsmNode() != null;
	}

	private static double compareAsmNodes(FieldInstance a, FieldInstance b) {
		return a.getAsmNode() == null && b.getAsmNode() == null ? 1 : 0;
	}

	public static abstract class AbstractClassifier implements IClassifier<FieldInstance> {
		public AbstractClassifier(String name) {
			this.name = name;
		}

		@Override
		public String getName() {
			return name;
		}

		@Override
		public double getWeight() {
			return weight;
		}

		private final String name;
		private double weight;
	}
} public class MethodClassifier {
	public static void init() {
		addClassifier(methodTypeCheck, 10);
		addClassifier(accessFlags, 4);
		addClassifier(argTypes, 10);
		addClassifier(retType, 5);
		addClassifier(signature, 5);
		addClassifier(classRefs, 3);
		addClassifier(stringConstants, 5);
		addClassifier(numericConstants, 5);
		addClassifier(parentMethods, 10);
		addClassifier(childMethods, 3);
		addClassifier(inReferences, 6);
		addClassifier(outReferences, 6);
		addClassifier(fieldReads, 5);
		addClassifier(fieldWrites, 5);
		addClassifier(position, 3);
		addClassifier(code, 12, ClassifierLevel.Full, ClassifierLevel.Extra);
		addClassifier(inRefsBci, 6, ClassifierLevel.Extra);
	}

	public static void addClassifier(AbstractClassifier classifier, double weight, ClassifierLevel... levels) {
		if (levels.length == 0) levels = ClassifierLevel.ALL;

		classifier.weight = weight;

		for (ClassifierLevel level : levels) {
			classifiers.computeIfAbsent(level, ignore -> new ArrayList<>()).add(classifier);
			maxScore.put(level, getMaxScore(level) + weight);
		}
	}

	public static double getMaxScore(ClassifierLevel level) {
		return maxScore.getOrDefault(level, 0.);
	}

	public static List<RankResult<MethodInstance>> rank(MethodInstance src, MethodInstance[] dsts, ClassifierLevel level, ClassEnvironment env) {
		return rank(src, dsts, level, env, Double.POSITIVE_INFINITY);
	}

	public static List<RankResult<MethodInstance>> rank(MethodInstance src, MethodInstance[] dsts, ClassifierLevel level, ClassEnvironment env, double maxMismatch) {
		if (src.hasMatch()) { // already matched,  limit dsts to the match
			if (!Arrays.asList(dsts).contains(src.getMatch())) {
				return Collections.emptyList();
			} else if (dsts.length != 1) {
				dsts = new MethodInstance[] { src.getMatch() };
			}
		} else { // limit dsts to the same method tree if there's a matched src
			MethodInstance matched = src.getHierarchyMatch();

			if (matched != null) {
				Set<MethodInstance> dstHierarchyMembers = matched.getAllHierarchyMembers();
				MethodInstance[] newDsts = new MethodInstance[dsts.length];
				int writeIdx = 0;

				for (int readIdx = 0; readIdx < dsts.length; readIdx++) {
					MethodInstance m = dsts[readIdx];

					if (dstHierarchyMembers.contains(m)) {
						newDsts[writeIdx++] = m;
					}
				}

				if (writeIdx == 0) return Collections.emptyList();
				if (writeIdx < newDsts.length) newDsts = Arrays.copyOf(newDsts, writeIdx);

				dsts = newDsts;
			}
		}

		return ClassifierUtil.rank(src, dsts, classifiers.getOrDefault(level, Collections.emptyList()), ClassifierUtil::checkPotentialEquality, env, maxMismatch);
	}

	private static final Map<ClassifierLevel, List<IClassifier<MethodInstance>>> classifiers = new EnumMap<>(ClassifierLevel.class);
	private static final Map<ClassifierLevel, Double> maxScore = new EnumMap<>(ClassifierLevel.class);

	private static AbstractClassifier methodTypeCheck = new AbstractClassifier("method type check") {
		@Override
		public double getScore(MethodInstance methodA, MethodInstance methodB, ClassEnvironment env) {
			if (!checkAsmNodes(methodA, methodB)) return compareAsmNodes(methodA, methodB);

			int mask = Opcodes.ACC_STATIC | Opcodes.ACC_NATIVE | Opcodes.ACC_ABSTRACT;
			int resultA = methodA.getAsmNode().access & mask;
			int resultB = methodB.getAsmNode().access & mask;

			return 1 - Integer.bitCount(resultA ^ resultB) / 3.;
		}
	};

	private static AbstractClassifier accessFlags = new AbstractClassifier("access flags") {
		@Override
		public double getScore(MethodInstance methodA, MethodInstance methodB, ClassEnvironment env) {
			if (!checkAsmNodes(methodA, methodB)) return compareAsmNodes(methodA, methodB);

			int mask = (Opcodes.ACC_PUBLIC | Opcodes.ACC_PROTECTED | Opcodes.ACC_PRIVATE) | Opcodes.ACC_FINAL | Opcodes.ACC_SYNCHRONIZED | Opcodes.ACC_BRIDGE | Opcodes.ACC_VARARGS | Opcodes.ACC_STRICT | Opcodes.ACC_SYNTHETIC;
			int resultA = methodA.getAsmNode().access & mask;
			int resultB = methodB.getAsmNode().access & mask;

			return 1 - Integer.bitCount(resultA ^ resultB) / 8.;
		}
	};

	private static AbstractClassifier argTypes = new AbstractClassifier("arg types") {
		@Override
		public double getScore(MethodInstance methodA, MethodInstance methodB, ClassEnvironment env) {
			return ClassifierUtil.compareClassLists(getArgTypes(methodA), getArgTypes(methodB));
		}
	};

	private static List<ClassInstance> getArgTypes(MethodInstance method) {
		MethodVarInstance[] args = method.getArgs();
		if (args.length == 0) return Collections.emptyList();

		List<ClassInstance> ret = new ArrayList<>(args.length);

		for (MethodVarInstance arg : args) {
			ret.add(arg.getType());
		}

		return ret;
	}

	private static AbstractClassifier retType = new AbstractClassifier("ret type") {
		@Override
		public double getScore(MethodInstance methodA, MethodInstance methodB, ClassEnvironment env) {
			return ClassifierUtil.checkPotentialEquality(methodA.getRetType(), methodB.getRetType()) ? 1 : 0;
		}
	};

	private static AbstractClassifier signature = new AbstractClassifier("signature") {
		@Override
		public double getScore(MethodInstance methodA, MethodInstance methodB, ClassEnvironment env) {
			MethodSignature sigA = methodA.getSignature();
			MethodSignature sigB = methodB.getSignature();

			if (sigA == null && sigB == null) return 1;
			if (sigA == null || sigB == null) return 0;

			return sigA.isPotentiallyEqual(sigB) ? 1 : 0;
		}
	};

	private static AbstractClassifier classRefs = new AbstractClassifier("class refs") {
		@Override
		public double getScore(MethodInstance methodA, MethodInstance methodB, ClassEnvironment env) {
			return ClassifierUtil.compareClassSets(methodA.getClassRefs(), methodB.getClassRefs(), true);
		}
	};

	private static AbstractClassifier stringConstants = new AbstractClassifier("string constants") {
		@Override
		public double getScore(MethodInstance methodA, MethodInstance methodB, ClassEnvironment env) {
			if (!checkAsmNodes(methodA, methodB)) return compareAsmNodes(methodA, methodB);

			Set<String> stringsA = new HashSet<>();
			ClassifierUtil.extractStrings(methodA.getAsmNode().instructions, stringsA);
			Set<String> stringsB = new HashSet<>();
			ClassifierUtil.extractStrings(methodB.getAsmNode().instructions, stringsB);

			return ClassifierUtil.compareSets(stringsA, stringsB, false);
		}
	};

	private static AbstractClassifier numericConstants = new AbstractClassifier("numeric constants") {
		@Override
		public double getScore(MethodInstance methodA, MethodInstance methodB, ClassEnvironment env) {
			if (!checkAsmNodes(methodA, methodB)) return compareAsmNodes(methodA, methodB);

			Set<Integer> intsA = new HashSet<>();
			Set<Integer> intsB = new HashSet<>();
			Set<Long> longsA = new HashSet<>();
			Set<Long> longsB = new HashSet<>();
			Set<Float> floatsA = new HashSet<>();
			Set<Float> floatsB = new HashSet<>();
			Set<Double> doublesA = new HashSet<>();
			Set<Double> doublesB = new HashSet<>();

			ClassifierUtil.extractNumbers(methodA.getAsmNode(), intsA, longsA, floatsA, doublesA);
			ClassifierUtil.extractNumbers(methodB.getAsmNode(), intsB, longsB, floatsB, doublesB);

			return (ClassifierUtil.compareSets(intsA, intsB, false)
					+ ClassifierUtil.compareSets(longsA, longsB, false)
					+ ClassifierUtil.compareSets(floatsA, floatsB, false)
					+ ClassifierUtil.compareSets(doublesA, doublesB, false)) / 4;
		}
	};

	private static AbstractClassifier parentMethods = new AbstractClassifier("parent methods") {
		@Override
		public double getScore(MethodInstance methodA, MethodInstance methodB, ClassEnvironment env) {
			return ClassifierUtil.compareMethodSets(methodA.getParents(), methodB.getParents(), true);
		}
	};

	private static AbstractClassifier childMethods = new AbstractClassifier("child methods") {
		@Override
		public double getScore(MethodInstance methodA, MethodInstance methodB, ClassEnvironment env) {
			return ClassifierUtil.compareMethodSets(methodA.getChildren(), methodB.getChildren(), true);
		}
	};

	private static AbstractClassifier outReferences = new AbstractClassifier("out references") {
		@Override
		public double getScore(MethodInstance methodA, MethodInstance methodB, ClassEnvironment env) {
			return ClassifierUtil.compareMethodSets(methodA.getRefsOut(), methodB.getRefsOut(), true);
		}
	};

	private static AbstractClassifier inReferences = new AbstractClassifier("in references") {
		@Override
		public double getScore(MethodInstance methodA, MethodInstance methodB, ClassEnvironment env) {
			return ClassifierUtil.compareMethodSets(methodA.getRefsIn(), methodB.getRefsIn(), true);
		}
	};

	private static AbstractClassifier fieldReads = new AbstractClassifier("field reads") {
		@Override
		public double getScore(MethodInstance methodA, MethodInstance methodB, ClassEnvironment env) {
			return ClassifierUtil.compareFieldSets(methodA.getFieldReadRefs(), methodB.getFieldReadRefs(), true);
		}
	};

	private static AbstractClassifier fieldWrites = new AbstractClassifier("field writes") {
		@Override
		public double getScore(MethodInstance methodA, MethodInstance methodB, ClassEnvironment env) {
			return ClassifierUtil.compareFieldSets(methodA.getFieldWriteRefs(), methodB.getFieldWriteRefs(), true);
		}
	};

	private static AbstractClassifier position = new AbstractClassifier("position") {
		@Override
		public double getScore(MethodInstance methodA, MethodInstance methodB, ClassEnvironment env) {
			return ClassifierUtil.classifyPosition(methodA, methodB, MemberInstance::getPosition, (m, idx) -> m.getCls().getMethod(idx), m -> m.getCls().getMethods());
		}
	};

	private static AbstractClassifier code = new AbstractClassifier("code") {
		@Override
		public double getScore(MethodInstance methodA, MethodInstance methodB, ClassEnvironment env) {
			if (!checkAsmNodes(methodA, methodB)) return compareAsmNodes(methodA, methodB);

			return ClassifierUtil.compareInsns(methodA, methodB);
		}
	};

	private static AbstractClassifier inRefsBci = new AbstractClassifier("in refs (bci)") {
		@Override
		public double getScore(MethodInstance methodA, MethodInstance methodB, ClassEnvironment env) {
			String ownerA = methodA.getCls().getName();
			String nameA = methodA.getName();
			String descA = methodA.getDesc();
			String ownerB = methodB.getCls().getName();
			String nameB = methodB.getName();
			String descB = methodB.getDesc();

			int matched = 0;
			int mismatched = 0;

			for (MethodInstance src : methodA.getRefsIn()) {
				if (src == methodA) continue;

				MethodInstance dst = src.getMatch();

				if (dst == null || !methodB.getRefsIn().contains(dst)) {
					mismatched++;
					continue;
				}

				int[] map = ClassifierUtil.mapInsns(src, dst);
				if (map == null) continue;

				InsnList ilA = src.getAsmNode().instructions;
				InsnList ilB = dst.getAsmNode().instructions;

				for (int srcIdx = 0; srcIdx < map.length; srcIdx++) {
					if (map[srcIdx] < 0) continue;

					AbstractInsnNode in = ilA.get(srcIdx);
					int type = in.getType();
					if (type != AbstractInsnNode.METHOD_INSN && type != AbstractInsnNode.INVOKE_DYNAMIC_INSN) continue;

					if (!isSameMethod(in, ownerA, nameA, descA, methodA)) continue;

					in = ilB.get(map[srcIdx]);

					if (!isSameMethod(in, ownerB, nameB, descB, methodB)) {
						mismatched++;
					} else {
						matched++;
					}
				}
			}

			if (matched == 0 && mismatched == 0) {
				return 1;
			} else {
				return (double) matched / (matched + mismatched);
			}
		}
	};

	private static boolean isSameMethod(AbstractInsnNode in, String owner, String name, String desc, MethodInstance method) {
		String sOwner, sName, sDesc;
		boolean sItf;

		if (in.getType() == AbstractInsnNode.METHOD_INSN) {
			MethodInsnNode min = (MethodInsnNode) in;
			sOwner = min.owner;
			sName = min.name;
			sDesc = min.desc;
			sItf = min.itf;
		} else {
			InvokeDynamicInsnNode din = (InvokeDynamicInsnNode) in;
			Handle impl = Util.getTargetHandle(din.bsm, din.bsmArgs);
			if (impl == null) return false;

			int tag = impl.getTag();
			if (tag < Opcodes.H_INVOKEVIRTUAL || tag > Opcodes.H_INVOKEINTERFACE) return false;

			sOwner = impl.getOwner();
			sName = impl.getName();
			sDesc = impl.getDesc();
			sItf = Util.isCallToInterface(impl);
		}

		ClassInstance target;

		return sName.equals(name)
				&& sDesc.equals(desc)
				&& (sOwner.equals(owner) || (target = method.getEnv().getClsByName(sOwner)) != null && target.resolveMethod(name, desc, sItf) == method);
	}

	private static boolean checkAsmNodes(MethodInstance a, MethodInstance b) {
		return a.getAsmNode() != null && b.getAsmNode() != null;
	}

	private static double compareAsmNodes(MethodInstance a, MethodInstance b) {
		return a.getAsmNode() == null && b.getAsmNode() == null ? 1 : 0;
	}

	public static abstract class AbstractClassifier implements IClassifier<MethodInstance> {
		public AbstractClassifier(String name) {
			this.name = name;
		}

		@Override
		public String getName() {
			return name;
		}

		@Override
		public double getWeight() {
			return weight;
		}

		private final String name;
		private double weight;
	}
} public class MethodVarClassifier {
	public static void init() {
		addClassifier(type, 10);
		addClassifier(position, 3);
		addClassifier(lvIndex, 2);
		addClassifier(usage, 8);
	}

	private static void addClassifier(AbstractClassifier classifier, double weight, ClassifierLevel... levels) {
		if (levels.length == 0) levels = ClassifierLevel.ALL;

		classifier.weight = weight;

		for (ClassifierLevel level : levels) {
			classifiers.computeIfAbsent(level, ignore -> new ArrayList<>()).add(classifier);
			maxScore.put(level, getMaxScore(level) + weight);
		}
	}

	public static double getMaxScore(ClassifierLevel level) {
		return maxScore.getOrDefault(level, 0.);
	}

	public static List<RankResult<MethodVarInstance>> rank(MethodVarInstance src, MethodVarInstance[] dsts, ClassifierLevel level, ClassEnvironment env, double maxMismatch) {
		return ClassifierUtil.rank(src, dsts, classifiers.getOrDefault(level, Collections.emptyList()), ClassifierUtil::checkPotentialEquality, env, maxMismatch);
	}

	private static final Map<ClassifierLevel, List<IClassifier<MethodVarInstance>>> classifiers = new EnumMap<>(ClassifierLevel.class);
	private static final Map<ClassifierLevel, Double> maxScore = new EnumMap<>(ClassifierLevel.class);

	private static AbstractClassifier type = new AbstractClassifier("type") {
		@Override
		public double getScore(MethodVarInstance argA, MethodVarInstance argB, ClassEnvironment env) {
			return ClassifierUtil.checkPotentialEquality(argA.getType(), argB.getType()) ? 1 : 0;
		}
	};

	private static AbstractClassifier position = new AbstractClassifier("position") {
		@Override
		public double getScore(MethodVarInstance methodA, MethodVarInstance methodB, ClassEnvironment env) {
			return ClassifierUtil.classifyPosition(methodA, methodB,
					MethodVarInstance::getIndex,
					(a, idx) -> (a.isArg() ? a.getMethod().getArg(idx) : a.getMethod().getVar(idx)),
					a -> (a.isArg() ? a.getMethod().getArgs() : a.getMethod().getVars()));
		}
	};

	private static AbstractClassifier lvIndex = new AbstractClassifier("lv index") {
		@Override
		public double getScore(MethodVarInstance argA, MethodVarInstance argB, ClassEnvironment env) {
			return argA.getLvIndex() == argB.getLvIndex() ? 1 : 0;
		}
	};

	private static AbstractClassifier usage = new AbstractClassifier("usage") {
		@Override
		public double getScore(MethodVarInstance argA, MethodVarInstance argB, ClassEnvironment env) {
			int[] map = ClassifierUtil.mapInsns(argA.getMethod(), argB.getMethod());
			if (map == null) return 1;

			InsnList ilA = argA.getMethod().getAsmNode().instructions;
			InsnList ilB = argB.getMethod().getAsmNode().instructions;
			int matched = 0;
			int mismatched = 0;

			for (int srcIdx = 0; srcIdx < map.length; srcIdx++) {
				int dstIdx = map[srcIdx];
				if (dstIdx < 0) continue;

				AbstractInsnNode inA = ilA.get(srcIdx);
				AbstractInsnNode inB = ilB.get(dstIdx);
				int varA, varB;

				if (inA.getType() == AbstractInsnNode.VAR_INSN) {
					varA = ((VarInsnNode) inA).var;
					varB = ((VarInsnNode) inB).var;
				} else if (inA.getType() == AbstractInsnNode.IINC_INSN) {
					varA = ((IincInsnNode) inA).var;
					varB = ((IincInsnNode) inB).var;
				} else {
					continue;
				}

				if (varA == argA.getLvIndex() && (argA.getStartInsn() < 0 || srcIdx >= argA.getStartInsn() && srcIdx < argA.getEndInsn())) {
					if (varB == argB.getLvIndex() && (argB.getStartInsn() < 0 || dstIdx >= argB.getStartInsn() && dstIdx < argB.getEndInsn())) {
						matched++;
					} else {
						mismatched++;
					}
				}
			}

			if (matched == 0 && mismatched == 0) {
				return 1;
			} else {
				return (double) matched / (matched + mismatched);
			}
		}
	};

	private static abstract class AbstractClassifier implements IClassifier<MethodVarInstance> {
		AbstractClassifier(String name) {
			this.name = name;
		}

		@Override
		public String getName() {
			return name;
		}

		@Override
		public double getWeight() {
			return weight;
		}

		private final String name;
		private double weight;
	}
} public class RankResult<T> {
	public RankResult(T subject, double score, List<ClassifierResult<T>> results) {
		this.subject = subject;
		this.score = score;
		this.results = results;
	}

	public T getSubject() {
		return subject;
	}

	public double getScore() {
		return score;
	}

	public List<ClassifierResult<T>> getResults() {
		return results;
	}

	private final T subject;
	private final double score;
	private final List<ClassifierResult<T>> results;
} public interface ClassEnv {
	boolean isShared();

	Collection<ClassInstance> getClasses();

	default ClassInstance getClsByName(String name) {
		return getClsById(ClassInstance.getId(name));
	}

	ClassInstance getClsById(String id);

	default ClassInstance getLocalClsByName(String name) {
		return getLocalClsById(ClassInstance.getId(name));
	}

	ClassInstance getLocalClsById(String id);

	default ClassInstance getCreateClassInstance(String id) {
		return getCreateClassInstance(id, true);
	}

	ClassInstance getCreateClassInstance(String id, boolean createUnknown);

	default ClassInstance getClsByName(String name, NameType nameType) {
		return getClsById(ClassInstance.getId(name), nameType);
	}

	ClassInstance getClsById(String id, NameType nameType);

	ClassEnvironment getGlobal();
	ClassEnv getOther();
} public final class ClassEnvironment implements ClassEnv {
	public void init(ProjectConfig config, DoubleConsumer progressReceiver) {
		final double cpInitCost = 0.05;
		final double classReadCost = 0.2;
		double progress = 0;

		inputsBeforeClassPath = config.hasInputsBeforeClassPath();
		nonObfuscatedClassPatternA = config.getNonObfuscatedClassPatternA().isEmpty() ? null : Pattern.compile(config.getNonObfuscatedClassPatternA());
		nonObfuscatedClassPatternB = config.getNonObfuscatedClassPatternB().isEmpty() ? null : Pattern.compile(config.getNonObfuscatedClassPatternB());
		nonObfuscatedMemberPatternA = config.getNonObfuscatedMemberPatternA().isEmpty() ? null : Pattern.compile(config.getNonObfuscatedMemberPatternA());
		nonObfuscatedMemberPatternB = config.getNonObfuscatedMemberPatternB().isEmpty() ? null : Pattern.compile(config.getNonObfuscatedMemberPatternB());

		try {
			for (int i = 0; i < 2; i++) {
				if ((i == 0) != inputsBeforeClassPath) {
					// class path indexing
					initClassPath(config.getSharedClassPath(), inputsBeforeClassPath);
					CompletableFuture.allOf(
							CompletableFuture.runAsync(() -> extractorA.processClassPath(config.getClassPathA(), inputsBeforeClassPath)),
							CompletableFuture.runAsync(() -> extractorB.processClassPath(config.getClassPathB(), inputsBeforeClassPath))).get();
					progress += cpInitCost;
				} else {
					// async class reading
					CompletableFuture.allOf(
							CompletableFuture.runAsync(() -> extractorA.processInputs(config.getPathsA(), nonObfuscatedClassPatternA)),
							CompletableFuture.runAsync(() -> extractorB.processInputs(config.getPathsB(), nonObfuscatedClassPatternB))).get();
					progress += classReadCost;
				}

				progressReceiver.accept(progress);
			}

			// synchronous feature extraction
			extractorA.process(nonObfuscatedMemberPatternA);
			progressReceiver.accept(0.8);

			extractorB.process(nonObfuscatedMemberPatternB);
			progressReceiver.accept(0.98);
		} catch (InterruptedException | ExecutionException | IOException e) {
			throw new RuntimeException(e);
		} finally {
			classPathIndex.clear();
			openFileSystems.forEach(Util::closeSilently);
			openFileSystems.clear();
		}

		progressReceiver.accept(1);
	}

	private void initClassPath(Collection<Path> sharedClassPath, boolean checkExisting) throws IOException {
		for (Path archive : sharedClassPath) {
			cpFiles.add(new InputFile(archive));

			FileSystem fs = Util.iterateJar(archive, false, file -> {
				String name = file.toAbsolutePath().toString();
				if (!name.startsWith("/") || !name.endsWith(".class") || name.startsWith("//")) throw new RuntimeException("invalid path: "+archive+" ("+name+")");
				name = name.substring(1, name.length() - ".class".length());

				if (!checkExisting || extractorA.getLocalClsByName(name) == null || extractorB.getLocalClsByName(name) == null) {
					classPathIndex.putIfAbsent(name, file);

					/*ClassNode cn = readClass(file);
					addSharedCls(new ClassInstance(ClassInstance.getId(cn.name), file.toUri(), cn));*/
				}
			});

			if (fs != null) openFileSystems.add(fs);
		}
	}

	public void reset() {
		cpFiles.clear();
		sharedClasses.clear();
		classPathIndex.clear();
		extractorA.reset();
		extractorB.reset();
		cache.clear();
	}

	public void addOpenFileSystem(FileSystem fs) {
		openFileSystems.add(fs);
	}

	public Pattern getNonObfuscatedClassPatternA() {
		return nonObfuscatedClassPatternA;
	}

	public Pattern getNonObfuscatedClassPatternB() {
		return nonObfuscatedClassPatternB;
	}

	public Pattern getNonObfuscatedMemberPatternA() {
		return nonObfuscatedMemberPatternA;
	}

	public Pattern getNonObfuscatedMemberPatternB() {
		return nonObfuscatedMemberPatternB;
	}

	@Override
	public boolean isShared() {
		return true;
	}

	@Override
	public ClassInstance getClsById(String id) {
		return getSharedClsById(id);
	}

	@Override
	public ClassInstance getLocalClsById(String id) {
		return getSharedClsById(id);
	}

	@Override
	public ClassInstance getClsById(String id, NameType nameType) {
		return getSharedClsById(id);
	}

	public LocalClassEnv getEnvA() {
		return extractorA;
	}

	public LocalClassEnv getEnvB() {
		return extractorB;
	}

	@Override
	public Collection<ClassInstance> getClasses() {
		return new AbstractCollection<ClassInstance>() {
			@Override
			public Iterator<ClassInstance> iterator() {
				return new Iterator<ClassInstance>() {
					@Override
					public boolean hasNext() {
						checkAdvance();

						return parent.hasNext();
					}

					@Override
					public ClassInstance next() {
						checkAdvance();

						return parent.next();
					}

					private void checkAdvance() {
						if (isA && !parent.hasNext()) {
							parent = extractorB.getClasses().iterator();
							isA = false;
						}
					}

					private Iterator<ClassInstance> parent = extractorA.getClasses().iterator();
					private boolean isA = true;
				};
			}

			@Override
			public int size() {
				return extractorA.getClasses().size() + extractorB.getClasses().size();
			}
		};
	}

	public ClassInstance getClsByNameA(String name) {
		return extractorA.getClsByName(name);
	}

	public ClassInstance getClsByNameB(String name) {
		return extractorB.getClsByName(name);
	}

	public ClassInstance getClsByIdA(String id) {
		return extractorA.getClsById(id);
	}

	public ClassInstance getClsByIdB(String id) {
		return extractorB.getClsById(id);
	}

	public ClassInstance getLocalClsByNameA(String name) {
		return extractorA.getLocalClsByName(name);
	}

	public ClassInstance getLocalClsByNameB(String name) {
		return extractorB.getLocalClsByName(name);
	}

	public ClassInstance getLocalClsByIdA(String id) {
		return extractorA.getLocalClsById(id);
	}

	public ClassInstance getLocalClsByIdB(String id) {
		return extractorB.getLocalClsById(id);
	}

	public ClassInstance getSharedClsById(String id) {
		assert !id.isEmpty();
		assert id.length() == 1 || id.charAt(id.length() - 1) == ';' || id.charAt(0) == '[' && id.lastIndexOf('[') == id.length() - 2 : id;

		return sharedClasses.get(id);
	}

	public ClassInstance addSharedCls(ClassInstance cls) {
		if (!cls.isShared()) throw new IllegalArgumentException("non-shared class");

		ClassInstance prev = sharedClasses.putIfAbsent(cls.getId(), cls);
		if (prev != null) return prev;

		return cls;
	}

	public Path getSharedClassLocation(String name) {
		return classPathIndex.get(name);
	}

	public Collection<ClassInstance> getClassesA() {
		return extractorA.getClasses();
	}

	public Collection<ClassInstance> getClassesB() {
		return extractorB.getClasses();
	}

	public List<ClassInstance> getDisplayClassesA(boolean inputsOnly) {
		return getDisplayClasses(extractorA, inputsOnly);
	}

	public List<ClassInstance> getDisplayClassesB(boolean inputsOnly) {
		return getDisplayClasses(extractorB, inputsOnly);
	}

	private static List<ClassInstance> getDisplayClasses(ClassFeatureExtractor extractor, boolean inputsOnly) {
		List<ClassInstance> ret = new ArrayList<>();

		for (ClassInstance cls : extractor.getClasses()) {
			if (!cls.isReal() || inputsOnly && !cls.isInput()) continue;

			ret.add(cls);
		}

		ret.sort(Comparator.comparing(ClassInstance::toString));

		return ret;
	}

	public Collection<InputFile> getClassPathFiles() {
		return cpFiles;
	}

	public Collection<InputFile> getClassPathFilesA() {
		return extractorA.getClassPathFiles();
	}

	public Collection<InputFile> getClassPathFilesB() {
		return extractorB.getClassPathFiles();
	}

	public Collection<InputFile> getInputFilesA() {
		return extractorA.getInputFiles();
	}

	public Collection<InputFile> getInputFilesB() {
		return extractorB.getInputFiles();
	}

	public String decompile(Decompiler decompiler, ClassInstance cls, NameType nameType) {
		ClassFeatureExtractor extractor;

		if (extractorA.getLocalClsById(cls.getId()) == cls) {
			extractor = extractorA;
		} else if (extractorB.getLocalClsById(cls.getId()) == cls) {
			extractor = extractorB;
		} else {
			throw new IllegalArgumentException("unknown class: "+cls);
		}

		return decompiler.decompile(cls, extractor, nameType);
	}

	@Override
	public ClassInstance getCreateClassInstance(String id, boolean createUnknown) {
		ClassInstance ret = getSharedClsById(id);
		if (ret != null) return ret;

		if (id.charAt(0) == '[') { // array type
			ClassInstance elementClass = getArrayCls(this, id);
			ClassInstance cls = new ClassInstance(id, elementClass);

			assert elementClass.isShared();
			ret = addSharedCls(cls);

			if (ret == cls) { // cls was added
				addSuperClass(ret, "java/lang/Object");
			}
		} else {
			ret = getMissingCls(id, createUnknown);
		}

		return ret;
	}

	static ClassInstance getArrayCls(ClassEnv env, String id) {
		assert id.startsWith("[");

		String elementId = id.substring(id.lastIndexOf('[') + 1);
		if (elementId.isEmpty()) throw new IllegalArgumentException("invalid class desc: "+id);
		assert elementId.length() == 1 || elementId.charAt(elementId.length() - 1) == ';' : elementId;

		return env.getCreateClassInstance(elementId);
	}

	ClassInstance getMissingCls(String id, boolean createUnknown) {
		if (id.length() > 1) {
			ClassInstance a = extractorA.getLocalClsById(id);
			ClassInstance b = extractorB.getLocalClsById(id);

			if (a != null && a.isReal() || b != null && b.isReal()) {
				throw new InvalidSharedEnvQueryException(a, b);
			}

			String name = ClassInstance.getName(id);
			Path file = getSharedClassLocation(name);

			if (file == null) {
				URL url = ClassLoader.getSystemResource(name+".class");

				if (url != null) {
					file = getPath(url);
				}
			}

			if (file != null) {
				ClassNode cn = readClass(file, true);
				ClassInstance cls = new ClassInstance(ClassInstance.getId(cn.name), getContainingUri(file.toUri(), cn.name), this, cn);
				if (!cls.getId().equals(id)) throw new RuntimeException("mismatched cls id "+id+" for "+file+", expected "+name);

				ClassInstance ret = addSharedCls(cls);

				if (ret == cls) { // cls was added
					processClassA(ret, null);
				}

				return ret;
			}
		}

		if (!createUnknown) return null;

		ClassInstance ret = new ClassInstance(id, this);
		addSharedCls(ret);

		return ret;
	}

	private Path getPath(URL url) {
		URI uri = null;

		try {
			uri = url.toURI();
			Path ret = Paths.get(uri);

			if (uri.getScheme().equals("jrt") && !Files.exists(ret)) {
				// workaround for https://bugs.openjdk.java.net/browse/JDK-8224946
				ret = Paths.get(new URI(uri.getScheme(), uri.getUserInfo(), uri.getHost(), uri.getPort(), "/modules".concat(uri.getPath()), uri.getQuery(), uri.getFragment()));
			}

			return ret;
		} catch (URISyntaxException e) {
			throw new RuntimeException(e);
		} catch (FileSystemNotFoundException e) {
			try {
				addOpenFileSystem(FileSystems.newFileSystem(uri, Collections.emptyMap()));

				return Paths.get(uri);
			} catch (FileSystemNotFoundException e2) {
				throw new RuntimeException("can't find fs for "+url, e2);
			} catch (IOException e2) {
				throw new UncheckedIOException(e2);
			}
		}
	}

	static URI getContainingUri(URI uri, String clsName) {
		String path;

		if (uri.getScheme().equals("jar")) {
			path = uri.getSchemeSpecificPart();
			int pos = path.lastIndexOf("!/");

			if (pos > 0) {
				try {
					return new URI(path.substring(0, pos));
				} catch (URISyntaxException e) {
					throw new RuntimeException(e);
				}
			} else {
				throw new UnsupportedOperationException("jar uri without !/: "+uri);
			}
		} else {
			path = uri.getPath();
		}

		if (path == null) {
			throw new UnsupportedOperationException("uri without path: "+uri);
		}

		int rootPos = path.length() - ".class".length() - clsName.length();

		if (rootPos <= 0 || !path.endsWith(".class") || !path.startsWith(clsName, rootPos)) {
			throw new UnsupportedOperationException("unknown path format: "+uri);
		}

		path = path.substring(0, rootPos - 1);

		try {
			return new URI(uri.getScheme(), uri.getUserInfo(), uri.getHost(), uri.getPort(), path, uri.getQuery(), uri.getFragment());
		} catch (URISyntaxException e) {
			throw new RuntimeException(e);
		}
	}

	static ClassNode readClass(Path path, boolean skipCode) {
		try {
			ClassReader reader = new ClassReader(Files.readAllBytes(path));
			ClassNode cn = new ClassNode();
			reader.accept(cn, ClassReader.EXPAND_FRAMES | (skipCode ? ClassReader.SKIP_CODE : 0));

			return cn;
		} catch (IOException e) {
			throw new UncheckedIOException(e);
		}
	}

	/**
	 * 1st class processing pass, member+class hierarchy and signature initialization.
	 *
	 * Only the (known) classes are fully available at this point.
	 */
	static void processClassA(ClassInstance cls, Pattern nonObfuscatedMemberPattern) {
		assert !cls.isInput() || !cls.isShared();

		Set<String> strings = cls.strings;

		for (ClassNode cn : cls.getAsmNodes()) {
			if (cls.isInput() && cls.getSignature() == null && cn.signature != null) {
				cls.setSignature(ClassSignature.parse(cn.signature, cls.getEnv()));
			}

			boolean isEnum = (cn.access & Opcodes.ACC_ENUM) != 0;

			for (int i = 0; i < cn.methods.size(); i++) {
				MethodNode mn = cn.methods.get(i);

				if (cls.getMethod(mn.name, mn.desc) == null) {
					boolean nameObfuscated = cls.isInput()
							&& !mn.name.equals("<clinit>")
							&& !mn.name.equals("<init>")
							&& (!mn.name.equals("main") || !mn.desc.equals("([Ljava/lang/String;)V") || mn.access != (Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC))
							&& (!isEnum || !isStandardEnumMethod(cn.name, mn))
							&& (nonObfuscatedMemberPattern == null || !nonObfuscatedMemberPattern.matcher(cn.name+"/"+mn.name+mn.desc).matches());

					cls.addMethod(new MethodInstance(cls, mn.name, mn.desc, mn, nameObfuscated, i));

					ClassifierUtil.extractStrings(mn.instructions, strings);
				}
			}

			for (int i = 0; i < cn.fields.size(); i++) {
				FieldNode fn = cn.fields.get(i);

				if (cls.getField(fn.name, fn.desc) == null) {
					boolean nameObfuscated = cls.isInput()
							&& (nonObfuscatedMemberPattern == null || !nonObfuscatedMemberPattern.matcher(cn.name+"/"+fn.name+";;"+fn.desc).matches());

					cls.addField(new FieldInstance(cls, fn.name, fn.desc, fn, nameObfuscated, i));

					if (fn.value instanceof String) {
						strings.add((String) fn.value);
					}
				}
			}

			if (cls.getOuterClass() == null) detectOuterClass(cls, cn);

			if (cn.superName != null && cls.getSuperClass() == null) {
				addSuperClass(cls, cn.superName);
			}

			for (String iface : cn.interfaces) {
				ClassInstance ifCls = cls.getEnv().getCreateClassInstance(ClassInstance.getId(iface));

				if (cls.interfaces.add(ifCls)) ifCls.implementers.add(cls);
			}
		}
	}

	private static boolean isStandardEnumMethod(String clsName, MethodNode m) {
		final int reqFlags = Opcodes.ACC_STATIC | Opcodes.ACC_PUBLIC;
		if ((m.access & reqFlags) != reqFlags) return false;

		return m.name.equals("values") && m.desc.startsWith("()[L") && m.desc.startsWith(clsName, 4) && m.desc.length() == clsName.length() + 5
				|| m.name.equals("valueOf") && m.desc.startsWith("(Ljava/lang/String;)L") && m.desc.startsWith(clsName, 21) && m.desc.length() == clsName.length() + 22;
	}

	private static void detectOuterClass(ClassInstance cls, ClassNode cn) {
		if (cn.outerClass != null) {
			addOuterClass(cls, cn.outerClass, true);
		} else if (cn.outerMethod != null) {
			throw new UnsupportedOperationException();
		} else { // determine outer class by outer$inner name pattern
			for (InnerClassNode icn : cn.innerClasses) {
				if (icn.name.equals(cn.name)) {
					if (icn.outerName == null) { // this attribute isn't guaranteed to be provided
						break;
					} else {
						addOuterClass(cls, icn.outerName, true);
						return;
					}
				}
			}

			int pos;

			if ((pos = cn.name.lastIndexOf('$')) > 0 && pos < cn.name.length() - 1) {
				addOuterClass(cls, cn.name.substring(0, pos), false);
			}
		}
	}

	private static void addOuterClass(ClassInstance cls, String name, boolean createUnknown) {
		ClassInstance outerClass = cls.getEnv().getLocalClsByName(name);

		if (outerClass == null) {
			outerClass = cls.getEnv().getCreateClassInstance(ClassInstance.getId(name), createUnknown);

			if (outerClass == null) {
				System.err.println("missing outer cls: "+name+" for "+cls);
				return;
			}
		}

		cls.outerClass = outerClass;
		outerClass.innerClasses.add(cls);
	}

	static void addSuperClass(ClassInstance cls, String name) {
		cls.superClass = cls.getEnv().getCreateClassInstance(ClassInstance.getId(name));
		cls.superClass.childClasses.add(cls);
	}

	@Override
	public ClassEnvironment getGlobal() {
		return this;
	}

	@Override
	public ClassEnv getOther() {
		return this;
	}

	public MatchingCache getCache() {
		return cache;
	}

	private final List<InputFile> cpFiles = new ArrayList<>();
	private final Map<String, ClassInstance> sharedClasses = new HashMap<>();
	private final List<FileSystem> openFileSystems = new ArrayList<>();
	private final Map<String, Path> classPathIndex = new HashMap<>();
	private final ClassFeatureExtractor extractorA = new ClassFeatureExtractor(this);
	private final ClassFeatureExtractor extractorB = new ClassFeatureExtractor(this);
	private final MatchingCache cache = new MatchingCache();

	private boolean inputsBeforeClassPath;
	private Pattern nonObfuscatedClassPatternA;
	private Pattern nonObfuscatedClassPatternB;
	private Pattern nonObfuscatedMemberPatternA;
	private Pattern nonObfuscatedMemberPatternB;

	public boolean assumeBothOrNoneObfuscated = false;

	public String classUidPrefix = "class_";
	public String methodUidPrefix = "method_";
	public String fieldUidPrefix = "field_";
	public String argUidPrefix = "arg_";
	public String varUidPrefix = "var_";
	public int nextClassUid;
	public int nextMethodUid;
	public int nextFieldUid;
} public final class ClassInstance implements Matchable<ClassInstance> {
	/**
	 * Create a shared unknown class.
	 */
	ClassInstance(String id, ClassEnv env) {
		this(id, null, env, null, false, false, null);

		assert id.indexOf('[') == -1 : id;
	}

	/**
	 * Create a known class (class path).
	 */
	public ClassInstance(String id, URI origin, ClassEnv env, ClassNode asmNode) {
		this(id, origin, env, asmNode, false, false, null);

		assert id.indexOf('[') == -1 : id;
	}

	/**
	 * Create an array class.
	 */
	ClassInstance(String id, ClassInstance elementClass) {
		this(id, null, elementClass.env, null, elementClass.nameObfuscated, false, elementClass);

		assert id.startsWith("[") : id;
		assert id.indexOf('[', getArrayDimensions()) == -1 : id;
		assert !elementClass.isArray();

		elementClass.addArray(this);
	}

	/**
	 * Create a non-array class.
	 */
	ClassInstance(String id, URI origin, ClassEnv env, ClassNode asmNode, boolean nameObfuscated) {
		this(id, origin, env, asmNode, nameObfuscated, true, null);

		assert id.startsWith("L") : id;
		assert id.indexOf('[') == -1 : id;
		assert asmNode != null;
	}

	private ClassInstance(String id, URI origin, ClassEnv env, ClassNode asmNode, boolean nameObfuscated, boolean input, ClassInstance elementClass) {
		if (id.isEmpty()) throw new IllegalArgumentException("empty id");
		if (env == null) throw new NullPointerException("null env");

		this.id = id;
		this.origin = origin;
		this.env = env;
		this.asmNodes = asmNode == null ? null : new ClassNode[] { asmNode };
		this.nameObfuscated = nameObfuscated;
		this.input = input;
		this.elementClass = elementClass;

		if (env.isShared()) matchedClass = this;
	}

	@Override
	public MatchableKind getKind() {
		return MatchableKind.CLASS;
	}

	@Override
	public String getId() {
		return id;
	}

	@Override
	public String getName() {
		return getName(id);
	}

	@Override
	public String getName(NameType type) {
		return getName(type, true);
	}

	public String getName(NameType type, boolean includeOuter) {
		if (type == NameType.PLAIN) {
			return includeOuter ? getName() : getInnerName0(getName());
		} else if (elementClass != null) {
			boolean isPrimitive = elementClass.isPrimitive();
			StringBuilder ret = new StringBuilder();

			ret.append(id, 0, getArrayDimensions());

			if (!isPrimitive) ret.append('L');
			ret.append(elementClass.getName(type, includeOuter));
			if (!isPrimitive) ret.append(';');

			return ret.toString();
		} else if (type == NameType.UID_PLAIN) {
			int uid = getUid();
			if (uid >= 0) return env.getGlobal().classUidPrefix+uid;
		}

		boolean locTmp = type == NameType.MAPPED_LOCTMP_PLAIN || type == NameType.LOCTMP_PLAIN;
		String ret;
		boolean fromMatched; // name retrieved from matched class

		if (type.mapped && mappedName != null) {
			// MAPPED_*, local name available
			ret = mappedName;
			fromMatched = false;
		} else if (type.mapped && matchedClass != null && matchedClass.mappedName != null) {
			// MAPPED_*, remote name available
			ret = matchedClass.mappedName;
			fromMatched = true;
		} else if (type.mapped && !nameObfuscated) {
			// MAPPED_*, local deobf
			ret = getInnerName0(getName());
			fromMatched = false;
		} else if (type.mapped && matchedClass != null && !matchedClass.nameObfuscated) {
			// MAPPED_*, remote deobf
			ret = matchedClass.getInnerName0(matchedClass.getName());
			fromMatched = true;
		} else if (type.isAux() && auxName != null && auxName.length > type.getAuxIndex() && auxName[type.getAuxIndex()] != null) {
			ret = auxName[type.getAuxIndex()];
			fromMatched = false;
		} else if (type.isAux() && matchedClass != null && matchedClass.auxName != null && matchedClass.auxName.length > type.getAuxIndex() && matchedClass.auxName[type.getAuxIndex()] != null) {
			ret = matchedClass.auxName[type.getAuxIndex()];
			fromMatched = true;
		} else if (type.tmp && matchedClass != null && matchedClass.tmpName != null) {
			// MAPPED_TMP_* with obf name or TMP_*, remote name available
			ret = matchedClass.tmpName;
			fromMatched = true;
		} else if ((type.tmp || locTmp) && tmpName != null) {
			// MAPPED_TMP_* or MAPPED_LOCTMP_* with obf name or TMP_* or LOCTMP_*, local name available
			ret = tmpName;
			fromMatched = false;
		} else if (type.plain) {
			ret = getInnerName0(getName());
			fromMatched = false;
		} else {
			return null;
		}

		assert ret == null || !hasOuterName(ret) : ret;

		if (!includeOuter) return ret;

		/*
		 * ret-outer: whether ret's source has an outer class
		 * this-outer: whether this has an outer class
		 * has outer class -> assume not normal name with pkg, but plain inner class name
		 *
		 * ret-outer this-outer action                    ret-example this-example result-example
		 *     n         n      ret                        a/b         d/e          a/b
		 *     n         y      this.outer+ret.strip-pkg   a/b         d/e$f        d/e$b
		 *     y         n      ret.outer.pkg+ret          a/b$c       d/e          a/c
		 *     y         y      this.outer+ret             a/b$c       d/e$f        d/e$c
		 */

		if (!fromMatched || (outerClass == null) == (matchedClass.outerClass == null)) { // ret-outer == this-outer
			return outerClass != null ? getNestedName(outerClass.getName(type), ret) : ret;
		} else if (outerClass != null) { // ret is normal name, strip package from ret before concatenating
			return getNestedName(outerClass.getName(type), ret.substring(ret.lastIndexOf('/') + 1));
		} else { // ret is an outer name, restore pkg
			String matchedOuterName = matchedClass.outerClass.getName(type);
			int pkgEnd = matchedOuterName.lastIndexOf('/');
			if (pkgEnd > 0) ret = matchedOuterName.substring(0, pkgEnd + 1).concat(ret);

			return ret;
		}
	}

	private String getInnerName0(String name) {
		if (outerClass == null && (isReal() || !hasOuterName(name))) {
			return name;
		} else {
			return getInnerName(name);
		}
	}

	@Override
	public String getDisplayName(NameType type, boolean full) {
		char lastChar = id.charAt(id.length() - 1);
		String ret;

		if (lastChar != ';') { // primitive or primitive array
			switch (lastChar) {
			case 'B': ret = "byte"; break;
			case 'C': ret = "char"; break;
			case 'D': ret = "double"; break;
			case 'F': ret = "float"; break;
			case 'I': ret = "int"; break;
			case 'J': ret = "long"; break;
			case 'S': ret = "short"; break;
			case 'V': ret = "void"; break;
			case 'Z': ret = "boolean"; break;
			default: throw new IllegalStateException("invalid class desc: "+id);
			}
		} else {
			ret = getName(type).replace('/', '.');
		}

		int dims = getArrayDimensions();

		if (dims > 0) {
			StringBuilder sb;

			if (lastChar != ';') { // primitive array, ret is in plain name form from above
				assert !ret.startsWith("[") && !ret.endsWith(";");

				sb = new StringBuilder(ret.length() + 2 * dims);
				sb.append(ret);
			} else { // reference array, in dot separated id form
				assert ret.startsWith("[") && ret.endsWith(";");

				sb = new StringBuilder(ret.length() + dims - 2);
				sb.append(ret, dims + 1, ret.length() - 1);
			}

			for (int i = 0; i < dims; i++) {
				sb.append("[]");
			}

			ret = sb.toString();
		}

		return full ? ret : ret.substring(ret.lastIndexOf('.') + 1);
	}

	public boolean isReal() {
		return origin != null;
	}

	public URI getOrigin() {
		return origin;
	}

	@Override
	public Matchable<?> getOwner() {
		return null;
	}

	@Override
	public ClassEnv getEnv() {
		return env;
	}

	public ClassNode[] getAsmNodes() {
		return asmNodes;
	}

	public URI getAsmNodeOrigin(int index) {
		if (index < 0 || index > 0 && (asmNodeOrigins == null || index >= asmNodeOrigins.length)) throw new IndexOutOfBoundsException(index);

		return index == 0 ? origin : asmNodeOrigins[index];
	}

	public ClassNode getMergedAsmNode() {
		if (asmNodes == null) return null;
		if (asmNodes.length == 1) return asmNodes[0];

		return asmNodes[0]; // TODO: actually merge
	}

	void addAsmNode(ClassNode node, URI origin) {
		if (!input) throw new IllegalStateException("not mergeable");

		asmNodes = Arrays.copyOf(asmNodes, asmNodes.length + 1);
		asmNodes[asmNodes.length - 1] = node;

		if (asmNodeOrigins == null) {
			asmNodeOrigins = new URI[2];
			asmNodeOrigins[0] = this.origin;
		} else {
			asmNodeOrigins = Arrays.copyOf(asmNodeOrigins, asmNodeOrigins.length + 1);
		}

		asmNodeOrigins[asmNodeOrigins.length - 1] = origin;
	}

	@Override
	public boolean hasPotentialMatch() {
		if (matchedClass != null) return true;
		if (!isMatchable()) return false;

		for (ClassInstance o : env.getOther().getClasses()) {
			if (o.isReal() && ClassifierUtil.checkPotentialEquality(this, o)) return true;
		}

		return false;
	}

	@Override
	public boolean isMatchable() {
		return matchable;
	}

	@Override
	public boolean setMatchable(boolean matchable) {
		if (!matchable && matchedClass != null) return false;

		this.matchable = matchable;

		return true;
	}

	@Override
	public ClassInstance getMatch() {
		return matchedClass;
	}

	public void setMatch(ClassInstance cls) {
		assert cls == null || isMatchable();
		assert cls == null || cls.getEnv() != env && !cls.getEnv().isShared();

		this.matchedClass = cls;
	}

	@Override
	public boolean isFullyMatched(boolean recursive) {
		if (matchedClass == null) return false;

		for (MethodInstance m : methods) {
			if (m.hasPotentialMatch() && (!m.hasMatch() || recursive && !m.isFullyMatched(true))) {
				return false;
			}
		}

		for (FieldInstance m : fields) {
			if (m.hasPotentialMatch() && (!m.hasMatch() || recursive && !m.isFullyMatched(true))) {
				return false;
			}
		}

		return true;
	}

	@Override
	public float getSimilarity() {
		if (matchedClass == null) return 0;

		return SimilarityChecker.compare(this, matchedClass);
	}

	@Override
	public boolean isNameObfuscated() {
		return nameObfuscated;
	}

	public boolean isInput() {
		return input;
	}

	public ClassInstance getElementClass() {
		if (!isArray()) throw new IllegalStateException("not applicable to non-array");

		return elementClass;
	}

	public ClassInstance getElementClassShallow(boolean create) {
		if (!isArray()) throw new IllegalStateException("not applicable to non-array");

		int dims = getArrayDimensions();
		if (dims <= 1) return elementClass;

		String retId = id.substring(1);

		return create ? env.getCreateClassInstance(retId) : env.getClsById(retId);
	}

	public ClassSignature getSignature() {
		return signature;
	}

	void setSignature(ClassSignature signature) {
		this.signature = signature;
	}

	public boolean isPrimitive() {
		char start = id.charAt(0);

		return start != 'L' && start != '[';
	}

	public int getSlotSize() {
		char start = id.charAt(0);

		return (start == 'D' || start == 'J') ? 2 : 1;
	}

	public boolean isArray() {
		return elementClass != null;
	}

	public int getArrayDimensions() {
		if (elementClass == null) return 0;

		for (int i = 0; i < id.length(); i++) {
			if (id.charAt(i) != '[') return i;
		}

		throw new IllegalStateException("invalid id: "+id);
	}

	public ClassInstance[] getArrays() {
		return arrays;
	}

	private void addArray(ClassInstance cls) {
		assert !Arrays.asList(arrays).contains(cls);

		arrays = Arrays.copyOf(arrays, arrays.length + 1);
		arrays[arrays.length - 1] = cls;
	}

	public int getAccess() {
		int ret;

		if (asmNodes != null) {
			ret = asmNodes[0].access;

			if (superClass != null && superClass.id.equals("Ljava/lang/Record;")) { // ACC_RECORD is added by ASM through Record component attribute presence, don't trust the flag to handle stripping of the attribute
				ret |= Opcodes.ACC_RECORD;
			}
		} else {
			ret = Opcodes.ACC_PUBLIC;

			if (!implementers.isEmpty()) {
				ret |= Opcodes.ACC_INTERFACE | Opcodes.ACC_ABSTRACT;
			} else if (superClass != null && superClass.id.equals("Ljava/lang/Enum;")) {
				ret |= Opcodes.ACC_ENUM;
				if (childClasses.isEmpty()) ret |= Opcodes.ACC_FINAL;
			} else if (superClass != null && superClass.id.equals("Ljava/lang/Record;")) {
				ret |= Opcodes.ACC_RECORD;
				if (childClasses.isEmpty()) ret |= Opcodes.ACC_FINAL;
			} else if (interfaces.size() == 1 && interfaces.iterator().next().id.equals("Ljava/lang/annotation/Annotation;")) {
				ret |= Opcodes.ACC_ANNOTATION | Opcodes.ACC_INTERFACE | Opcodes.ACC_ABSTRACT;
			}
		}

		return ret;
	}

	public boolean isInterface() {
		return (getAccess() & Opcodes.ACC_INTERFACE) != 0;
	}

	public boolean isEnum() {
		return (getAccess() & Opcodes.ACC_ENUM) != 0;
	}

	public boolean isAnnotation() {
		return (getAccess() & Opcodes.ACC_ANNOTATION) != 0;
	}

	public boolean isRecord() {
		return (getAccess() & Opcodes.ACC_RECORD) != 0 || superClass != null && superClass.id.equals("Ljava/lang/Record;");
	}

	public MethodInstance getMethod(String id) {
		return methodIdx.get(id);
	}

	public FieldInstance getField(String id) {
		return fieldIdx.get(id);
	}

	public MethodInstance getMethod(String name, String desc) {
		if (desc != null) {
			return methodIdx.get(MethodInstance.getId(name, desc));
		} else {
			MethodInstance ret = null;

			for (MethodInstance method : methods) {
				if (method.origName.equals(name)) {
					if (ret != null) return null; // non-unique

					ret = method;
				}
			}

			return ret;
		}
	}

	public MethodInstance getMethod(String name, String desc, NameType nameType) {
		if (nameType == NameType.PLAIN) return getMethod(name, desc);

		MethodInstance ret = null;

		methodLoop: for (MethodInstance method : methods) {
			String mappedName = method.getName(nameType);

			if (mappedName == null || !name.equals(mappedName)) {
				continue;
			}

			if (desc != null) {
				assert desc.startsWith("(");
				int idx = 0;
				int pos = 1;
				boolean last = false;

				do {
					char c = desc.charAt(pos);
					ClassInstance match;

					if (c == ')') {
						if (idx != method.args.length) continue methodLoop;
						last = true;
						pos++;
						c = desc.charAt(pos);
						match = method.retType;
					} else {
						if (idx >= method.args.length) continue methodLoop;
						match = method.args[idx].type;
					}

					if (c == '[') { // array cls
						int dims = 1;
						while ((c = desc.charAt(++pos)) == '[') dims++;

						if (match.getArrayDimensions() != dims) continue methodLoop;
						match = match.elementClass;
					} else {
						if (match.isArray()) continue methodLoop;
					}

					int end;

					if (c != 'L') { // primitive cls
						end = pos + 1;
					} else {
						end = desc.indexOf(';', pos + 1) + 1;
						assert end != 0;
					}

					String clsMappedName = match.getName(nameType);
					if (clsMappedName == null) continue methodLoop;

					if (c != 'L') {
						if (clsMappedName.length() != end - pos || !desc.startsWith(clsMappedName, pos)) continue methodLoop;
					} else {
						if (clsMappedName.length() != end - pos - 2 || !desc.startsWith(clsMappedName, pos + 1)) continue methodLoop;
					}

					pos = end;
					idx++;
				} while (!last);
			}

			if (ret != null) return null; // non-unique

			ret = method;
		}

		return ret;
	}

	public FieldInstance getField(String name, String desc) {
		if (desc != null) {
			return fieldIdx.get(FieldInstance.getId(name, desc));
		} else {
			FieldInstance ret = null;

			for (FieldInstance field : fields) {
				if (field.origName.equals(name)) {
					if (ret != null) return null; // non-unique

					ret = field;
				}
			}

			return ret;
		}
	}

	public FieldInstance getField(String name, String desc, NameType nameType) {
		if (nameType == NameType.PLAIN) return getField(name, desc);

		FieldInstance ret = null;

		for (FieldInstance field : fields) {
			String mappedName = field.getName(nameType);

			if (mappedName == null || !name.equals(mappedName)) {
				continue;
			}

			if (desc != null) {
				String clsMappedName = field.type.getName(nameType);
				if (clsMappedName == null) continue;

				if (desc.startsWith("[") || !desc.endsWith(";")) {
					if (!desc.equals(clsMappedName)) continue;
				} else {
					if (desc.length() != clsMappedName.length() + 2 || !desc.startsWith(clsMappedName, 1)) continue;
				}
			}

			if (ret != null) return null; // non-unique

			ret = field;
		}

		return ret;
	}

	public MethodInstance resolveMethod(String name, String desc, boolean toInterface) {
		// toInterface = false: https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-5.html#jvms-5.4.3.3
		// toInterface = true: https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-5.html#jvms-5.4.3.4
		// TODO: access check after resolution

		assert asmNodes == null || isInterface() == toInterface;

		if (!toInterface) {
			MethodInstance ret = resolveSignaturePolymorphicMethod(name);
			if (ret != null) return ret;

			ret = getMethod(name, desc);
			if (ret != null) return ret; // <this> is unconditional

			ClassInstance cls = this;

			while ((cls = cls.superClass) != null) {
				ret = cls.resolveSignaturePolymorphicMethod(name);
				if (ret != null) return ret;

				ret = cls.getMethod(name, desc);
				if (ret != null) return ret;
			}

			return resolveInterfaceMethod(name, desc);
		} else {
			MethodInstance ret = getMethod(name, desc);
			if (ret != null) return ret; // <this> is unconditional

			if (superClass != null) {
				assert superClass.id.equals("Ljava/lang/Object;");

				ret = superClass.getMethod(name, desc);
				if (ret != null && (!ret.isReal() || (ret.access & (Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC)) == Opcodes.ACC_PUBLIC)) return ret;
			}

			return resolveInterfaceMethod(name, desc);
		}
	}

	private MethodInstance resolveSignaturePolymorphicMethod(String name) {
		if (id.equals("Ljava/lang/invoke/MethodHandle;")) { // check for signature polymorphic method - jvms-2.9
			MethodInstance ret = getMethod(name, "([Ljava/lang/Object;)Ljava/lang/Object;");
			final int reqFlags = Opcodes.ACC_VARARGS | Opcodes.ACC_NATIVE;

			if (ret != null && (!ret.isReal() || (ret.access & reqFlags) == reqFlags)) {
				return ret;
			}
		}

		return null;
	}

	private MethodInstance resolveInterfaceMethod(String name, String desc) {
		Queue<ClassInstance> queue = new ArrayDeque<>();
		Set<ClassInstance> queued = Util.newIdentityHashSet();
		ClassInstance cls = this;

		do {
			for (ClassInstance ifCls : cls.interfaces) {
				if (queued.add(ifCls)) queue.add(ifCls);
			}
		} while ((cls = cls.superClass) != null);

		if (queue.isEmpty()) return null;

		Set<MethodInstance> matches = Util.newIdentityHashSet();
		boolean foundNonAbstract = false;

		while ((cls = queue.poll()) != null) {
			MethodInstance ret = cls.getMethod(name, desc);

			if (ret != null
					&& (!ret.isReal() || (ret.access & (Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC)) == 0)) {
				matches.add(ret);

				if (ret.isReal() && (ret.access & Opcodes.ACC_ABSTRACT) == 0) { // jvms prefers the closest non-abstract method
					foundNonAbstract = true;
				}
			}

			for (ClassInstance ifCls : cls.interfaces) {
				if (queued.add(ifCls)) queue.add(ifCls);
			}
		}

		if (matches.isEmpty()) return null;
		if (matches.size() == 1) return matches.iterator().next();

		// non-abstract methods take precedence over non-abstract methods, remove all abstract ones if there's at least 1 non-abstract

		if (foundNonAbstract) {
			for (Iterator<MethodInstance> it = matches.iterator(); it.hasNext(); ) {
				MethodInstance m = it.next();

				if (!m.isReal() || (m.access & Opcodes.ACC_ABSTRACT) != 0) {
					it.remove();
				}
			}

			assert !matches.isEmpty();
			if (matches.size() == 1) return matches.iterator().next();
		}

		// eliminate not maximally specific method declarations, i.e. those that have a child method in matches

		for (Iterator<MethodInstance> it = matches.iterator(); it.hasNext(); ) {
			MethodInstance m = it.next();

			cmpLoop: for (MethodInstance m2 : matches) {
				if (m2 == m) continue;

				if (m2.cls.interfaces.contains(m.cls)) { // m2 is a direct child of m, so m isn't maximally specific
					it.remove();
					break;
				}

				queue.addAll(m2.cls.interfaces);

				while ((cls = queue.poll()) != null) {
					if (cls.interfaces.contains(m.cls)) { // m2 is an indirect child of m, so m isn't maximally specific
						it.remove();
						queue.clear();
						break cmpLoop;
					}

					queue.addAll(cls.interfaces);
				}
			}
		}

		// return an arbitrary choice

		return matches.iterator().next();
	}

	public FieldInstance resolveField(String name, String desc) {
		FieldInstance ret = getField(name, desc);
		if (ret != null) return ret;

		if (!interfaces.isEmpty()) {
			Deque<ClassInstance> queue = new ArrayDeque<>();
			queue.addAll(interfaces);
			ClassInstance cls;

			while ((cls = queue.pollFirst()) != null) {
				ret = cls.getField(name, desc);
				if (ret != null) return ret;

				for (ClassInstance iface : cls.interfaces) {
					queue.addFirst(iface);
				}
			}
		}

		ClassInstance cls = superClass;

		while (cls != null) {
			ret = cls.getField(name, desc);
			if (ret != null) return ret;

			cls = cls.superClass;
		}

		return null;
	}

	public MethodInstance getMethod(int pos) {
		if (pos < 0 || pos >= methods.length) throw new IndexOutOfBoundsException();
		if (asmNodes == null) throw new UnsupportedOperationException();

		return methods[pos];
	}

	public FieldInstance getField(int pos) {
		if (pos < 0 || pos >= fields.length) throw new IndexOutOfBoundsException();
		if (asmNodes == null) throw new UnsupportedOperationException();

		return fields[pos];
	}

	public MethodInstance[] getMethods() {
		return methods;
	}

	public FieldInstance[] getFields() {
		return fields;
	}

	public ClassInstance getOuterClass() {
		return outerClass;
	}

	public Set<ClassInstance> getInnerClasses() {
		return innerClasses;
	}

	public ClassInstance getSuperClass() {
		return superClass;
	}

	public Set<ClassInstance> getChildClasses() {
		return childClasses;
	}

	public Set<ClassInstance> getInterfaces() {
		return interfaces;
	}

	public Set<ClassInstance> getImplementers() {
		return implementers;
	}

	public Set<MethodInstance> getMethodTypeRefs() {
		return methodTypeRefs;
	}

	public Set<FieldInstance> getFieldTypeRefs() {
		return fieldTypeRefs;
	}

	public Set<String> getStrings() {
		return strings;
	}

	public boolean isShared() {
		return matchedClass == this;
	}

	@Override
	public boolean hasLocalTmpName() {
		return tmpName != null;
	}

	public void setTmpName(String tmpName) {
		this.tmpName = tmpName;
	}

	@Override
	public int getUid() {
		if (uid >= 0) {
			if (matchedClass != null && matchedClass.uid >= 0) {
				return Math.min(uid, matchedClass.uid);
			} else {
				return uid;
			}
		} else if (matchedClass != null) {
			return matchedClass.uid;
		} else {
			return -1;
		}
	}

	public void setUid(int uid) {
		this.uid = uid;
	}

	@Override
	public boolean hasMappedName() {
		return mappedName != null
				|| matchedClass != null && matchedClass.mappedName != null
				|| elementClass != null && elementClass.hasMappedName()
				/*|| outerClass != null && outerClass.hasMappedName() TODO: for anonymous only?*/;
	}

	public boolean hasNoFullyMappedName() {
		assert elementClass == null || outerClass == null; // array classes can't have an outer class

		return outerClass != null
				&& mappedName == null
				&& (matchedClass == null || matchedClass.mappedName == null);
	}

	public void setMappedName(String mappedName) {
		assert mappedName == null || !hasOuterName(mappedName);

		this.mappedName = mappedName;
	}

	@Override
	public String getMappedComment() {
		if (mappedComment != null) {
			return mappedComment;
		} else if (matchedClass != null) {
			return matchedClass.mappedComment;
		} else {
			return null;
		}
	}

	@Override
	public void setMappedComment(String comment) {
		if (comment != null && comment.isEmpty()) comment = null;

		this.mappedComment = comment;
	}

	@Override
	public boolean hasAuxName(int index) {
		return auxName != null && auxName.length > index && auxName[index] != null;
	}

	public void setAuxName(int index, String name) {
		assert name == null || !hasOuterName(name);

		if (this.auxName == null) this.auxName = new String[NameType.AUX_COUNT];
		this.auxName[index] = name;
	}

	public boolean isAssignableFrom(ClassInstance c) {
		if (c == this) return true;
		if (isPrimitive()) return false;

		if (!isInterface()) {
			ClassInstance sc = c;

			while ((sc = sc.superClass) != null) {
				if (sc == this) return true;
			}
		} else {
			if (implementers.isEmpty()) return false;

			// check if c directly implements this
			if (implementers.contains(c)) return true;

			// check if a superclass of c directly implements this
			ClassInstance sc = c;

			while ((sc = sc.superClass) != null) {
				if (implementers.contains(sc)) return true; // cls -> this
			}

			// check if c or a superclass of c implements this with one indirection
			sc = c;
			Queue<ClassInstance> toCheck = null;

			do {
				for (ClassInstance iface : sc.getInterfaces()) {
					assert iface != this; // already checked iface directly
					if (iface.interfaces.isEmpty()) continue;
					if (implementers.contains(iface)) return true; // cls -> if -> this

					if (toCheck == null) toCheck = new ArrayDeque<>();

					toCheck.addAll(iface.interfaces);
				}
			} while ((sc = sc.superClass) != null);

			// check if c or a superclass of c implements this with multiple indirections
			if (toCheck != null) {
				while ((sc = toCheck.poll()) != null) {
					for (ClassInstance iface : sc.getInterfaces()) {
						assert iface != this; // already checked

						if (implementers.contains(iface)) return true;

						toCheck.addAll(iface.interfaces);
					}
				}
			}
		}

		return false;
	}

	public ClassInstance getCommonSuperClass(ClassInstance o) {
		if (o == this) return this;
		if (isPrimitive() || o.isPrimitive()) return null;
		if (isAssignableFrom(o)) return this;
		if (o.isAssignableFrom(this)) return o;

		ClassInstance objCls = env.getCreateClassInstance("Ljava/lang/Object;");

		if (!isInterface() && !o.isInterface()) {
			ClassInstance sc = this;

			while ((sc = sc.superClass) != null && sc != objCls) {
				if (sc.isAssignableFrom(o)) return sc;
			}
		}

		if (!interfaces.isEmpty() || !o.interfaces.isEmpty()) {
			List<ClassInstance> ret = new ArrayList<>();
			Queue<ClassInstance> toCheck = new ArrayDeque<>();
			Set<ClassInstance> checked = Util.newIdentityHashSet();
			toCheck.addAll(interfaces);
			toCheck.addAll(o.interfaces);

			ClassInstance cls;

			while ((cls = toCheck.poll()) != null) {
				if (!checked.add(cls)) continue;

				if (cls.isAssignableFrom(o)) {
					ret.add(cls);
				} else {
					toCheck.addAll(cls.interfaces);
				}
			}

			if (!ret.isEmpty()) {
				if (ret.size() >= 1) {
					for (Iterator<ClassInstance> it = ret.iterator(); it.hasNext(); ) {
						cls = it.next();

						for (ClassInstance cls2 : ret) {
							if (cls != cls2 && cls.isAssignableFrom(cls2)) {
								it.remove();
								break;
							}
						}
					}
					// TODO: multiple options..
				}

				return ret.get(0);
			}
		}

		return objCls;
	}

	public void accept(ClassVisitor visitor, NameType nameType) {
		ClassNode cn = getMergedAsmNode();
		if (cn == null) throw new IllegalArgumentException("cls without asm node: "+this);

		synchronized (Util.asmNodeSync) {
			if (nameType != NameType.PLAIN) {
				AsmClassRemapper.process(cn, new AsmRemapper(env, nameType), visitor);
			} else {
				cn.accept(visitor);
			}
		}
	}

	public byte[] serialize(NameType nameType) {
		ClassWriter writer = new ClassWriter(0);
		accept(writer, nameType);

		return writer.toByteArray();
	}

	@Override
	public String toString() {
		return getDisplayName(NameType.PLAIN, true);
	}

	void addMethod(MethodInstance method) {
		if (method == null) throw new NullPointerException("null method");

		methodIdx.put(method.id, method);
		methods = Arrays.copyOf(methods, methods.length + 1);
		methods[methods.length - 1] = method;
	}

	void addField(FieldInstance field) {
		if (field == null) throw new NullPointerException("null field");

		fieldIdx.put(field.id, field);
		fields = Arrays.copyOf(fields, fields.length + 1);
		fields[fields.length - 1] = field;
	}

	public static String getId(String name) {
		if (name.isEmpty()) throw new IllegalArgumentException("empty class name");
		assert name.charAt(name.length() - 1) != ';' || name.charAt(0) == '[' : name;

		if (name.charAt(0) == '[') {
			assert name.charAt(name.length() - 1) == ';' || name.lastIndexOf('[') == name.length() - 2;

			return name;
		}

		return "L"+name+";";
	}

	public static String getName(String id) {
		return id.startsWith("L") ? id.substring(1, id.length() - 1) : id;
	}

	public static boolean hasOuterName(String name) {
		int pos = name.indexOf('$');

		return pos > 0 && name.charAt(pos - 1) != '/'; // ignore names starting with $
	}

	public static String getInnerName(String name) {
		return name.substring(name.lastIndexOf('$') + 1);
	}

	public static String getNestedName(String outerName, String innerName) {
		if (outerName == null || innerName == null) {
			return null;
		} else {
			return outerName + '$' + innerName;
		}
	}

	public static final Comparator<ClassInstance> nameComparator = Comparator.comparing(ClassInstance::getName);

	private static final ClassInstance[] noArrays = new ClassInstance[0];
	private static final MethodInstance[] noMethods = new MethodInstance[0];
	private static final FieldInstance[] noFields = new FieldInstance[0];

	final String id;
	private final URI origin;
	final ClassEnv env;
	private ClassNode[] asmNodes;
	private URI[] asmNodeOrigins;
	final boolean nameObfuscated;
	private final boolean input;
	final ClassInstance elementClass; // 0-dim class TODO: improve handling of array classes (references etc.)
	private ClassSignature signature;

	MethodInstance[] methods = noMethods;
	FieldInstance[] fields = noFields;
	final Map<String, MethodInstance> methodIdx = new HashMap<>();
	final Map<String, FieldInstance> fieldIdx = new HashMap<>();

	private ClassInstance[] arrays = noArrays;

	ClassInstance outerClass;
	final Set<ClassInstance> innerClasses = Util.newIdentityHashSet();

	ClassInstance superClass;
	final Set<ClassInstance> childClasses = Util.newIdentityHashSet();
	final Set<ClassInstance> interfaces = Util.newIdentityHashSet();
	final Set<ClassInstance> implementers = Util.newIdentityHashSet();

	final Set<MethodInstance> methodTypeRefs = Util.newIdentityHashSet();
	final Set<FieldInstance> fieldTypeRefs = Util.newIdentityHashSet();

	final Set<String> strings = new HashSet<>();

	private String tmpName;
	private int uid = -1;

	private String mappedName;
	private String mappedComment;

	private String[] auxName;

	private boolean matchable = true;
	private ClassInstance matchedClass;
} public final class FieldInstance extends MemberInstance<FieldInstance> {
	/**
	 * Create a shared unknown field.
	 */
	FieldInstance(ClassInstance cls, String origName, String desc, boolean isStatic) {
		this(cls, origName, desc, null, false, -1, isStatic);
	}

	/**
	 * Create a known field.
	 */
	FieldInstance(ClassInstance cls, String origName, String desc, FieldNode asmNode, boolean nameObfuscated, int position) {
		this(cls, origName, desc, asmNode, nameObfuscated, position, (asmNode.access & Opcodes.ACC_STATIC) != 0);
	}

	private FieldInstance(ClassInstance cls, String origName, String desc, FieldNode asmNode, boolean nameObfuscated, int position, boolean isStatic) {
		super(cls, getId(origName, desc), origName, nameObfuscated, position, isStatic);

		try {
			this.type = cls.getEnv().getCreateClassInstance(desc);
			this.asmNode = asmNode;
			this.signature = asmNode == null || asmNode.signature == null || !cls.isInput() ? null : FieldSignature.parse(asmNode.signature, cls.getEnv());
		} catch (InvalidSharedEnvQueryException e) {
			throw e.checkOrigin(cls);
		}

		type.fieldTypeRefs.add(this);
	}

	@Override
	public MatchableKind getKind() {
		return MatchableKind.FIELD;
	}

	@Override
	public String getDisplayName(NameType type, boolean full) {
		StringBuilder ret = new StringBuilder(64);

		ret.append(super.getDisplayName(type, full));
		ret.append(' ');
		ret.append(this.type.getDisplayName(type, full));

		return ret.toString();
	}

	@Override
	public String getDesc() {
		return type.id;
	}

	@Override
	public String getDesc(NameType type) {
		if (type == NameType.PLAIN || this.type.isPrimitive()) {
			return this.type.id;
		} else {
			String typeName = this.type.getName(type);

			return typeName != null ? ClassInstance.getId(typeName) : null;
		}
	}

	@Override
	public boolean isReal() {
		return asmNode != null;
	}

	public FieldNode getAsmNode() {
		return asmNode;
	}

	public ClassInstance getType() {
		return type;
	}

	@Override
	public int getAccess() {
		if (asmNode == null) {
			int ret = Opcodes.ACC_PUBLIC;
			if (isStatic) ret |= Opcodes.ACC_STATIC;
			if (isStatic && type == cls && cls.isEnum()) ret |= Opcodes.ACC_ENUM;
			if (isStatic && cls.isInterface()) ret |= Opcodes.ACC_FINAL;

			return ret;
		} else {
			return asmNode.access;
		}
	}

	public FieldSignature getSignature() {
		return signature;
	}

	public List<AbstractInsnNode> getInitializer() {
		return initializer;
	}

	public Set<MethodInstance> getReadRefs() {
		return readRefs;
	}

	public Set<MethodInstance> getWriteRefs() {
		return writeRefs;
	}

	@Override
	public boolean canBeRecordComponent() {
		return cls.isRecord() && !isStatic() && !isProtected() && !isPublic() && isFinal(); // jls requires private, but proguard(?) uses package-private too
	}

	@Override
	public MethodInstance getLinkedRecordComponent(NameType nameType) {
		if (!canBeRecordComponent()) return null;

		String name = nameType != null ? getName(nameType) : null;
		MethodInstance ret = null;

		for (MethodInstance method : cls.getMethods()) {
			if (method.canBeRecordComponent()
					&& method.getRetType().equals(type)
					&& (name == null || name.equals(method.getName(nameType)))
					&& (ret == null || !readRefs.contains(ret) && readRefs.contains(method))) {
				ret = method;
			}
		}

		return ret;
	}

	@Override
	protected String getUidString() {
		int uid = getUid();
		if (uid < 0) return null;

		return cls.env.getGlobal().fieldUidPrefix+uid;
	}

	@Override
	public boolean hasPotentialMatch() {
		if (matchedInstance != null) return true;
		if (!cls.hasMatch() || !isMatchable()) return false;

		for (FieldInstance o : cls.getMatch().getFields()) {
			if (ClassifierUtil.checkPotentialEquality(this, o)) return true;
		}

		return false;
	}

	@Override
	public boolean isFullyMatched(boolean recursive) {
		return matchedInstance != null;
	}

	public static String getId(String name, String desc) {
		return name+";;"+desc;
	}

	final FieldNode asmNode;
	final ClassInstance type;
	ClassInstance exactType;
	private final FieldSignature signature;
	List<AbstractInsnNode> initializer;

	final Set<MethodInstance> readRefs = Util.newIdentityHashSet();
	final Set<MethodInstance> writeRefs = Util.newIdentityHashSet();
} public enum MatchType {
	Class, Method, Field, MethodVar;
} public interface Matchable<T extends Matchable<T>> {
	MatchableKind getKind();

	String getId();
	String getName();
	String getName(NameType type);

	default String getDisplayName(NameType type, boolean full) {
		return getName(type);
	}

	boolean hasMappedName();
	boolean hasLocalTmpName();
	boolean hasAuxName(int index);

	String getMappedComment();
	void setMappedComment(String comment);

	Matchable<?> getOwner();
	ClassEnv getEnv();

	int getUid();

	boolean hasPotentialMatch();

	boolean isMatchable();
	boolean setMatchable(boolean matchable);

	default boolean hasMatch() {
		return getMatch() != null;
	}

	T getMatch();
	boolean isFullyMatched(boolean recursive);
	float getSimilarity();
	boolean isNameObfuscated();
} public abstract class MemberInstance<T extends MemberInstance<T>> implements Matchable<T> {
	@SuppressWarnings("unchecked")
	protected MemberInstance(ClassInstance cls, String id, String origName, boolean nameObfuscated, int position, boolean isStatic) {
		this.cls = cls;
		this.id = id;
		this.origName = origName;
		this.nameObfuscatedLocal = nameObfuscated;
		this.position = position;
		this.isStatic = isStatic;

		if (cls.isShared()) {
			matchedInstance = (T) this;
		}
	}

	public final ClassInstance getCls() {
		return cls;
	}

	@Override
	public final String getId() {
		return id;
	}

	@Override
	public final String getName() {
		return origName;
	}

	@Override
	public String getName(NameType type) {
		if (type == NameType.PLAIN) {
			return origName;
		} else if (type == NameType.UID_PLAIN) {
			int uid = getUid();
			if (uid >= 0) return getUidString();
		}

		boolean locTmp = type == NameType.MAPPED_LOCTMP_PLAIN || type == NameType.LOCTMP_PLAIN;
		String ret;

		if (type.mapped && cls.isInput() && (ret = getMappedName()) != null) {
			// MAPPED_*, mapped name available
		} else if (type.mapped && !isNameObfuscated()) {
			// MAPPED_*, local deobf
			ret = origName;
		} else if (type.mapped && matchedInstance != null && !matchedInstance.isNameObfuscated()) {
			// MAPPED_*, remote deobf
			ret = matchedInstance.origName;
		} else if (type.isAux() && cls.isInput() && (ret = getAuxName(type.getAuxIndex())) != null) {
			// *_AUX*, aux available
		} else if (type.tmp && cls.isInput() && (ret = getTmpName()) != null) {
			// MAPPED_TMP_* with obf name or TMP_*, remote name available
		} else if ((type.tmp || locTmp) && hierarchyData != null && hierarchyData.tmpName != null) {
			// MAPPED_TMP_* or MAPPED_LOCTMP_* with obf name or TMP_* or LOCTMP_*, local name available
			ret = hierarchyData.tmpName;
		} else if (type.plain) {
			ret = origName;
		} else {
			ret = null;
		}

		return ret;
	}

	@Override
	public String getDisplayName(NameType type, boolean full) {
		String name = getName(type);

		if (full) {
			return cls.getDisplayName(type, full) + "." + name;
		} else {
			return name;
		}
	}

	public abstract String getDesc();
	public abstract String getDesc(NameType type);
	public abstract boolean isReal();

	@Override
	public Matchable<?> getOwner() {
		return cls;
	}

	@Override
	public ClassEnv getEnv() {
		return cls.getEnv();
	}

	@Override
	public boolean isNameObfuscated() {
		return hierarchyData == null ? nameObfuscatedLocal : hierarchyData.nameObfuscated;
	}

	public int getPosition() {
		return position;
	}

	public abstract int getAccess();

	public boolean isStatic() {
		return isStatic;
	}

	public boolean isPublic() {
		return (getAccess() & Opcodes.ACC_PUBLIC) != 0;
	}

	public boolean isProtected() {
		return (getAccess() & Opcodes.ACC_PROTECTED) != 0;
	}

	public boolean isPrivate() {
		return (getAccess() & Opcodes.ACC_PRIVATE) != 0;
	}

	public boolean isFinal() {
		return (getAccess() & Opcodes.ACC_FINAL) != 0;
	}

	public boolean isSynthetic() {
		return (getAccess() & Opcodes.ACC_SYNTHETIC) != 0;
	}

	public abstract boolean canBeRecordComponent();
	public abstract MemberInstance<?> getLinkedRecordComponent(NameType nameType);

	void addParent(T parent) {
		assert parent.getCls() != getCls();
		assert parent != this;
		assert !children.contains(parent);

		if (parents.isEmpty()) parents = Util.newIdentityHashSet();

		parents.add(parent);
	}

	public Set<T> getParents() {
		return parents;
	}

	void addChild(T child) {
		assert child.getCls() != getCls();
		assert child != this;
		assert !parents.contains(child);

		if (children.isEmpty()) children = Util.newIdentityHashSet();

		children.add(child);
	}

	public Set<T> getChildren() {
		return children;
	}

	public T getHierarchyMatch() {
		assert hierarchyData != null; // only available for input classes

		if (hierarchyData.matchedHierarchy == null) return null;

		T ret = getMatch();
		if (ret != null) return ret;

		ClassEnv reqEnv = cls.getEnv();

		for (T m : hierarchyData.getMembers()) {
			ret = m.getMatch();

			if (ret != null) {
				ClassEnv env = m.cls.getEnv();

				if (env.isShared() || env == reqEnv) {
					return ret;
				}
			}
		}

		return null;
	}

	public Set<T> getAllHierarchyMembers() {
		assert hierarchyData != null; // only available for input classes

		return hierarchyData.getMembers();
	}

	@Override
	public boolean hasLocalTmpName() {
		return hierarchyData != null && hierarchyData.tmpName != null;
	}

	private String getTmpName() {
		assert hierarchyData != null; // only available for input classes

		if (hierarchyData.tmpName != null) {
			return hierarchyData.tmpName;
		} else if (hierarchyData.matchedHierarchy != null && hierarchyData.matchedHierarchy.tmpName != null) {
			return hierarchyData.matchedHierarchy.tmpName;
		}

		return null;
	}

	public void setTmpName(String tmpName) {
		hierarchyData.tmpName = tmpName;
	}

	@Override
	public int getUid() {
		assert hierarchyData != null; // only available for input classes

		if (hierarchyData.uid >= 0) {
			if (hierarchyData.matchedHierarchy != null && hierarchyData.matchedHierarchy.uid >= 0) {
				return Math.min(hierarchyData.uid, hierarchyData.matchedHierarchy.uid);
			} else {
				return hierarchyData.uid;
			}
		} else if (hierarchyData.matchedHierarchy != null) {
			return hierarchyData.matchedHierarchy.uid;
		} else {
			return -1;
		}
	}

	public void setUid(int uid) {
		hierarchyData.matchedHierarchy.uid = uid;
	}

	protected abstract String getUidString();

	@Override
	public boolean hasMappedName() {
		return getMappedName() != null;
	}

	private String getMappedName() {
		assert hierarchyData != null; // only available for input classes

		if (hierarchyData.mappedName != null) {
			return hierarchyData.mappedName;
		} else if (hierarchyData.matchedHierarchy != null && hierarchyData.matchedHierarchy.mappedName != null) {
			return hierarchyData.matchedHierarchy.mappedName;
		}

		return null;
	}

	public void setMappedName(String mappedName) {
		hierarchyData.mappedName = mappedName;
	}

	@Override
	public String getMappedComment() {
		if (mappedComment != null) {
			return mappedComment;
		} else if (matchedInstance != null) {
			return matchedInstance.mappedComment;
		} else {
			return null;
		}
	}

	@Override
	public void setMappedComment(String comment) {
		if (comment != null && comment.isEmpty()) comment = null;

		this.mappedComment = comment;
	}

	@Override
	public boolean hasAuxName(int index) {
		return getAuxName(index) != null;
	}

	private String getAuxName(int index) {
		assert hierarchyData != null; // only available for input classes

		if (hierarchyData.auxName != null && hierarchyData.auxName.length > index && hierarchyData.auxName[index] != null) {
			return hierarchyData.auxName[index];
		} else if (hierarchyData.matchedHierarchy != null
				&& hierarchyData.matchedHierarchy.auxName != null
				&& hierarchyData.matchedHierarchy.auxName.length > index
				&& hierarchyData.matchedHierarchy.auxName[index] != null) {
			return hierarchyData.matchedHierarchy.auxName[index];
		}

		return null;
	}

	public void setAuxName(int index, String name) {
		if (hierarchyData.auxName == null) hierarchyData.auxName = new String[NameType.AUX_COUNT];
		hierarchyData.auxName[index] = name;
	}

	@Override
	public boolean isMatchable() {
		return hierarchyData != null && hierarchyData.matchable && cls.isMatchable();
	}

	@Override
	public boolean setMatchable(boolean matchable) {
		if (!matchable && matchedInstance != null) return false;
		if (matchable && !cls.isMatchable()) return false;
		if (hierarchyData == null) return !matchable;
		if (!matchable && hierarchyData.matchedHierarchy != null) return false;

		hierarchyData.matchable = matchable;

		return true;
	}

	@Override
	public T getMatch() {
		return matchedInstance;
	}

	public void setMatch(T match) {
		assert match == null || isMatchable();
		assert match == null || cls == match.cls.getMatch();

		this.matchedInstance = match;
		this.hierarchyData.matchedHierarchy = match != null ? match.hierarchyData : null;
	}

	@Override
	public float getSimilarity() {
		if (matchedInstance == null) return 0;

		return SimilarityChecker.compare(this, matchedInstance);
	}

	@Override
	public String toString() {
		return getDisplayName(NameType.PLAIN, true);
	}

	public static final Comparator<MemberInstance<?>> nameComparator = Comparator.<MemberInstance<?>, String>comparing(MemberInstance::getName).thenComparing(m -> m.getDesc());

	final ClassInstance cls;
	final String id;
	final String origName;
	boolean nameObfuscatedLocal;
	final int position;
	final boolean isStatic;

	private Set<T> parents = Collections.emptySet();
	private Set<T> children = Collections.emptySet();
	MemberHierarchyData<T> hierarchyData;

	String mappedComment;

	T matchedInstance;
} public final class MethodInstance extends MemberInstance<MethodInstance> {
	/**
	 * Create a shared unknown method.
	 */
	MethodInstance(ClassInstance cls, String origName, String desc, boolean isStatic) {
		this(cls, origName, desc, null, false, -1, isStatic);
	}

	/**
	 * Create a known method.
	 */
	MethodInstance(ClassInstance cls, String origName, String desc, MethodNode asmNode, boolean nameObfuscated, int position) {
		this(cls, origName, desc, asmNode, nameObfuscated, position, (asmNode.access & Opcodes.ACC_STATIC) != 0);
	}

	private MethodInstance(ClassInstance cls, String origName, String desc, MethodNode asmNode, boolean nameObfuscated, int position, boolean isStatic) {
		super(cls, getId(origName, desc), origName, nameObfuscated, position, isStatic);

		try {
			this.real = asmNode != null;
			this.access = asmNode != null ? asmNode.access : approximateAccess(isStatic);
			this.args = gatherArgs(this, desc, asmNode);
			this.vars = cls.isInput() ? gatherVars(this, asmNode) : emptyVars;
			this.retType = cls.getEnv().getCreateClassInstance(Type.getReturnType(desc).getDescriptor());
			this.signature = asmNode == null || asmNode.signature == null || !cls.isInput() ? null : MethodSignature.parse(asmNode.signature, cls.getEnv());
			this.asmNode = !cls.getEnv().isShared() ? asmNode : null;
		} catch (InvalidSharedEnvQueryException e) {
			throw e.checkOrigin(cls);
		}

		classRefs.add(retType);
		retType.methodTypeRefs.add(this);
	}

	private static int approximateAccess(boolean isStatic) {
		int ret = Opcodes.ACC_PUBLIC;
		if (isStatic) ret |= Opcodes.ACC_STATIC;

		return ret;
	}

	private static MethodVarInstance[] gatherArgs(MethodInstance method, String desc, MethodNode asmNode) {
		Type[] argTypes = Type.getArgumentTypes(desc);
		if (argTypes.length == 0) return emptyVars;

		MethodVarInstance[] args = new MethodVarInstance[argTypes.length];
		List<LocalVariableNode> locals;
		InsnList il;
		AbstractInsnNode firstInsn;

		if (asmNode != null) {
			locals = asmNode.localVariables;
			il = asmNode.instructions;
			firstInsn = il.getFirst();
		} else {
			locals =  null;
			il = null;
			firstInsn = null;
		}

		int lvIdx = method.isStatic ? 0 : 1;

		for (int i = 0; i < argTypes.length; i++) {
			Type asmType = argTypes[i];
			ClassInstance type = method.cls.getEnv().getCreateClassInstance(asmType.getDescriptor());
			int asmIndex = -1;
			int startInsn = -1;
			int endInsn = -1;
			String name = null;

			if (locals != null) {
				for (int j = 0; j < locals.size(); j++) {
					LocalVariableNode n = locals.get(j);

					if (n.index == lvIdx && n.start == firstInsn) {
						assert n.desc.equals(type.id);

						asmIndex = j;
						startInsn = il.indexOf(n.start);
						endInsn = il.indexOf(n.end);
						name = n.name;

						break;
					}
				}
			}

			MethodVarInstance arg = new MethodVarInstance(method, true, i, lvIdx, asmIndex,
					type, startInsn, endInsn, 0,
					name,
					name == null || method.nameObfuscatedLocal || method.cls.nameObfuscated || !Util.isValidJavaIdentifier(name));
			args[i] = arg;

			method.classRefs.add(type);
			type.methodTypeRefs.add(method);

			lvIdx += type.getSlotSize();
		}

		return args;
	}

	private static MethodVarInstance[] gatherVars(MethodInstance method, MethodNode asmNode) {
		if (asmNode == null) return emptyVars;
		if (asmNode.localVariables == null) return emptyVars; // TODO: generate?
		if (asmNode.localVariables.isEmpty()) return emptyVars;

		InsnList il = asmNode.instructions;
		AbstractInsnNode firstInsn = il.getFirst();
		List<LocalVariableNode> vars = new ArrayList<>();

		lvLoop: for (int i = 0; i < asmNode.localVariables.size(); i++) {
			LocalVariableNode var = asmNode.localVariables.get(i);

			if (var.start == firstInsn) { // check if it's an arg
				if (var.index == 0 && !method.isStatic) continue;

				for (MethodVarInstance arg : method.args) {
					if (arg.asmIndex == i) { // var is an arg
						continue lvLoop;
					}
				}
			}

			vars.add(var);
		}

		if (vars.isEmpty()) return emptyVars;

		// stable sort by start bci
		Collections.sort(vars, Comparator.comparingInt(var -> il.indexOf(var.start))); // Collections.sort is specified as stable, List.sort isn't (only implNote in OpenJDK 8)

		MethodVarInstance[] ret = new MethodVarInstance[vars.size()];

		for (int i = 0; i < vars.size(); i++) {
			LocalVariableNode var = vars.get(i);

			assert var.name != null;
			assert method.args.length == 0 || var.index > method.args[method.args.length - 1].lvIndex;

			int startInsn = il.indexOf(var.start);
			int endInsn = il.indexOf(var.end);
			assert startInsn >= 0 && endInsn >= 0;

			AbstractInsnNode start = var.start;
			int startOpIdx = 0;

			while ((start = start.getPrevious()) != null) {
				if (start.getOpcode() >= 0) startOpIdx++;
			}

			ret[i] = new MethodVarInstance(method, false, i, var.index, asmNode.localVariables.indexOf(var),
					method.getEnv().getCreateClassInstance(var.desc), startInsn, endInsn, startOpIdx,
					var.name,
					var.name == null || method.nameObfuscatedLocal || method.cls.nameObfuscated || !Util.isValidJavaIdentifier(var.name));
		}

		return ret;
	}

	@Override
	public MatchableKind getKind() {
		return MatchableKind.METHOD;
	}

	@Override
	public String getDisplayName(NameType type, boolean full) {
		StringBuilder ret = new StringBuilder(64);
		ret.append(super.getDisplayName(type, full));
		ret.append('(');
		boolean first = true;

		for (MethodVarInstance arg : args) {
			if (first) {
				first = false;
			} else {
				ret.append(", ");
			}

			ret.append(arg.getType().getDisplayName(type, full));
		}

		ret.append(')');
		ret.append(retType.getDisplayName(type, full));

		return ret.toString();
	}

	@Override
	public String getDesc() {
		return getDesc(NameType.PLAIN);
	}

	@Override
	public String getDesc(NameType type) {
		int descStart = id.indexOf('(');
		if (type == NameType.PLAIN || id.indexOf('L', descStart + 1) < 0) return id.substring(descStart);

		StringBuilder ret = new StringBuilder(id.length());
		ret.append('(');

		for (MethodVarInstance arg : args) {
			if (!appendId(arg.getType(), type, ret)) return null;
		}

		ret.append(')');
		if (!appendId(retType, type, ret)) return null;

		return ret.toString();
	}

	private static boolean appendId(ClassInstance cls, NameType type, StringBuilder out) {
		if (type == NameType.PLAIN || cls.isPrimitive()) {
			out.append(cls.id);
		} else {
			String name = cls.getName(type);
			if (name == null) return false;

			if (cls.isArray()) {
				out.append(name);
			} else {
				out.append('L');
				out.append(name);
				out.append(';');
			}
		}

		return true;
	}

	@Override
	public boolean isReal() {
		return real;
	}

	public MethodNode getAsmNode() {
		return asmNode;
	}

	public MethodVarInstance getArg(int index) {
		if (index < 0 || index >= args.length) throw new IllegalArgumentException("invalid arg index: "+index);

		return args[index];
	}

	public MethodVarInstance getArg(String id) {
		return getArg(Integer.parseInt(id));
	}

	public MethodVarInstance getVar(int index) {
		if (index < 0 || index >= vars.length) throw new IllegalArgumentException("invalid var index: "+index);

		return vars[index];
	}

	public MethodVarInstance getVar(String id) {
		return getVar(Integer.parseInt(id));
	}

	public MethodVarInstance getVar(String id, boolean isArg) {
		return isArg ? getArg(id) : getVar(id);
	}

	public MethodVarInstance getArgOrVar(int lvIndex, int pos) {
		return getArgOrVar(lvIndex, pos, pos + 1);
	}

	public MethodVarInstance getArgOrVar(int lvIndex, int start, int end) {
		if (args.length > 0 && lvIndex <= args[args.length - 1].getLvIndex()) {
			for (int i = 0; i < args.length; i++) {
				MethodVarInstance arg = args[i];

				if (arg.getLvIndex() == lvIndex) {
					assert arg.getStartInsn() < 0
					|| start < arg.getEndInsn() && end > arg.getStartInsn()
					|| arg.getStartInsn() < end && arg.getEndInsn() > start;

					return arg;
				}
			}
		} else {
			MethodVarInstance candidate = null;
			boolean conflict = false;

			for (int i = 0; i < vars.length; i++) {
				MethodVarInstance var = vars[i];
				if (var.getLvIndex() != lvIndex) continue;

				if (start < var.getEndInsn() && end > var.getStartInsn()) { // requested interval within var interval (assumes matcher's interval never too loose)
					return var;
				} else if (var.getStartInsn() < end && var.getEndInsn() > start) { // var interval within requested interval, only allow unique match
					if (candidate != null) {
						conflict = true;
					} else {
						candidate = var;
					}
				}
			}

			if (candidate != null && !conflict) {
				return candidate;
			}
		}

		return null;
	}

	public MethodVarInstance[] getArgs() {
		return args;
	}

	public MethodVarInstance[] getVars() {
		return vars;
	}

	public boolean hasAllArgsMapped() {
		for (MethodVarInstance arg : args) {
			if (!arg.hasMappedName()) return false;
		}

		return true;
	}

	public ClassInstance getRetType() {
		return retType;
	}

	@Override
	public int getAccess() {
		return access;
	}

	public MethodSignature getSignature() {
		return signature;
	}

	@Override
	public boolean canBeRecordComponent() {
		return cls.isRecord() && !isStatic() && isPublic() && args.length == 0 && !retType.id.equals("V");
	}

	@Override
	public FieldInstance getLinkedRecordComponent(NameType nameType) {
		if (!canBeRecordComponent()) return null;

		String name = nameType != null ? getName(nameType) : null;
		FieldInstance ret = null;

		for (FieldInstance field : cls.getFields()) {
			if (field.canBeRecordComponent()
					&& field.getType().equals(retType)
					&& (name == null || name.equals(field.getName(nameType)))
					&& (ret == null || !fieldReadRefs.contains(ret) && fieldReadRefs.contains(field))) {
				ret = field;
			}
		}

		return ret;
	}

	public boolean isBridge() {
		return (getAccess() & Opcodes.ACC_BRIDGE) != 0;
	}

	public MethodType getType() {
		return type;
	}

	public Set<MethodInstance> getRefsIn() {
		return refsIn;
	}

	public Set<MethodInstance> getRefsOut() {
		return refsOut;
	}

	public Set<FieldInstance> getFieldReadRefs() {
		return fieldReadRefs;
	}

	public Set<FieldInstance> getFieldWriteRefs() {
		return fieldWriteRefs;
	}

	public Set<ClassInstance> getClassRefs() {
		return classRefs;
	}

	@Override
	public String getUidString() {
		int uid = getUid();
		if (uid < 0) return null;

		return cls.env.getGlobal().methodUidPrefix+uid;
	}

	@Override
	public boolean hasPotentialMatch() {
		if (matchedInstance != null) return true;
		if (!cls.hasMatch() || !isMatchable()) return false;

		for (MethodInstance o : cls.getMatch().getMethods()) {
			if (ClassifierUtil.checkPotentialEquality(this, o)) return true;
		}

		return false;
	}

	@Override
	public boolean isFullyMatched(boolean recursive) {
		if (matchedInstance == null) return false;

		boolean anyUnmatched = false;

		for (MethodVarInstance v : args) {
			if (!v.hasMatch()) {
				anyUnmatched = true;
				break;
			}
		}

		if (anyUnmatched) {
			for (MethodVarInstance a : args) {
				if (a.hasMatch()) continue;

				// check for any potential match to ignore methods that are impossible to match
				for (MethodVarInstance b : matchedInstance.args) {
					if (!b.hasMatch() && ClassifierUtil.checkPotentialEquality(a, b)) {
						return false;
					}
				}
			}
		}

		anyUnmatched = false;

		for (MethodVarInstance v : vars) {
			if (!v.hasMatch()) {
				anyUnmatched = true;
				break;
			}
		}

		if (anyUnmatched) {
			for (MethodVarInstance a : vars) {
				if (a.hasMatch()) continue;

				for (MethodVarInstance b : matchedInstance.vars) {
					if (!b.hasMatch() && ClassifierUtil.checkPotentialEquality(a, b)) {
						return false;
					}
				}
			}
		}

		return true;
	}

	public static String getId(String name, String desc) {
		return name+desc;
	}

	private static final MethodVarInstance[] emptyVars = new MethodVarInstance[0];

	final boolean real;
	final int access;
	final MethodVarInstance[] args;
	final ClassInstance retType;
	MethodVarInstance[] vars;
	final MethodSignature signature;
	private final MethodNode asmNode;

	MethodType type = MethodType.UNKNOWN;

	final Set<MethodInstance> refsIn = Util.newIdentityHashSet();
	final Set<MethodInstance> refsOut = Util.newIdentityHashSet();
	final Set<FieldInstance> fieldReadRefs = Util.newIdentityHashSet();
	final Set<FieldInstance> fieldWriteRefs = Util.newIdentityHashSet();
	final Set<ClassInstance> classRefs = Util.newIdentityHashSet();
} public final class MethodVarInstance implements Matchable<MethodVarInstance> {
	MethodVarInstance(MethodInstance method, boolean isArg, int index, int lvIndex, int asmIndex,
			ClassInstance type, int startInsn, int endInsn, int startOpIdx,
			String origName, boolean nameObfuscated) {
		this.method = method;
		this.isArg = isArg;
		this.index = index;
		this.lvIndex = lvIndex;
		this.asmIndex = asmIndex;
		this.type = type;
		this.startInsn = startInsn;
		this.endInsn = endInsn;
		this.startOpIdx = startOpIdx;
		this.origName = origName;
		this.nameObfuscated = nameObfuscated;
	}

	@Override
	public MatchableKind getKind() {
		return isArg ? MatchableKind.METHOD_ARG : MatchableKind.METHOD_VAR;
	}

	public MethodInstance getMethod() {
		return method;
	}

	public boolean isArg() {
		return isArg;
	}

	public int getIndex() {
		return index;
	}

	public int getLvIndex() {
		return lvIndex;
	}

	public int getAsmIndex() {
		return asmIndex;
	}

	public ClassInstance getType() {
		return type;
	}

	public int getStartInsn() {
		return startInsn;
	}

	public int getEndInsn() {
		return endInsn;
	}

	public int getStartOpIdx() {
		return startOpIdx;
	}

	@Override
	public String getId() {
		return Integer.toString(index);
	}

	public String getTypedId() {
		return (isArg ? "arg" : "lv").concat(getId());
	}

	@Override
	public String getName() {
		return origName;
	}

	@Override
	public String getName(NameType type) {
		if (type == NameType.PLAIN) {
			return hasValidOrigName() ? origName : getTypedId();
		} else if (type == NameType.UID_PLAIN) {
			ClassEnvironment env = method.cls.env.getGlobal();
			int uid = getUid();
			if (uid >= 0) return (isArg ? env.argUidPrefix : env.varUidPrefix)+uid;
		}

		boolean locTmp = type == NameType.MAPPED_LOCTMP_PLAIN || type == NameType.LOCTMP_PLAIN;
		String ret;

		if (type.mapped && mappedName != null) {
			// MAPPED_*, local name available
			ret = mappedName;
		} else if (type.mapped && matchedInstance != null && matchedInstance.mappedName != null) {
			// MAPPED_*, remote name available
			ret = matchedInstance.mappedName;
		} else if (type.mapped && !nameObfuscated && hasValidOrigName()) {
			// MAPPED_*, local deobf
			ret = origName;
		} else if (type.mapped && matchedInstance != null && !matchedInstance.nameObfuscated) {
			// MAPPED_*, remote deobf
			ret = matchedInstance.origName;
		} else if (type.isAux() && auxName != null && auxName.length > type.getAuxIndex() && auxName[type.getAuxIndex()] != null) {
			ret = auxName[type.getAuxIndex()];
		} else if (type.isAux() && matchedInstance != null && matchedInstance.auxName != null && matchedInstance.auxName.length > type.getAuxIndex() && matchedInstance.auxName[type.getAuxIndex()] != null) {
			ret = matchedInstance.auxName[type.getAuxIndex()];
		} else if (type.tmp && matchedInstance != null && matchedInstance.tmpName != null) {
			// MAPPED_TMP_* with obf name or TMP_*, remote name available
			ret = matchedInstance.tmpName;
		} else if ((type.tmp || locTmp) && tmpName != null) {
			// MAPPED_TMP_* or MAPPED_LOCTMP_* with obf name or TMP_* or LOCTMP_*, local name available
			ret = tmpName;
		} else if (type.plain) {
			ret = hasValidOrigName() ? origName : getTypedId();
		} else {
			ret = null;
		}

		return ret;
	}

	private boolean hasValidOrigName() {
		if (origName == null
				|| !Util.isValidJavaIdentifier(origName)
				|| origName.startsWith("arg") && origName.length() > 3 && origName.charAt(3) >= '0' && origName.charAt(3) <= '9' // conflicts with argX typed id
				|| origName.startsWith("lv") && origName.length() > 2 && origName.charAt(2) >= '0' && origName.charAt(2) <= '9') { // conflicts with lvX typed id
			return false;
		}

		// check if unique

		for (MethodVarInstance var : method.getArgs()) {
			if (var != this && origName.equals(var.origName)) return false;
		}

		for (MethodVarInstance var : method.getVars()) {
			if (var != this && origName.equals(var.origName)) return false;
		}

		return true;
	}

	@Override
	public Matchable<?> getOwner() {
		return method;
	}

	@Override
	public ClassEnv getEnv() {
		return method.getEnv();
	}

	@Override
	public boolean isNameObfuscated() {
		return nameObfuscated;
	}

	@Override
	public boolean hasLocalTmpName() {
		return tmpName != null;
	}

	public void setTmpName(String tmpName) {
		this.tmpName = tmpName;
	}

	@Override
	public int getUid() {
		return uid;
	}

	public void setUid(int uid) {
		this.uid = uid;
	}

	@Override
	public boolean hasMappedName() {
		return mappedName != null || matchedInstance != null && matchedInstance.mappedName != null;
	}

	public void setMappedName(String mappedName) {
		this.mappedName = mappedName;
	}

	@Override
	public String getMappedComment() {
		if (mappedComment != null) {
			return mappedComment;
		} else if (matchedInstance != null) {
			return matchedInstance.mappedComment;
		} else {
			return null;
		}
	}

	@Override
	public void setMappedComment(String comment) {
		if (comment != null && comment.isEmpty()) comment = null;

		this.mappedComment = comment;
	}

	@Override
	public boolean hasAuxName(int index) {
		return auxName != null && auxName.length > index && auxName[index] != null;
	}

	public void setAuxName(int index, String name) {
		if (this.auxName == null) this.auxName = new String[NameType.AUX_COUNT];
		this.auxName[index] = name;
	}

	@Override
	public boolean hasPotentialMatch() {
		if (matchedInstance != null) return true;
		if (!method.hasMatch() || !isMatchable()) return false;

		for (MethodVarInstance o : (isArg ? method.getMatch().getArgs() : method.getMatch().getVars())) {
			if (ClassifierUtil.checkPotentialEquality(this, o)) return true;
		}

		return false;
	}

	@Override
	public boolean isMatchable() {
		return matchable && method.isMatchable();
	}

	@Override
	public boolean setMatchable(boolean matchable) {
		if (!matchable && matchedInstance != null) return false;
		if (matchable && !method.isMatchable()) return false;

		this.matchable = matchable;

		return true;
	}

	@Override
	public MethodVarInstance getMatch() {
		return matchedInstance;
	}

	public void setMatch(MethodVarInstance match) {
		assert match == null || isMatchable();
		assert match == null || method == match.method.getMatch();

		this.matchedInstance = match;
	}

	@Override
	public boolean isFullyMatched(boolean recursive) {
		return matchedInstance != null;
	}

	@Override
	public float getSimilarity() {
		if (matchedInstance == null) return 0;

		return SimilarityChecker.compare(this, matchedInstance);
	}

	@Override
	public String toString() {
		return method+":"+getTypedId();
	}

	final MethodInstance method;
	final boolean isArg;
	final int index;
	final int lvIndex;
	final int asmIndex;
	final ClassInstance type;
	private final int startInsn; // inclusive
	private final int endInsn; // exclusive
	private final int startOpIdx;
	final String origName;
	final boolean nameObfuscated;

	private String tmpName;
	private int uid = -1;

	private String mappedName;
	String mappedComment;

	String[] auxName;

	private boolean matchable = true;
	private MethodVarInstance matchedInstance;
}
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.Callable;
import javafx.scene.control.ListView;
import javafx.scene.control.SplitPane;
import javafx.scene.control.TextField;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import matcher.NameType;
import matcher.classifier.ClassClassifier;
import matcher.classifier.ClassifierLevel;
import matcher.classifier.FieldClassifier;
import matcher.classifier.MethodClassifier;
import matcher.classifier.MethodVarClassifier;
import matcher.classifier.RankResult;
import matcher.type.ClassEnv;
import matcher.type.ClassEnvironment;
import matcher.type.ClassInstance;
import matcher.type.FieldInstance;
import matcher.type.MatchType;
import matcher.type.Matchable;
import matcher.type.MemberInstance;
import matcher.type.MethodInstance;
import matcher.type.MethodVarInstance;
			case "dup":
				opTypeA = OP_TYPE_ANY;
				break;
			case "name":
			case "mapped":
			case "mappedname":
			case "aux":
			case "auxname":
			case "aux2":
			case "aux2name":
				opTypeA = OP_TYPE_MATCHABLE;
				break;
			case "supercls":
				opTypeA = OP_TYPE_CLASS;
				break;
			case "instanceof":
				opTypeA = OP_TYPE_CLASS;
				opTypeB = OP_TYPE_CLASS;
				break;
			case "swap":
			case "eq":
			case "equals":
				opTypeA = opTypeB = OP_TYPE_ANY;
				break;
			case "and":
			case "or":
				opTypeA = opTypeB = OP_TYPE_BOOL;
				break;
			case "not":
				opTypeA = OP_TYPE_BOOL;
				break;
			case "startswith":
			case "endswith":
			case "contains":
				opTypeA =  opTypeB = OP_TYPE_STRING;
				break;
			case "class":
			case "package":
				opTypeA = OP_TYPE_STRING;
				break;
			default:
				if (part.length() >= 2 && part.charAt(0) == '"' && part.charAt(part.length() - 1) == '"') {
					part = part.substring(0, part.length() - 1);
				}

				stack.add(part);
				break;
			}

			Object opA = null;
			Object opB = null;

			for (int i = 0; i < 2; i++) {
				byte type = i == 0 ? opTypeB : opTypeA;
				if (type == OP_TYPE_NONE) continue;

				if (stack.isEmpty()) {
					System.err.println("stack underflow");
					return null;
				}

				Object operand = stack.remove(stack.size() - 1);

				boolean valid = type == OP_TYPE_ANY
						|| type == OP_TYPE_MATCHABLE && (operand instanceof RankResult<?> || operand instanceof Matchable<?>)
						|| type == OP_TYPE_CLASS && (operand instanceof RankResult<?> || operand instanceof ClassInstance || operand instanceof String)
						|| type == OP_TYPE_STRING && operand instanceof String
						|| type == OP_TYPE_BOOL && operand instanceof Boolean
						|| type == OP_TYPE_INT && operand instanceof Integer
						|| type == OP_TYPE_COMPARABLE && operand instanceof Comparable<?>;

				if (!valid) {
					System.err.println("invalid operand type");
					return null;
				}

				if (type == OP_TYPE_MATCHABLE && operand instanceof RankResult<?>) {
					operand = ((RankResult<? extends Matchable<?>>) operand).getSubject();
				} else if (type == OP_TYPE_CLASS && operand instanceof RankResult<?>) {
					operand = getClass(((RankResult<? extends Matchable<?>>) operand).getSubject());
				} else if (type == OP_TYPE_CLASS && operand instanceof String) {
					ClassInstance cls = env.getClsByName((String) operand);

					if (cls == null) {
						System.err.println("unknown class "+operand);
						return null;
					} else {
						operand = cls;
					}
				}

				if (i == 0) {
					opB = operand;
				} else {
					opA = operand;
				}
			}

			//System.out.printf("opA: %s, opB: %s%n", opA, opB);

			switch (op) {
			case "a":
				stack.add(itemA);
				break;
			case "b":
				stack.add(resB);
				break;
			case "dup":
				stack.add(opA);
				stack.add(opA);
				break;
			case "swap":
				stack.add(opB);
				stack.add(opA);
				break;
			case "name":
				stack.add(((Matchable<?>) opA).getName());
				break;
			case "mapped":
			case "mappedname":
				stack.add(((Matchable<?>) opA).getName(NameType.MAPPED_PLAIN));