static String format(Collection<? extends Matchable<?>> c, NameType nameType) {
	return format(c.stream(), nameType);
} static String getName(Matchable<?> m, NameType nameType) {
	String ret = m.getDisplayName(nameType.withMapped(false), true);
	String mapped = m.getDisplayName(nameType.withMapped(true), true);

	if (Objects.equals(ret, mapped)) {
		return ret;
	} else {
		return ret+" - "+mapped;
	}
} static String formatClass(Collection<? extends MemberInstance<?>> c, NameType nameType) {
	return c.stream().map(v -> getName(v.getCls(), nameType)).sorted().collect(Collectors.joining("\n"));
} 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 Util {
	public static <T> Set<T> newIdentityHashSet() {
		return Collections.newSetFromMap(new IdentityHashMap<>());//new IdentityHashSet<>();
	}

	public static <T> Set<T> newIdentityHashSet(Collection<? extends T> c) {
		Set<T> ret = Collections.newSetFromMap(new IdentityHashMap<>(c.size()));
		ret.addAll(c);

		return ret;//new IdentityHashSet<>(c);
	}

	public static <T> Set<T> copySet(Set<T> set) {
		if (set instanceof HashSet) {
			return new HashSet<>(set);
		} else {
			return newIdentityHashSet(set);
		}
	}

	public static FileSystem iterateJar(Path archive, boolean autoClose, Consumer<Path> handler) {
		boolean existing = false;
		FileSystem fs = null;

		try {
			URI uri = new URI("jar:"+archive.toUri().toString());

			synchronized (Util.class) {
				try {
					fs = FileSystems.getFileSystem(uri);
					existing = true;
					autoClose = false;
				} catch (FileSystemNotFoundException e) {
					fs = FileSystems.newFileSystem(uri, Collections.emptyMap());
					existing = false;
				}
			}

			Files.walkFileTree(fs.getPath("/"), new SimpleFileVisitor<Path>() {
				@Override
				public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
					if (file.toString().endsWith(".class")) {
						handler.accept(file);
					}

					return FileVisitResult.CONTINUE;
				}
			});
		} catch (IOException e) {
			closeSilently(fs);
			throw new UncheckedIOException(e);
		} catch (URISyntaxException e) {
			closeSilently(fs);
			throw new RuntimeException(e);
		} catch (Throwable t) {
			closeSilently(fs);
			throw t;
		}

		if (autoClose) closeSilently(fs);

		return autoClose || existing ? null : fs;
	}

	public static boolean clearDir(Path path, Predicate<Path> disallowed) throws IOException {
		try (Stream<Path> stream = Files.walk(path, FileVisitOption.FOLLOW_LINKS)) {
			if (stream.anyMatch(disallowed)) return false;
		}

		AtomicBoolean ret = new AtomicBoolean(true);

		Files.walkFileTree(path, new SimpleFileVisitor<Path>() {
			@Override
			public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
				if (disallowed.test(file)) {
					ret.set(false);

					return FileVisitResult.TERMINATE;
				} else {
					Files.delete(file);

					return FileVisitResult.CONTINUE;
				}
			}

			@Override
			public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
				if (exc != null) throw exc;
				if (!dir.equals(path)) Files.delete(dir);

				return FileVisitResult.CONTINUE;
			}
		});

		return ret.get();
	}

	public static void closeSilently(Closeable c) {
		if (c == null) return;

		try {
			c.close();
		} catch (IOException e) { }
	}

	public static boolean isCallToInterface(MethodInsnNode insn) {
		assert insn.itf || insn.getOpcode() != Opcodes.INVOKEINTERFACE;

		return insn.itf;
		/*return insn.getOpcode() == Opcodes.INVOKEINTERFACE
				|| (insn.getOpcode() == Opcodes.INVOKESPECIAL || insn.getOpcode() == Opcodes.INVOKESTATIC) && insn.itf;*/
	}

	public static boolean isCallToInterface(Handle handle) {
		assert handle.isInterface() || handle.getTag() != Opcodes.H_INVOKEINTERFACE;

		return handle.isInterface();

		/*return handle.getTag() == Opcodes.H_INVOKEINTERFACE
				|| (handle.getTag() == Opcodes.H_INVOKESPECIAL || handle.getTag() == Opcodes.H_NEWINVOKESPECIAL || handle.getTag() == Opcodes.H_INVOKESTATIC) && handle.isInterface();*/
	}

	public static String formatAccessFlags(int access, AFElementType type) {
		int assoc = type.assoc;
		StringBuilder sb = new StringBuilder();

		for (int i = 0; i < accessFlags.length; i++) {
			if ((accessAssoc[i] & assoc) == 0) continue;
			if ((access & accessFlags[i]) == 0) continue;

			if (sb.length() != 0) sb.append(' ');
			sb.append(accessNames[i]);

			access &= ~accessFlags[i];
		}

		if (access != 0) {
			if (sb.length() != 0) sb.append(' ');
			sb.append("0x");
			sb.append(Integer.toHexString(access));
		}

		return sb.toString();
	}

	public enum AFElementType {
		Class(1), Method(2), Field(4), Parameter(8), InnerClass(16);

		AFElementType(int assoc) {
			this.assoc = assoc;
		}

		final int assoc;
	}

	private static final int[] accessFlags = new int[] { Opcodes.ACC_PUBLIC, Opcodes.ACC_PRIVATE, Opcodes.ACC_PROTECTED, Opcodes.ACC_STATIC,
			Opcodes.ACC_FINAL, Opcodes.ACC_SUPER, Opcodes.ACC_SYNCHRONIZED, Opcodes.ACC_VOLATILE, Opcodes.ACC_BRIDGE, Opcodes.ACC_VARARGS,
			Opcodes.ACC_TRANSIENT, Opcodes.ACC_NATIVE, Opcodes.ACC_INTERFACE, Opcodes.ACC_ABSTRACT, Opcodes.ACC_STRICT, Opcodes.ACC_SYNTHETIC,
			Opcodes.ACC_ANNOTATION, Opcodes.ACC_ENUM, Opcodes.ACC_MANDATED };
	private static final String[] accessNames = new String[] { "public", "private", "protected", "static",
			"final", "super", "synchronized", "volatile", "bridge", "varargs",
			"transient", "native", "interface", "abstract", "strict", "synthetic",
			"annotation", "enum", "mandated" };
	private static final byte[] accessAssoc = new byte[] { 7, 7, 7, 6,
			15, 1, 2, 4, 2, 2,
			4, 2, 1, 3, 2, 15,
			1, 21, 8 };

	public static Handle getTargetHandle(Handle bsm, Object[] bsmArgs) {
		if (isJavaLambdaMetafactory(bsm)) {
			return (Handle) bsmArgs[1];
		} else if (isIrrelevantBsm(bsm)) {
			return null;
		} else {
			System.out.printf("unknown invokedynamic bsm: %s/%s%s (tag=%d iif=%b)%n", bsm.getOwner(), bsm.getName(), bsm.getDesc(), bsm.getTag(), bsm.isInterface());

			return null;
		}
	}

	public static boolean isJavaLambdaMetafactory(Handle bsm) {
		return bsm.getTag() == Opcodes.H_INVOKESTATIC
				&& bsm.getOwner().equals("java/lang/invoke/LambdaMetafactory")
				&& (bsm.getName().equals("metafactory")
						&& bsm.getDesc().equals("(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;")
						|| bsm.getName().equals("altMetafactory")
						&& bsm.getDesc().equals("(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;"))
				&& !bsm.isInterface();
	}

	public static boolean isIrrelevantBsm(Handle bsm) {
		return bsm.getOwner().equals("java/lang/invoke/StringConcatFactory")
				|| bsm.getOwner().equals("java/lang/runtime/ObjectMethods");
	}

	public static boolean isValidJavaIdentifier(String s) {
		if (s.isEmpty()) return false;

		int cp = s.codePointAt(0);
		if (!Character.isJavaIdentifierStart(cp)) return false;

		for (int i = Character.charCount(cp), max = s.length(); i < max; i += Character.charCount(cp)) {
			cp = s.codePointAt(i);
			if (!Character.isJavaIdentifierPart(cp)) return false;
		}

		return true;
	}

	public static int compareNatural(String a, String b) {
		final int lenA = a.length();
		final int lenB = b.length();
		int posA = 0;
		int lastSize = 0;

		for (int max = Math.min(lenA, lenB); posA < max; ) {
			int cA = Character.codePointAt(a, posA);
			int cB = Character.codePointAt(b, posA);

			if (cA != cB) break;

			lastSize = Character.charCount(cA);
			posA += lastSize;
		}

		if (posA == lenA && lenA == lenB) return 0;

		int posB = posA = posA - lastSize;
		int endA = -1;
		int endB = -1;

		for (;;) {
			int startA = posA;
			boolean isNumA = false;

			while (posA < lenA) {
				int c = Character.codePointAt(a, posA);

				if ((c >= '0' && c <= '9') != isNumA) {
					if (posA == startA) {
						isNumA = !isNumA; // isNum had the wrong initial value
					} else {
						if (endA < posA) {
							endA = posA;

							do {
								endA += Character.charCount(c);
							} while (endA < lenA && (c = Character.codePointAt(a, endA)) != '.' && c != '/');
						}

						break;
					}
				} else if (c == '.' || c == '/') {
					endA = posA; // unconditionally mark end to handle 0-length segments (those won't be re-visited)
					if (posA == startA) posA++; // consume only if first to avoid polluting comparisons within segments, otherwise trigger revisit
					break;
				} else if (c == '$') {
					if (posA == startA) posA++;
					break;
				}

				posA += Character.charCount(c);
			}

			int startB = posB;
			boolean isNumB = false;

			while (posB < lenB) {
				int c = Character.codePointAt(b, posB);

				if ((c >= '0' && c <= '9') != isNumB) {
					if (posB == startB) {
						isNumB = !isNumB;
					} else {
						if (endB < posB) {
							endB = posB;

							do {
								endB += Character.charCount(c);
							} while (endB < lenB && (c = Character.codePointAt(b, endB)) != '.' && c != '/');
						}

						break;
					}
				} else if (c == '.' || c == '/') {
					endB = posB;
					if (posB == startB) posB++;
					break;
				} else if (c == '$') {
					if (posB == startB) posB++;
					break;
				}

				posB += Character.charCount(c);
			}

			boolean hasEndA = endA >= startA && endA < lenA; // segment separator exists after current region
			boolean hasEndB = endB >= startB && endB < lenB;

			if (hasEndA != hasEndB) {
				return hasEndA ? 1 : -1;
			}

			if (isNumA && isNumB) {
				int segLenA = posA - startA;
				int segLenB = posB - startB;

				if (segLenA != segLenB) {
					return segLenA < segLenB ? -1 : 1;
				}
			}

			while (startA < posA) {
				if (startB == posB) return 1;

				int cA = Character.codePointAt(a, startA);
				int cB = Character.codePointAt(b, startB);

				if (cA != cB) {
					return cA < cB ? -1 : 1;
				}

				startA += Character.charCount(cA);
				startB += Character.charCount(cB);
			}

			if (startB != posB) return -1;
		}
	}

	public static final Object asmNodeSync = new Object();
} public enum AFElementType {
	Class(1), Method(2), Field(4), Parameter(8), InnerClass(16);

	AFElementType(int assoc) {
		this.assoc = assoc;
	}

	final int assoc;
} public class Gui extends Application {
	@Override
	public void start(Stage stage) {
		Matcher.init();

		env = new ClassEnvironment();
		matcher = new Matcher(env);

		GridPane border = new GridPane();

		ColumnConstraints colConstraint = new ColumnConstraints();
		colConstraint.setPercentWidth(50);
		border.getColumnConstraints().addAll(colConstraint, colConstraint);

		RowConstraints defaultRowConstraints = new RowConstraints();
		RowConstraints contentRowConstraints = new RowConstraints();
		contentRowConstraints.setVgrow(Priority.ALWAYS);
		border.getRowConstraints().addAll(defaultRowConstraints, contentRowConstraints, defaultRowConstraints);

		menu = new MainMenuBar(this);
		components.add(menu);
		border.add(menu, 0, 0, 2, 1);

		srcPane = new MatchPaneSrc(this);
		components.add(srcPane);
		border.add(srcPane, 0, 1);

		dstPane = new MatchPaneDst(this, srcPane);
		components.add(dstPane);
		border.add(dstPane, 1, 1);

		bottomPane = new BottomPane(this, srcPane, dstPane);
		components.add(bottomPane);
		border.add(bottomPane, 0, 2, 2, 1);

		scene = new Scene(border, 1400, 800);
		Shortcuts.init(this);

		for (Consumer<Gui> l : loadListeners) {
			l.accept(this);
		}

		stage.setScene(scene);
		stage.setTitle("Matcher");
		stage.show();
	}

	@Override
	public void stop() throws Exception {
		threadPool.shutdown();
	}

	public ClassEnvironment getEnv() {
		return env;
	}

	public Matcher getMatcher() {
		return matcher;
	}

	public Scene getScene() {
		return scene;
	}

	public void addListeningComponent(IGuiComponent component) {
		components.add(component);
	}

	public MainMenuBar getMenu() {
		return menu;
	}

	public MatchPaneSrc getSrcPane() {
		return srcPane;
	}

	public MatchPaneDst getDstPane() {
		return dstPane;
	}

	public BottomPane getBottomPane() {
		return bottomPane;
	}

	public SortKey getSortKey() {
		return sortKey;
	}

	public void setSortKey(SortKey sortKey) {
		if (sortKey == null) throw new NullPointerException("null sort key");
		if (this.sortKey == sortKey) return;

		this.sortKey = sortKey;

		for (IGuiComponent c : components) {
			c.onViewChange();
		}
	}

	public boolean isSortMatchesAlphabetically() {
		return sortMatchesAlphabetically;
	}

	public void setSortMatchesAlphabetically(boolean value) {
		if (this.sortMatchesAlphabetically == value) return;

		this.sortMatchesAlphabetically = value;

		for (IGuiComponent c : components) {
			c.onViewChange();
		}
	}

	public boolean isUseClassTreeView() {
		return useClassTreeView;
	}

	public void setUseClassTreeView(boolean value) {
		if (this.useClassTreeView == value) return;

		this.useClassTreeView = value;

		for (IGuiComponent c : components) {
			c.onViewChange();
		}
	}

	public boolean isShowNonInputs() {
		return showNonInputs;
	}

	public void setShowNonInputs(boolean showNonInputs) {
		if (this.showNonInputs == showNonInputs) return;

		this.showNonInputs = showNonInputs;

		for (IGuiComponent c : components) {
			c.onViewChange();
		}
	}

	public boolean isUseDiffColors() {
		return useDiffColors;
	}

	public void setUseDiffColors(boolean useDiffColors) {
		if (this.useDiffColors == useDiffColors) return;

		this.useDiffColors = useDiffColors;

		for (IGuiComponent c : components) {
			c.onViewChange();
		}
	}

	public NameType getNameType() {
		return nameType;
	}

	public void setNameType(NameType value) {
		if (this.nameType == value) return;

		this.nameType = value;

		for (IGuiComponent c : components) {
			c.onViewChange();
		}
	}


	public BuiltinDecompiler getDecompiler() {
		return decompiler;
	}

	public void setDecompiler(BuiltinDecompiler value) {
		if (this.decompiler == value) return;

		this.decompiler = value;

		for (IGuiComponent c : components) {
			c.onViewChange();
		}
	}

	public void onProjectChange() {
		for (IGuiComponent c : components) {
			c.onProjectChange();
		}
	}

	public void onMappingChange() {
		for (IGuiComponent c : components) {
			c.onMappingChange();
		}
	}

	public void onMatchChange(Set<MatchType> types) {
		for (IGuiComponent c : components) {
			c.onMatchChange(types);
		}
	}

	public static <T> CompletableFuture<T> runAsyncTask(Callable<T> task) {
		Task<T> jfxTask = new Task<T>() {
			@Override
			protected T call() throws Exception {
				return task.call();
			}
		};

		CompletableFuture<T> ret = new CompletableFuture<T>();

		jfxTask.setOnSucceeded(event -> ret.complete(jfxTask.getValue()));
		jfxTask.setOnFailed(event -> ret.completeExceptionally(jfxTask.getException()));
		jfxTask.setOnCancelled(event -> ret.cancel(false));

		threadPool.execute(jfxTask);

		return ret;
	}

	public void runProgressTask(String labelText, Consumer<DoubleConsumer> task) {
		runProgressTask(labelText, task, null, null);
	}

	public void runProgressTask(String labelText, Consumer<DoubleConsumer> task, Runnable onSuccess, Consumer<Throwable> onError) {
		Stage stage = new Stage(StageStyle.UTILITY);
		stage.initOwner(this.scene.getWindow());
		VBox pane = new VBox(GuiConstants.padding);

		stage.setScene(new Scene(pane));
		stage.initModality(Modality.APPLICATION_MODAL);
		stage.setOnCloseRequest(event -> event.consume());
		stage.setResizable(false);
		stage.setTitle("Operation progress");

		pane.setPadding(new Insets(GuiConstants.padding));

		pane.getChildren().add(new Label(labelText));

		ProgressBar progress = new ProgressBar(0);
		progress.setPrefWidth(400);
		pane.getChildren().add(progress);

		stage.show();

		Task<Void> jfxTask = new Task<Void>() {
			@Override
			protected Void call() throws Exception {
				task.accept(cProgress -> Platform.runLater(() -> progress.setProgress(cProgress)));

				return null;
			}
		};

		jfxTask.setOnSucceeded(event -> {
			stage.hide();
			if (onSuccess != null) onSuccess.run();
		});

		jfxTask.setOnFailed(event -> {
			stage.hide();
			if (onError != null) onError.accept(jfxTask.getException());
		});

		threadPool.execute(jfxTask);
	}

	public void showAlert(AlertType type, String title, String headerText, String text) {
		Alert alert = new Alert(type, text);

		alert.setTitle(title);
		alert.setHeaderText(headerText);

		Platform.runLater(() -> alert.getDialogPane().getScene().getWindow().sizeToScene()); // work around linux display bug  (JDK-8193502)

		alert.showAndWait();
	}

	public boolean requestConfirmation(String title, String headerText, String text) {
		Alert alert = new Alert(AlertType.CONFIRMATION, text);

		alert.setTitle(title);
		alert.setHeaderText(headerText);

		Platform.runLater(() -> alert.getDialogPane().getScene().getWindow().sizeToScene()); // work around linux display bug (JDK-8193502)

		return alert.showAndWait().orElse(ButtonType.CANCEL) == ButtonType.OK;
	}

	public static SelectedFile requestFile(String title, Window parent, List<ExtensionFilter> extensionFilters, boolean isOpen) {
		FileChooser fileChooser = setupFileChooser(title, extensionFilters);

		File file = isOpen ? fileChooser.showOpenDialog(parent) : fileChooser.showSaveDialog(parent);
		if (file == null) return null;

		lastChooserFile = file.getParentFile();

		return new SelectedFile(file.toPath(), fileChooser.getSelectedExtensionFilter());
	}

	public static List<SelectedFile> requestFiles(String title, Window parent, List<ExtensionFilter> extensionFilters) {
		FileChooser fileChooser = setupFileChooser(title, extensionFilters);

		List<File> file = fileChooser.showOpenMultipleDialog(parent);
		if (file == null || file.isEmpty()) return Collections.emptyList();

		lastChooserFile = file.get(0).getParentFile();

		return file.stream().map(file1 -> new SelectedFile(file1.toPath(), fileChooser.getSelectedExtensionFilter())).collect(Collectors.toList());
	}

	private static FileChooser setupFileChooser(String title, List<ExtensionFilter> extensionFilters) {
		FileChooser fileChooser = new FileChooser();

		fileChooser.setTitle(title);
		fileChooser.getExtensionFilters().addAll(extensionFilters);

		while (lastChooserFile != null && !lastChooserFile.isDirectory()) {
			lastChooserFile = lastChooserFile.getParentFile();
		}

		if (lastChooserFile != null)
			fileChooser.setInitialDirectory(lastChooserFile);

		return fileChooser;
	}

	public static class SelectedFile {
		SelectedFile(Path path, ExtensionFilter filter) {
			this.path = path;
			this.filter = filter;
		}

		public final Path path;
		public final ExtensionFilter filter;
	}

	public static Path requestDir(String title, Window parent) {
		DirectoryChooser fileChooser = new DirectoryChooser();

		fileChooser.setTitle(title);

		while (lastChooserFile != null && !lastChooserFile.isDirectory()) {
			lastChooserFile = lastChooserFile.getParentFile();
		}

		if (lastChooserFile != null) fileChooser.setInitialDirectory(lastChooserFile);

		File file = fileChooser.showDialog(parent);
		if (file == null) return null;

		lastChooserFile = file;

		return file.toPath();
	}

	public enum SortKey {
		Name, MappedName, MatchStatus, Similarity;
	}

	public static final List<Consumer<Gui>> loadListeners = new ArrayList<>();

	private static final ExecutorService threadPool = Executors.newCachedThreadPool();

	private ClassEnvironment env;
	private Matcher matcher;

	private Scene scene;
	private final Collection<IGuiComponent> components = new ArrayList<>();

	private MainMenuBar menu;
	private MatchPaneSrc srcPane;
	private MatchPaneDst dstPane;
	private BottomPane bottomPane;

	private SortKey sortKey = SortKey.Name;
	private boolean sortMatchesAlphabetically;
	private boolean useClassTreeView;
	private boolean showNonInputs;
	private boolean useDiffColors;

	private NameType nameType = NameType.MAPPED_PLAIN;
	private BuiltinDecompiler decompiler = BuiltinDecompiler.CFR;

	private static File lastChooserFile;
} public class GuiConstants {
	public static final double padding = 5;
} public interface IGuiComponent {
	default void onProjectChange() { }

	default void onViewChange() { }

	default void onMappingChange() { }
	default void onMatchChange(Set<MatchType> types) { }

	default void onClassSelect(ClassInstance cls) { }
	default void onMethodSelect(MethodInstance method) { }
	default void onFieldSelect(FieldInstance field) { }
	default void onMethodVarSelect(MethodVarInstance arg) { }

	default void onMatchListRefresh() { }
} public interface ISelectionProvider {
	ClassInstance getSelectedClass();
	MemberInstance<?> getSelectedMember();
	MethodInstance getSelectedMethod();
	FieldInstance getSelectedField();
	MethodVarInstance getSelectedMethodVar();

	default RankResult<?> getSelectedRankResult(MatchType type) {
		return null;
	}
} 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();
}
import static matcher.gui.tab.ClassInfoTab.format;
import static matcher.gui.tab.ClassInfoTab.getName;
import static matcher.gui.tab.MethodInfoTab.formatClass;
import javafx.geometry.Insets;
import javafx.geometry.VPos;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.Tab;
import javafx.scene.layout.GridPane;
import org.objectweb.asm.tree.FieldNode;
import matcher.NameType;
import matcher.Util;
import matcher.Util.AFElementType;
import matcher.gui.Gui;
import matcher.gui.GuiConstants;
import matcher.gui.IGuiComponent;
import matcher.gui.ISelectionProvider;
import matcher.type.FieldInstance;
package matcher.gui.tab;




public class FieldInfoTab extends Tab implements IGuiComponent {
	public FieldInfoTab(Gui gui, ISelectionProvider selectionProvider, boolean unmatchedTmp) {
		super("field info");

		this.gui = gui;
		this.selectionProvider = selectionProvider;
		this.unmatchedTmp = unmatchedTmp;

		for (int i = 0; i < NameType.AUX_COUNT; i++) {
			auxNameLabels[i] = new Label();
		}

		init();
	}

	private void init() {
		GridPane grid = new GridPane();
		grid.setPadding(new Insets(GuiConstants.padding));
		grid.setHgap(GuiConstants.padding);
		grid.setVgap(GuiConstants.padding);
		int row = 0;

		row = addRow("Owner", ownerLabel, grid, row);
		row = addRow("Name", nameLabel, grid, row);
		row = addRow("Tmp name", tmpNameLabel, grid, row);
		row = addRow("Mapped name", mappedNameLabel, grid, row);

		for (int i = 0; i < NameType.AUX_COUNT; i++) {
			row = addRow("AUX name "+(i+1), auxNameLabels[i], grid, row);
		}

		row = addRow("UID", uidLabel, grid, row);
		row = addRow("Name obf.", nameObfLabel, grid, row);
		row = addRow("Inputs", inputLabel, grid, row);
		row = addRow("Type", typeLabel, grid, row);
		row = addRow("Access", accessLabel, grid, row);
		row = addRow("Signature", sigLabel, grid, row);
		row = addRow("Parents", parentLabel, grid, row);
		row = addRow("Children", childLabel, grid, row);
		row = addRow("Read refs", readRefLabel, grid, row);
		row = addRow("Write refs", writeRefLabel, grid, row);
		row = addRow("Comment", mapCommentLabel, grid, row);

		ScrollPane scroll = new ScrollPane(grid);
		setContent(scroll);
	}

	private static int addRow(String name, Node content, GridPane grid, int row) {
		Label label = new Label(name+":");
		label.setMinWidth(Label.USE_PREF_SIZE);
		grid.add(label, 0, row);
		GridPane.setValignment(label, VPos.TOP);

		grid.add(content, 1, row);

		return row + 1;
	}

	@Override
	public void onMappingChange() {
		update(selectionProvider.getSelectedField());
	}

	@Override
	public void onFieldSelect(FieldInstance field) {
		update(field);
	}

	@Override
	public void onViewChange() {
		update(selectionProvider.getSelectedField());
	}

	private void update(FieldInstance field) {
		if (field == null) {
			ownerLabel.setText("-");
			nameLabel.setText("-");
			tmpNameLabel.setText("-");
			mappedNameLabel.setText("-");

			for (int i = 0; i < NameType.AUX_COUNT; i++) {
				auxNameLabels[i].setText("-");
			}

			uidLabel.setText("-");
			nameObfLabel.setText("-");
			inputLabel.setText("-");
			typeLabel.setText("-");
			accessLabel.setText("-");
			sigLabel.setText("-");
			parentLabel.setText("-");
			childLabel.setText("-");
			readRefLabel.setText("-");
			writeRefLabel.setText("-");
			mapCommentLabel.setText("-");
		} else {
			NameType nameType = gui.getNameType().withUnmatchedTmp(unmatchedTmp);

			ownerLabel.setText(getName(field.getCls(), nameType));
			nameLabel.setText(field.getName());
			tmpNameLabel.setText(field.hasLocalTmpName() ? field.getName(NameType.LOCTMP_PLAIN) : "-");
			mappedNameLabel.setText(field.hasMappedName() ? field.getName(NameType.MAPPED) : "-");

			for (int i = 0; i < NameType.AUX_COUNT; i++) {
				auxNameLabels[i].setText(field.hasAuxName(i) ? field.getName(NameType.getAux(i)) : "-");
			}

			uidLabel.setText(field.getUid() >= 0 ? Integer.toString(field.getUid()) : "-");
			nameObfLabel.setText(Boolean.toString(field.isNameObfuscated()));
			inputLabel.setText(ClassInfoTab.getInputPaths(field.getCls(), node -> node.fields.stream().anyMatch(m -> m.name.equals(field.getName()) && m.desc.equals(field.getDesc()))));
			typeLabel.setText(getName(field.getType(), nameType));
			accessLabel.setText(Util.formatAccessFlags(field.getAccess(), AFElementType.Method));

			FieldNode asmNode = field.getAsmNode();
			sigLabel.setText(asmNode == null || asmNode.signature == null ? "-" : asmNode.signature);

			parentLabel.setText(!field.getParents().isEmpty() ? formatClass(field.getParents(), nameType) : "-");
			childLabel.setText(!field.isFinal() ?  formatClass(field.getChildren(), nameType) : "-");

			readRefLabel.setText(format(field.getReadRefs(), nameType));