package jadx.gui.settings.ui;

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Font;
import java.awt.Toolkit;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.StringSelection;
import java.awt.event.ItemEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.function.Consumer;

import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.InputMap;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSpinner;
import javax.swing.JSplitPane;
import javax.swing.KeyStroke;
import javax.swing.ScrollPaneConstants;
import javax.swing.SpinnerNumberModel;
import javax.swing.SwingUtilities;
import javax.swing.WindowConstants;

import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import jadx.api.CommentsLevel;
import jadx.api.DecompilationMode;
import jadx.api.JadxArgs;
import jadx.api.JadxArgs.UseKotlinMethodsForVarNames;
import jadx.api.JadxDecompiler;
import jadx.api.args.GeneratedRenamesMappingFileMode;
import jadx.api.args.IntegerFormat;
import jadx.api.args.ResourceNameSource;
import jadx.api.args.UseSourceNameAsClassNameAlias;
import jadx.api.plugins.events.JadxEvents;
import jadx.api.plugins.events.types.ReloadSettingsWindow;
import jadx.api.plugins.gui.ISettingsGroup;
import jadx.core.utils.StringUtils;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.gui.settings.JadxSettings;
import jadx.gui.settings.JadxSettingsData;
import jadx.gui.settings.JadxUpdateChannel;
import jadx.gui.settings.LineNumbersMode;
import jadx.gui.settings.XposedCodegenLanguage;
import jadx.gui.settings.data.SaveOptionEnum;
import jadx.gui.settings.font.FontAdapter;
import jadx.gui.settings.font.FontSettings;
import jadx.gui.settings.ui.cache.CacheSettingsGroup;
import jadx.gui.settings.ui.font.JadxFontDialog;
import jadx.gui.settings.ui.plugins.PluginSettings;
import jadx.gui.settings.ui.shortcut.ShortcutsSettingsGroup;
import jadx.gui.ui.MainWindow;
import jadx.gui.ui.codearea.theme.EditorThemeManager;
import jadx.gui.ui.codearea.theme.ThemeIdAndName;
import jadx.gui.ui.tab.dnd.TabDndGhostType;
import jadx.gui.utils.FontUtils;
import jadx.gui.utils.LafManager;
import jadx.gui.utils.LangLocale;
import jadx.gui.utils.NLS;
import jadx.gui.utils.UiUtils;
import jadx.gui.utils.ui.ActionHandler;

public class JadxSettingsWindow extends JDialog {
	private static final long serialVersionUID = -1804570470377354148L;

	private static final Logger LOG = LoggerFactory.getLogger(JadxSettingsWindow.class);

	private final transient MainWindow mainWindow;
	private final transient JadxSettings settings;
	private final transient String startSettings;
	private final transient String startSettingsHash;
	private final transient LangLocale prevLang;
	private final transient Consumer<ReloadSettingsWindow> reloadListener;

	private transient boolean needReload = false;
	private transient SettingsTree tree;
	private List<ISettingsGroup> groups;
	private JPanel wrapGroupPanel;

	public JadxSettingsWindow(MainWindow mainWindow, JadxSettings settings) {
		this.mainWindow = mainWindow;
		this.settings = settings;
		this.startSettings = settings.getSettingsJsonString();
		this.startSettingsHash = calcSettingsHash();
		this.prevLang = settings.getLangLocale();

		initUI();

		setTitle(NLS.str("preferences.title"));
		setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
		setModalityType(ModalityType.APPLICATION_MODAL);
		pack();
		UiUtils.setWindowIcons(this);
		setLocationRelativeTo(null);
		if (!mainWindow.getSettings().loadWindowPos(this)) {
			setSize(700, 800);
		}
		reloadListener = ev -> UiUtils.uiRun(this::reloadUI);
		mainWindow.events().global().addListener(JadxEvents.RELOAD_SETTINGS_WINDOW, reloadListener);
	}

	private void reloadUI() {
		int[] selection = tree.getSelectionRows();
		closeGroups(false);
		getContentPane().removeAll();
		initUI();
		// wait for other events to process
		UiUtils.uiRun(() -> {
			tree.setSelectionRows(selection);
			SwingUtilities.updateComponentTreeUI(this);
		});
	}

	private void initUI() {
		wrapGroupPanel = new JPanel(new BorderLayout(10, 10));

		groups = new ArrayList<>();
		groups.add(makeDecompilationGroup());
		groups.add(makeDeobfuscationGroup());
		groups.add(makeRenameGroup());
		groups.add(new CacheSettingsGroup(this));
		groups.add(makeAppearanceGroup());
		groups.add(new ShortcutsSettingsGroup(this, settings));
		groups.add(makeProjectGroup());
		groups.add(new PluginSettings(mainWindow, settings).build());
		groups.add(makeOtherGroup());

		tree = new SettingsTree(this);
		tree.init(groups);
		tree.setFocusable(true);
		JScrollPane leftPane = new JScrollPane(tree);
		leftPane.setBorder(BorderFactory.createEmptyBorder(10, 10, 3, 3));

		JScrollPane rightPane = new JScrollPane(wrapGroupPanel);
		rightPane.getVerticalScrollBar().setUnitIncrement(16);
		rightPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED);
		rightPane.setBorder(BorderFactory.createEmptyBorder(10, 3, 3, 10));

		JSplitPane splitPane = new JSplitPane();
		splitPane.setResizeWeight(0.2);
		splitPane.setLeftComponent(leftPane);
		splitPane.setRightComponent(rightPane);

		Container contentPane = getContentPane();
		contentPane.add(splitPane, BorderLayout.CENTER);
		contentPane.add(buildButtonsPane(), BorderLayout.PAGE_END);

		KeyStroke strokeEsc = KeyStroke.getKeyStroke("ESCAPE");
		InputMap inputMap = getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
		inputMap.put(strokeEsc, "ESCAPE");
		getRootPane().getActionMap().put("ESCAPE", new ActionHandler(this::cancel));
	}

	private JPanel buildButtonsPane() {
		JButton saveBtn = new JButton(NLS.str("preferences.save"));
		saveBtn.addActionListener(event -> save());

		JButton cancelButton = new JButton(NLS.str("preferences.cancel"));
		cancelButton.addActionListener(event -> cancel());

		JButton resetBtn = new JButton(NLS.str("preferences.reset"));
		resetBtn.addActionListener(event -> reset());

		JButton copyBtn = new JButton(NLS.str("preferences.copy"));
		copyBtn.addActionListener(event -> copySettings());

		JPanel buttonPane = new JPanel();
		buttonPane.setLayout(new BoxLayout(buttonPane, BoxLayout.LINE_AXIS));
		buttonPane.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
		buttonPane.add(resetBtn);
		buttonPane.add(Box.createRigidArea(new Dimension(10, 0)));
		buttonPane.add(copyBtn);
		buttonPane.add(Box.createHorizontalGlue());
		buttonPane.add(saveBtn);
		buttonPane.add(Box.createRigidArea(new Dimension(10, 0)));
		buttonPane.add(cancelButton);

		getRootPane().setDefaultButton(saveBtn);
		return buttonPane;
	}

	/**
	 * Activate the settings page by location.
	 *
	 * @param location - can be title of a settings group or settings group class implementation (end
	 *                 with .class)
	 */
	public void activatePage(String location) {
		if (location.endsWith(".class")) {
			String clsName = StringUtils.removeSuffix(location, ".class");
			for (ISettingsGroup group : groups) {
				String groupCls = group.getClass().getSimpleName();
				if (groupCls.equals(clsName)) {
					selectGroup(group);
					return;
				}
			}
			throw new JadxRuntimeException("No setting group class: " + location);
		} else {
			for (ISettingsGroup group : groups) {
				if (group.getTitle().equals(location)) {
					selectGroup(group);
					return;
				}
			}
			throw new JadxRuntimeException("No setting group with title: " + location);
		}
	}

	public void selectGroup(ISettingsGroup group) {
		tree.selectGroup(group);
	}

	public void activateGroup(@Nullable ISettingsGroup group) {
		wrapGroupPanel.removeAll();
		if (group != null) {
			wrapGroupPanel.add(group.buildComponent());
		}
		wrapGroupPanel.updateUI();
	}

	private static void enableComponents(Container container, boolean enable) {
		for (Component component : container.getComponents()) {
			if (component instanceof Container) {
				enableComponents((Container) component, enable);
			}
			component.setEnabled(enable);
		}
	}

	private SettingsGroup makeDeobfuscationGroup() {
		JCheckBox deobfOn = new JCheckBox();
		deobfOn.setSelected(settings.isDeobfuscationOn());
		deobfOn.addItemListener(e -> {
			settings.setDeobfuscationOn(e.getStateChange() == ItemEvent.SELECTED);
			needReload();
		});

		SpinnerNumberModel minLenModel = new SpinnerNumberModel(settings.getDeobfuscationMinLength(), 0, Integer.MAX_VALUE, 1);
		JSpinner minLenSpinner = new JSpinner(minLenModel);
		minLenSpinner.addChangeListener(e -> {
			settings.setDeobfuscationMinLength((Integer) minLenSpinner.getValue());
			needReload();
		});

		SpinnerNumberModel maxLenModel = new SpinnerNumberModel(settings.getDeobfuscationMaxLength(), 0, Integer.MAX_VALUE, 1);
		JSpinner maxLenSpinner = new JSpinner(maxLenModel);
		maxLenSpinner.addChangeListener(e -> {
			settings.setDeobfuscationMaxLength((Integer) maxLenSpinner.getValue());
			needReload();
		});

		JComboBox<ResourceNameSource> resNamesSource = new JComboBox<>(ResourceNameSource.values());
		resNamesSource.setSelectedItem(settings.getResourceNameSource());
		resNamesSource.addActionListener(e -> {
			settings.setResourceNameSource((ResourceNameSource) resNamesSource.getSelectedItem());
			needReload();
		});

		JCheckBox useHeaders = new JCheckBox();
		useHeaders.setSelected(settings.isUseHeadersForDetectResourceExtensions());
		useHeaders.addItemListener(e -> {
			settings.setUseHeadersForDetectResourceExtensions(e.getStateChange() == ItemEvent.SELECTED);
			needReload();
		});

		JComboBox<GeneratedRenamesMappingFileMode> generatedRenamesMappingFileModeCB =
				new JComboBox<>(GeneratedRenamesMappingFileMode.values());
		generatedRenamesMappingFileModeCB.setSelectedItem(settings.getGeneratedRenamesMappingFileMode());
		generatedRenamesMappingFileModeCB.addActionListener(e -> {
			GeneratedRenamesMappingFileMode newValue =
					(GeneratedRenamesMappingFileMode) generatedRenamesMappingFileModeCB.getSelectedItem();
			if (newValue != settings.getGeneratedRenamesMappingFileMode()) {
				settings.setGeneratedRenamesMappingFileMode(newValue);
				needReload();
			}
		});

		JButton editWhitelistedEntities = new JButton(NLS.str("preferences.excludedPackages.button"));
		editWhitelistedEntities.addActionListener(event -> {
			String prevWhitelistedEntities = settings.getDeobfuscationWhitelistStr();
			String result = JOptionPane.showInputDialog(this,
					NLS.str("preferences.deobfuscation_whitelist.editDialog"),
					prevWhitelistedEntities);
			if (result != null) {
				settings.setDeobfuscationWhitelistStr(result);
				if (!prevWhitelistedEntities.equals(result)) {
					needReload();
				}
			}
		});

		SettingsGroup deobfGroup = new SettingsGroup(NLS.str("preferences.deobfuscation"));
		deobfGroup.addRow(NLS.str("preferences.deobfuscation_on"), deobfOn);
		deobfGroup.addRow(NLS.str("preferences.deobfuscation_min_len"), minLenSpinner);
		deobfGroup.addRow(NLS.str("preferences.deobfuscation_max_len"), maxLenSpinner);
		deobfGroup.addRow(NLS.str("preferences.deobfuscation_res_name_source"), resNamesSource);
		deobfGroup.addRow(NLS.str("preferences.deobfuscation_res_use_headers"), useHeaders);
		deobfGroup.addRow(NLS.str("preferences.generated_renames_mapping_file_mode"), generatedRenamesMappingFileModeCB);
		deobfGroup.addRow(NLS.str("preferences.deobfuscation_whitelist"),
				NLS.str("preferences.deobfuscation_whitelist.tooltip"), editWhitelistedEntities);

		deobfGroup.end();

		Collection<JComponent> connectedComponents = Arrays.asList(minLenSpinner, maxLenSpinner);
		deobfOn.addItemListener(e -> enableComponentList(connectedComponents, e.getStateChange() == ItemEvent.SELECTED));
		enableComponentList(connectedComponents, settings.isDeobfuscationOn());
		return deobfGroup;
	}

	private SettingsGroup makeRenameGroup() {
		JCheckBox renameCaseSensitive = new JCheckBox();
		renameCaseSensitive.setSelected(settings.isRenameCaseSensitive());
		renameCaseSensitive.addItemListener(e -> {
			settings.updateRenameFlag(JadxArgs.RenameEnum.CASE, e.getStateChange() == ItemEvent.SELECTED);
			needReload();
		});

		JCheckBox renameValid = new JCheckBox();
		renameValid.setSelected(settings.isRenameValid());
		renameValid.addItemListener(e -> {
			settings.updateRenameFlag(JadxArgs.RenameEnum.VALID, e.getStateChange() == ItemEvent.SELECTED);
			needReload();
		});

		JCheckBox renamePrintable = new JCheckBox();
		renamePrintable.setSelected(settings.isRenamePrintable());
		renamePrintable.addItemListener(e -> {
			settings.updateRenameFlag(JadxArgs.RenameEnum.PRINTABLE, e.getStateChange() == ItemEvent.SELECTED);
			needReload();
		});

		JComboBox<UseSourceNameAsClassNameAlias> useSourceNameAsClassNameAlias = new JComboBox<>(UseSourceNameAsClassNameAlias.values());
		useSourceNameAsClassNameAlias.setSelectedItem(settings.getUseSourceNameAsClassNameAlias());
		useSourceNameAsClassNameAlias.addActionListener(e -> {
			settings.setUseSourceNameAsClassNameAlias((UseSourceNameAsClassNameAlias) useSourceNameAsClassNameAlias.getSelectedItem());
			needReload();
		});

		JSpinner repeatLimit = new JSpinner(new SpinnerNumberModel(settings.getSourceNameRepeatLimit(), 1, Integer.MAX_VALUE, 1));
		repeatLimit.addChangeListener(e -> {
			settings.setSourceNameRepeatLimit((Integer) repeatLimit.getValue());
			needReload();
		});

		SettingsGroup group = new SettingsGroup(NLS.str("preferences.rename"));
		group.addRow(NLS.str("preferences.rename_case"), renameCaseSensitive);
		group.addRow(NLS.str("preferences.rename_valid"), renameValid);
		group.addRow(NLS.str("preferences.rename_printable"), renamePrintable);
		group.addRow(NLS.str("preferences.rename_use_source_name_as_class_name_alias"), useSourceNameAsClassNameAlias);
		group.addRow(NLS.str("preferences.rename_source_name_repeat_limit"), repeatLimit);
		return group;
	}

	private void enableComponentList(Collection<JComponent> connectedComponents, boolean enabled) {
		connectedComponents.forEach(comp -> comp.setEnabled(enabled));
	}

	private SettingsGroup makeProjectGroup() {
		JComboBox<SaveOptionEnum> dropdown = new JComboBox<>(SaveOptionEnum.values());
		dropdown.setSelectedItem(settings.getSaveOption());
		dropdown.addActionListener(e -> {
			settings.setSaveOption((SaveOptionEnum) dropdown.getSelectedItem());
			needReload();
		});

		SettingsGroup group = new SettingsGroup(NLS.str("preferences.project"));
		group.addRow(NLS.str("preferences.saveOption"), dropdown);

		return group;
	}

	private SettingsGroup makeAppearanceGroup() {
		JComboBox<LangLocale> languageCbx = new JComboBox<>(NLS.getLangLocales());
		for (LangLocale locale : NLS.getLangLocales()) {
			if (locale.equals(settings.getLangLocale())) {
				languageCbx.setSelectedItem(locale);
				break;
			}
		}
		languageCbx.addActionListener(e -> settings.setLangLocale((LangLocale) languageCbx.getSelectedItem()));

		EditorThemeManager editorThemeManager = mainWindow.getEditorThemeManager();
		JComboBox<ThemeIdAndName> themesCbx = new JComboBox<>(editorThemeManager.getThemeIdNameArray());
		themesCbx.setSelectedItem(editorThemeManager.getCurrentThemeIdName());
		themesCbx.addActionListener(evt -> {
			ThemeIdAndName selected = (ThemeIdAndName) themesCbx.getSelectedItem();
			if (selected != null) {
				settings.setEditorTheme(selected.getId());
				mainWindow.loadSettings();
			}
		});

		JComboBox<String> lafCbx = new JComboBox<>(LafManager.getThemes());
		lafCbx.setSelectedItem(settings.getLafTheme());
		lafCbx.addActionListener(e -> {
			settings.setLafTheme((String) lafCbx.getSelectedItem());
			mainWindow.loadSettings();
		});

		JSpinner uiZoomSpinner = new JSpinner(new SpinnerNumberModel(settings.getUiZoom(), 0.1, 10.0, 0.25));
		uiZoomSpinner.addChangeListener(e -> {
			float zoomValue = ((Double) uiZoomSpinner.getValue()).floatValue();
			settings.setUiZoom(zoomValue);
			mainWindow.loadSettings();
		});

		JCheckBox applyUiZoomToFontsChB = new JCheckBox();
		applyUiZoomToFontsChB.setSelected(settings.isApplyUiZoomToFonts());
		applyUiZoomToFontsChB.addItemListener(e -> {
			settings.setApplyUiZoomToFonts(e.getStateChange() == ItemEvent.SELECTED);
			mainWindow.loadSettings();
		});

		SettingsGroup group = new SettingsGroup(NLS.str("preferences.appearance"));
		group.addRow(NLS.str("preferences.language"), languageCbx);
		group.addRow(NLS.str("preferences.ui_zoom"), uiZoomSpinner);
		group.addRow(NLS.str("preferences.apply_ui_zoom_to_fonts"), applyUiZoomToFontsChB);

		FontSettings fontSettings = settings.getFontSettings();
		addFontEditor(group, NLS.str("preferences.ui_font"), fontSettings.getUiFontAdapter(), false);
		addFontEditor(group, NLS.str("preferences.code_font"), fontSettings.getCodeFontAdapter(), false);
		addFontEditor(group, NLS.str("preferences.smali_font"), fontSettings.getSmaliFontAdapter(), true);

		group.addRow(NLS.str("preferences.laf_theme"), lafCbx);
		group.addRow(NLS.str("preferences.theme"), themesCbx);

		JComboBox<TabDndGhostType> tabDndGhostTypeCbx = new JComboBox<>(TabDndGhostType.values());
		tabDndGhostTypeCbx.setSelectedItem(settings.getTabDndGhostType());
		tabDndGhostTypeCbx.addActionListener(e -> {
			settings.setTabDndGhostType((TabDndGhostType) tabDndGhostTypeCbx.getSelectedItem());
			mainWindow.loadSettings();
		});
		group.addRow(NLS.str("preferences.tab_dnd_appearance"), tabDndGhostTypeCbx);

		return group;
	}

	private void addFontEditor(SettingsGroup group, String title, FontAdapter fontAdapter, boolean monospace) {
		JLabel fontLabel = new JLabel(getFontLabelStr(fontAdapter.getFont()));
		JButton fontBtn = new JButton(NLS.str("preferences.select_font"));
		fontBtn.addMouseListener(new MouseAdapter() {
			@Override
			public void mouseClicked(MouseEvent e) {
				Font font = new JadxFontDialog(JadxSettingsWindow.this, settings, title)
						.select(fontAdapter.getFont(), monospace);
				if (font != null) {
					fontLabel.setText(getFontLabelStr(font));
					fontAdapter.setFont(font);
					mainWindow.loadSettings();
				}
			}
		});
		JPanel fontPanel = new JPanel();
		fontPanel.setLayout(new FlowLayout(FlowLayout.LEFT));
		fontPanel.add(fontLabel);
		fontPanel.add(fontBtn);
		group.addRow(title, fontPanel);
	}

	private static String getFontLabelStr(Font font) {
		return font.getFamily()
				+ ' ' + FontUtils.convertFontStyleToString(font.getStyle())
				+ ' ' + font.getSize();
	}

	private SettingsGroup makeDecompilationGroup() {
		JCheckBox useDx = new JCheckBox();
		useDx.setSelected(settings.isUseDx());
		useDx.addItemListener(e -> {
			settings.setUseDx(e.getStateChange() == ItemEvent.SELECTED);
			needReload();
		});

		JComboBox<DecompilationMode> decompilationModeComboBox = new JComboBox<>(DecompilationMode.values());
		decompilationModeComboBox.setSelectedItem(settings.getDecompilationMode());
		decompilationModeComboBox.addActionListener(e -> {
			settings.setDecompilationMode((DecompilationMode) decompilationModeComboBox.getSelectedItem());
			needReload();
		});

		JCheckBox showInconsistentCode = new JCheckBox();
		showInconsistentCode.setSelected(settings.isShowInconsistentCode());
		showInconsistentCode.addItemListener(e -> {
			settings.setShowInconsistentCode(e.getStateChange() == ItemEvent.SELECTED);
			needReload();
		});

		JCheckBox resourceDecode = new JCheckBox();
		resourceDecode.setSelected(settings.isSkipResources());
		resourceDecode.addItemListener(e -> {
			settings.setSkipResources(e.getStateChange() == ItemEvent.SELECTED);
			needReload();
		});

		// fix for #1331
		int threadsCountValue = settings.getThreadsCount();
		int threadsCountMax = Math.max(2, Math.max(threadsCountValue, Runtime.getRuntime().availableProcessors() * 2));
		SpinnerNumberModel spinnerModel = new SpinnerNumberModel(threadsCountValue, 1, threadsCountMax, 1);
		JSpinner threadsCount = new JSpinner(spinnerModel);
		threadsCount.addChangeListener(e -> {
			settings.setThreadsCount((Integer) threadsCount.getValue());
			needReload();
		});

		JButton editExcludedPackages = new JButton(NLS.str("preferences.excludedPackages.button"));
		editExcludedPackages.addActionListener(event -> {

			String oldExcludedPackages = settings.getExcludedPackages();
			String result = JOptionPane.showInputDialog(this, NLS.str("preferences.excludedPackages.editDialog"),
					settings.getExcludedPackages());
			if (result != null) {
				settings.setExcludedPackages(result);
				if (!oldExcludedPackages.equals(result)) {
					needReload();
				}
			}
		});

		JCheckBox autoStartJobs = new JCheckBox();
		autoStartJobs.setSelected(settings.isAutoStartJobs());
		autoStartJobs.addItemListener(e -> settings.setAutoStartJobs(e.getStateChange() == ItemEvent.SELECTED));

		JCheckBox escapeUnicode = new JCheckBox();
		escapeUnicode.setSelected(settings.isEscapeUnicode());
		escapeUnicode.addItemListener(e -> {
			settings.setEscapeUnicode(e.getStateChange() == ItemEvent.SELECTED);
			needReload();
		});

		JCheckBox replaceConsts = new JCheckBox();
		replaceConsts.setSelected(settings.isReplaceConsts());
		replaceConsts.addItemListener(e -> {
			settings.setReplaceConsts(e.getStateChange() == ItemEvent.SELECTED);
			needReload();
		});

		JCheckBox respectBytecodeAccessModifiers = new JCheckBox();
		respectBytecodeAccessModifiers.setSelected(settings.isRespectBytecodeAccessModifiers());
		respectBytecodeAccessModifiers.addItemListener(e -> {
			settings.setRespectBytecodeAccessModifiers(e.getStateChange() == ItemEvent.SELECTED);
			needReload();
		});

		JCheckBox useImports = new JCheckBox();
		useImports.setSelected(settings.isUseImports());
		useImports.addItemListener(e -> {
			settings.setUseImports(e.getStateChange() == ItemEvent.SELECTED);
			needReload();
		});

		JCheckBox useDebugInfo = new JCheckBox();
		useDebugInfo.setSelected(settings.isDebugInfo());
		useDebugInfo.addItemListener(e -> {
			settings.setDebugInfo(e.getStateChange() == ItemEvent.SELECTED);
			needReload();
		});

		JCheckBox inlineAnonymous = new JCheckBox();
		inlineAnonymous.setSelected(settings.isInlineAnonymousClasses());
		inlineAnonymous.addItemListener(e -> {
			settings.setInlineAnonymousClasses(e.getStateChange() == ItemEvent.SELECTED);
			needReload();
		});

		JCheckBox inlineMethods = new JCheckBox();
		inlineMethods.setSelected(settings.isInlineMethods());
		inlineMethods.addItemListener(e -> {
			settings.setInlineMethods(e.getStateChange() == ItemEvent.SELECTED);
			needReload();
		});

		JCheckBox inlineKotlinLambdas = new JCheckBox();
		inlineKotlinLambdas.setSelected(settings.isAllowInlineKotlinLambda());
		inlineKotlinLambdas.addItemListener(e -> {
			settings.setAllowInlineKotlinLambda(e.getStateChange() == ItemEvent.SELECTED);
			needReload();
		});

		JCheckBox moveInnerClasses = new JCheckBox();
		moveInnerClasses.setSelected(settings.isMoveInnerClasses());
		moveInnerClasses.addItemListener(e -> {
			settings.setMoveInnerClasses(e.getStateChange() == ItemEvent.SELECTED);
			needReload();
		});

		JCheckBox extractFinally = new JCheckBox();
		extractFinally.setSelected(settings.isExtractFinally());
		extractFinally.addItemListener(e -> {
			settings.setExtractFinally(e.getStateChange() == ItemEvent.SELECTED);
			needReload();
		});

		JCheckBox restoreSwitchOverString = new JCheckBox();
		restoreSwitchOverString.setSelected(settings.isRestoreSwitchOverString());
		restoreSwitchOverString.addItemListener(e -> {
			settings.setRestoreSwitchOverString(e.getStateChange() == ItemEvent.SELECTED);
			needReload();
		});

		JCheckBox fsCaseSensitive = new JCheckBox();
		fsCaseSensitive.setSelected(settings.isFsCaseSensitive());
		fsCaseSensitive.addItemListener(e -> {
			settings.setFsCaseSensitive(e.getStateChange() == ItemEvent.SELECTED);
			needReload();
		});

		JComboBox<UseKotlinMethodsForVarNames> kotlinRenameVars = new JComboBox<>(UseKotlinMethodsForVarNames.values());
		kotlinRenameVars.setSelectedItem(settings.getUseKotlinMethodsForVarNames());
		kotlinRenameVars.addActionListener(e -> {
			settings.setUseKotlinMethodsForVarNames((UseKotlinMethodsForVarNames) kotlinRenameVars.getSelectedItem());
			needReload();
		});

		JComboBox<CommentsLevel> commentsLevel = new JComboBox<>(CommentsLevel.values());
		commentsLevel.setSelectedItem(settings.getCommentsLevel());
		commentsLevel.addActionListener(e -> {
			settings.setCommentsLevel((CommentsLevel) commentsLevel.getSelectedItem());
			needReload();
		});

		JComboBox<IntegerFormat> integerFormat = new JComboBox<>(IntegerFormat.values());
		integerFormat.setSelectedItem(settings.getIntegerFormat());
		integerFormat.addActionListener(e -> {
			settings.setIntegerFormat((IntegerFormat) integerFormat.getSelectedItem());
			needReload();
		});

		JSpinner typeUpdatesLimitCount = new JSpinner(
				new SpinnerNumberModel(settings.getTypeUpdatesLimitCount(), 1, Short.MAX_VALUE, 1));
		typeUpdatesLimitCount.addChangeListener(e -> {
			int newValue = (Integer) typeUpdatesLimitCount.getValue();
			if (newValue < 1) {
				UiUtils.uiRun(() -> typeUpdatesLimitCount.setValue(1));
			} else {
				settings.setTypeUpdatesLimitCount(newValue);
				needReload();
			}
		});

		SettingsGroup other = new SettingsGroup(NLS.str("preferences.decompile"));
		other.addRow(NLS.str("preferences.threads"), threadsCount);
		other.addRow(NLS.str("preferences.excludedPackages"),
				NLS.str("preferences.excludedPackages.tooltip"), editExcludedPackages);
		other.addRow(NLS.str("preferences.start_jobs"), autoStartJobs);
		other.addRow(NLS.str("preferences.decompilationMode"), decompilationModeComboBox);
		other.addRow(NLS.str("preferences.showInconsistentCode"), showInconsistentCode);
		other.addRow(NLS.str("preferences.escapeUnicode"), escapeUnicode);
		other.addRow(NLS.str("preferences.replaceConsts"), replaceConsts);
		other.addRow(NLS.str("preferences.respectBytecodeAccessModifiers"), respectBytecodeAccessModifiers);
		other.addRow(NLS.str("preferences.useImports"), useImports);
		other.addRow(NLS.str("preferences.useDebugInfo"), useDebugInfo);
		other.addRow(NLS.str("preferences.inlineAnonymous"), inlineAnonymous);
		other.addRow(NLS.str("preferences.inlineMethods"), inlineMethods);
		other.addRow(NLS.str("preferences.inlineKotlinLambdas"), inlineKotlinLambdas);
		other.addRow(NLS.str("preferences.moveInnerClasses"), moveInnerClasses);
		other.addRow(NLS.str("preferences.extractFinally"), extractFinally);
		other.addRow(NLS.str("preferences.restoreSwitchOverString"), restoreSwitchOverString);
		other.addRow(NLS.str("preferences.fsCaseSensitive"), fsCaseSensitive);
		other.addRow(NLS.str("preferences.useDx"), useDx);
		other.addRow(NLS.str("preferences.skipResourcesDecode"), resourceDecode);
		other.addRow(NLS.str("preferences.useKotlinMethodsForVarNames"), kotlinRenameVars);
		other.addRow(NLS.str("preferences.commentsLevel"), commentsLevel);
		other.addRow(NLS.str("preferences.integerFormat"), integerFormat);
		other.addRow(NLS.str("preferences.typeUpdatesCountLimit"), typeUpdatesLimitCount);
		return other;
	}

	private SettingsGroup makeOtherGroup() {
		JComboBox<LineNumbersMode> lineNumbersMode = new JComboBox<>(LineNumbersMode.values());
		lineNumbersMode.setSelectedItem(settings.getLineNumbersMode());
		lineNumbersMode.addActionListener(e -> {
			settings.setLineNumbersMode((LineNumbersMode) lineNumbersMode.getSelectedItem());
			mainWindow.loadSettings();
		});

		JCheckBox jumpOnDoubleClick = new JCheckBox();
		jumpOnDoubleClick.setSelected(settings.isJumpOnDoubleClick());
		jumpOnDoubleClick.addItemListener(e -> settings.setJumpOnDoubleClick(e.getStateChange() == ItemEvent.SELECTED));

		JSpinner resultsPerPage = new JSpinner(
				new SpinnerNumberModel(settings.getSearchResultsPerPage(), 0, Integer.MAX_VALUE, 1));
		resultsPerPage.addChangeListener(ev -> settings.setSearchResultsPerPage((Integer) resultsPerPage.getValue()));

		JCheckBox useAltFileDialog = new JCheckBox();
		useAltFileDialog.setSelected(settings.isUseAlternativeFileDialog());
		useAltFileDialog.addItemListener(e -> settings.setUseAlternativeFileDialog(e.getStateChange() == ItemEvent.SELECTED));

		JCheckBox update = new JCheckBox();
		update.setSelected(settings.isCheckForUpdates());
		update.addItemListener(e -> settings.setCheckForUpdates(e.getStateChange() == ItemEvent.SELECTED));

		JCheckBox disableTooltipOnHover = new JCheckBox();
		disableTooltipOnHover.setSelected(settings.isDisableTooltipOnHover());
		disableTooltipOnHover.addItemListener(e -> settings.setDisableTooltipOnHover(e.getStateChange() == ItemEvent.SELECTED));

		JCheckBox cfg = new JCheckBox();
		cfg.setSelected(settings.isCfgOutput());
		cfg.addItemListener(e -> {
			settings.setCfgOutput(e.getStateChange() == ItemEvent.SELECTED);
			needReload();
		});

		JCheckBox rawCfg = new JCheckBox();
		rawCfg.setSelected(settings.isRawCfgOutput());
		rawCfg.addItemListener(e -> {
			settings.setRawCfgOutput(e.getStateChange() == ItemEvent.SELECTED);
			needReload();
		});

		JComboBox<XposedCodegenLanguage> xposedCodegenLanguage = new JComboBox<>(XposedCodegenLanguage.values());
		xposedCodegenLanguage.setSelectedItem(settings.getXposedCodegenLanguage());
		xposedCodegenLanguage.addActionListener(e -> {
			settings.setXposedCodegenLanguage((XposedCodegenLanguage) xposedCodegenLanguage.getSelectedItem());
			mainWindow.loadSettings();
		});

		JComboBox<JadxUpdateChannel> updateChannel = new JComboBox<>(JadxUpdateChannel.values());
		updateChannel.setSelectedItem(settings.getJadxUpdateChannel());
		updateChannel.addActionListener(e -> {
			settings.setJadxUpdateChannel((JadxUpdateChannel) updateChannel.getSelectedItem());
			mainWindow.loadSettings();
		});

		SettingsGroup group = new SettingsGroup(NLS.str("preferences.other"));
		group.addRow(NLS.str("preferences.lineNumbersMode"), lineNumbersMode);
		group.addRow(NLS.str("preferences.jumpOnDoubleClick"), jumpOnDoubleClick);
		group.addRow(NLS.str("preferences.disable_tooltip_on_hover"), disableTooltipOnHover);
		group.addRow(NLS.str("preferences.search_results_per_page"), resultsPerPage);
		group.addRow(NLS.str("preferences.useAlternativeFileDialog"), useAltFileDialog);
		group.addRow(NLS.str("preferences.cfg"), cfg);
		group.addRow(NLS.str("preferences.raw_cfg"), rawCfg);
		group.addRow(NLS.str("preferences.xposed_codegen_language"), xposedCodegenLanguage);
		group.addRow(NLS.str("preferences.check_for_updates"), update);
		group.addRow(NLS.str("preferences.update_channel"), updateChannel);
		return group;
	}

	private void closeGroups(boolean save) {
		for (ISettingsGroup group : groups) {
			group.close(save);
		}
	}

	private void save() {
		closeGroups(true);
		settings.sync();
		enableComponents(this, false);
		SwingUtilities.invokeLater(() -> {
			if (shouldReload()) {
				mainWindow.getShortcutsController().loadSettings();
				mainWindow.reopen();
			}
			if (!settings.getLangLocale().equals(prevLang)) {
				JOptionPane.showMessageDialog(
						this,
						NLS.str("msg.language_changed", settings.getLangLocale()),
						NLS.str("msg.language_changed_title", settings.getLangLocale()),
						JOptionPane.INFORMATION_MESSAGE);
			}
			dispose();
		});
	}

	private void cancel() {
		closeGroups(false);
		settings.loadSettingsFromJsonString(startSettings);
		mainWindow.loadSettings();
		dispose();
	}

	private void reset() {
		int res = JOptionPane.showConfirmDialog(
				JadxSettingsWindow.this,
				NLS.str("preferences.reset_message"),
				NLS.str("preferences.reset_title"),
				JOptionPane.YES_NO_OPTION);
		if (res == JOptionPane.YES_OPTION) {
			settings.loadSettingsData(new JadxSettingsData());
			mainWindow.loadSettings();
			needReload();
			getContentPane().removeAll();
			initUI();
			pack();
			repaint();
		}
	}

	private void copySettings() {
		String settingsText = settings.exportSettingsString();
		Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
		StringSelection selection = new StringSelection(settingsText);
		clipboard.setContents(selection, selection);
		JOptionPane.showMessageDialog(
				JadxSettingsWindow.this,
				NLS.str("preferences.copy_message"));
	}

	public void needReload() {
		needReload = true;
	}

	private boolean shouldReload() {
		return needReload || !startSettingsHash.equals(calcSettingsHash());
	}

	@SuppressWarnings("resource")
	private String calcSettingsHash() {
		JadxDecompiler decompiler = mainWindow.getWrapper().getCurrentDecompiler().orElse(null);
		return settings.toJadxArgs().makeCodeArgsHash(decompiler);
	}

	public MainWindow getMainWindow() {
		return mainWindow;
	}

	@Override
	public void dispose() {
		mainWindow.events().global().removeListener(JadxEvents.RELOAD_SETTINGS_WINDOW, reloadListener);
		settings.saveWindowPos(this);
		super.dispose();
	}
}
