<commit_before>package de.prob2.ui.config;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.Singleton;

import de.prob.Main;
import de.prob2.ui.MainController;
import de.prob2.ui.consoles.Console;
import de.prob2.ui.consoles.b.BConsole;
import de.prob2.ui.consoles.groovy.GroovyConsole;
import de.prob2.ui.internal.StopActions;
import de.prob2.ui.layout.FontSize;
import de.prob2.ui.menu.MainView;
import de.prob2.ui.menu.RecentProjects;
import de.prob2.ui.operations.OperationsView;
import de.prob2.ui.persistence.TablePersistenceHandler;
import de.prob2.ui.persistence.UIState;
import de.prob2.ui.plugin.ProBPluginManager;
import de.prob2.ui.preferences.GlobalPreferences;
import de.prob2.ui.preferences.PreferencesStage;
import de.prob2.ui.prob2fx.CurrentProject;
import de.prob2.ui.states.StatesView;
import de.prob2.ui.verifications.VerificationsView;

import javafx.geometry.BoundingBox;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Singleton
public final class Config {
	/**
	 * A subset of the full {@link ConfigData}, which is used to load parts of the config before the injector is set up. This is needed to apply the locale override for example.
	 */
	private static class BasicConfigData {
		Locale localeOverride;

		private BasicConfigData() {}
	}

	/*
	 * The full set of config settings, used when the injector is available.
	 */
	private static final class ConfigData extends BasicConfigData {
		int maxRecentProjects;
		int fontSize;
		List<String> recentProjects;
		Console.ConfigData groovyConsoleSettings;
		Console.ConfigData bConsoleSettings;
		String guiState;
		List<String> visibleStages;
		Map<String, BoundingBox> stageBoxes;
		String currentPreference;
		String currentMainTab;
		String currentVerificationTab;
		List<String> expandedTitledPanes;
		String defaultProjectLocation;
		double[] horizontalDividerPositions;
		double[] verticalDividerPositions;
		double[] statesViewColumnsWidth;
		String[] statesViewColumnsOrder;
		OperationsView.SortMode operationsSortMode;
		boolean operationsShowNotEnabled;
		Map<String, String> globalPreferences;
		private Path pluginDirectory;
		private Map<FileChooserManager.Kind, Path> fileChooserInitialDirectories;
		
		
		private ConfigData() {}
	}

	// This Gson instance is for loadBasicConfig only. Everywhere else, injection should be used to get a GSON object.
	private static final Gson BASIC_GSON = new GsonBuilder().create();
	private static final Path LOCATION = Paths.get(Main.getProBDirectory(), "prob2ui", "config.json");

	private static final Logger logger = LoggerFactory.getLogger(Config.class);

	private final Gson gson;
	private final RecentProjects recentProjects;
	private final GroovyConsole groovyConsole;
	private final BConsole bConsole;
	private final Injector injector;
	private final UIState uiState;
	private final CurrentProject currentProject;
	private final GlobalPreferences globalPreferences;
	private final RuntimeOptions runtimeOptions;
	private final ProBPluginManager proBPluginManager;
	private final FileChooserManager fileChooserManager;
	

	@Inject
	private Config(
		final Gson gson,
		final RecentProjects recentProjects,
		final UIState uiState,
		final GroovyConsole groovyConsole,
		final BConsole bConsole,
		final Injector injector,
		final CurrentProject currentProject,
		final GlobalPreferences globalPreferences,
		final RuntimeOptions runtimeOptions,
		final StopActions stopActions,
		final ProBPluginManager proBPluginManager,
		final FileChooserManager fileChooserManager
	) {
		this.gson = gson;
		this.recentProjects = recentProjects;
		this.uiState = uiState;
		this.groovyConsole = groovyConsole;
		this.bConsole = bConsole;
		this.injector = injector;
		this.currentProject = currentProject;
		this.globalPreferences = globalPreferences;
		this.runtimeOptions = runtimeOptions;
		this.proBPluginManager = proBPluginManager;
		this.fileChooserManager = fileChooserManager;

		try {
			Files.createDirectories(LOCATION.getParent());
		} catch (IOException e) {
			logger.warn("Failed to create the parent directory for the config file {}", LOCATION, e);
		}

		this.load();
		
		stopActions.add(this::save);
	}

	/**
	 * Load basic settings from the config file. This method is static so it can be called before all of {@link Config}'s dependencies are available.
	 *
	 * @return basic settings from the config file
	 */
	private static BasicConfigData loadBasicConfig() {
		try (final Reader reader = Files.newBufferedReader(LOCATION)) {
			final BasicConfigData data = BASIC_GSON.fromJson(reader, BasicConfigData.class);
			if (data == null) {
				// Config file is empty, use defaults instead.
				return new BasicConfigData();
			}
			return data;
		} catch (FileNotFoundException | NoSuchFileException exc) {
			logger.info("Config file not found while loading basic config, loading default settings", exc);
			return new BasicConfigData();
		} catch (IOException exc) {
			logger.warn("Failed to open config file while loading basic config", exc);
			return new BasicConfigData();
		}
	}

	/**
	 * Get the locale override from the config file. This method is static so it can be called before all of {@link Config}'s dependencies are available.
	 *
	 * @return the locale override
	 */
	public static Locale getLocaleOverride() {
		return loadBasicConfig().localeOverride;
	}

	private void replaceMissingWithDefaults(final ConfigData configData) {
		// If some keys are null (for example when loading a config from a
		// previous version that did not have those keys), replace them with
		// their values from the default config.
		if (configData.maxRecentProjects <= 0) {
			configData.maxRecentProjects = 10;
		}
		if (configData.recentProjects == null) {
			configData.recentProjects = new ArrayList<>();
		}
		if (configData.fontSize <= 0) {
			configData.fontSize = FontSize.DEFAULT_FONT_SIZE;
		}
		if (configData.guiState == null || configData.guiState.isEmpty()) {
			configData.guiState = "main.fxml";
		}
		if (configData.visibleStages == null) {
			configData.visibleStages = new ArrayList<>();
		}
		if (configData.stageBoxes == null) {
			configData.stageBoxes = new HashMap<>();
		}
		if (configData.currentPreference == null) {
			configData.currentPreference = "general";
		}
		if (configData.currentMainTab == null) {
			configData.currentMainTab = "states";
		}
		if (configData.currentVerificationTab == null) {
			configData.currentVerificationTab = "modelchecking";
		}
		if (configData.expandedTitledPanes == null) {
			configData.expandedTitledPanes = new ArrayList<>();
		}
		if (configData.defaultProjectLocation == null) {
			configData.defaultProjectLocation = System.getProperty("user.home");
		}

		MainController main = injector.getInstance(MainController.class);
		if (configData.horizontalDividerPositions == null) {
			configData.horizontalDividerPositions = main.getHorizontalDividerPositions();
		}
		if (configData.verticalDividerPositions == null) {
			configData.verticalDividerPositions = main.getVerticalDividerPositions();
		}

		if (configData.statesViewColumnsWidth == null) {
			configData.statesViewColumnsWidth = new double[]{0.3333333333333333, 0.3333333333333333, 0.3333333333333333};
		}

		if (configData.statesViewColumnsOrder == null) {
			configData.statesViewColumnsOrder = new String[]{"name", "value", "previousValue"};
		}

		if (configData.operationsSortMode == null) {
			configData.operationsSortMode = OperationsView.SortMode.MODEL_ORDER;
		}

		if (configData.globalPreferences == null) {
			configData.globalPreferences = new HashMap<>();
		}

		if (configData.fileChooserInitialDirectories == null) {
			configData.fileChooserInitialDirectories = new EnumMap<>(FileChooserManager.Kind.class);
		}
		// Gson represents unknown kinds (from older or newer configs) as null.
		// We simply remove them here, because there's nothing meaningful we can do with them.
		configData.fileChooserInitialDirectories.remove(null);
	}

	public void load() {
		ConfigData configData;
		if (this.runtimeOptions.isLoadConfig()) {
			try (final Reader reader = Files.newBufferedReader(LOCATION)) {
				configData = gson.fromJson(reader, ConfigData.class);
				if (configData == null) {
					// Config file is empty, use default config.
					configData = new ConfigData();
				}
			} catch (FileNotFoundException | NoSuchFileException exc) {
				logger.info("Config file not found, loading default settings");
				configData = new ConfigData();
			} catch (IOException exc) {
				logger.warn("Failed to open config file", exc);
				return;
			}
		} else {
			logger.info("Config loading disabled via runtime options, loading default config");
			configData = new ConfigData();
		}
		
		this.replaceMissingWithDefaults(configData);

		this.uiState.setLocaleOverride(configData.localeOverride);

		this.recentProjects.setMaximum(configData.maxRecentProjects);
		this.recentProjects.setAll(configData.recentProjects);

		this.currentProject.setDefaultLocation(Paths.get(configData.defaultProjectLocation));

		this.uiState.setGuiState(configData.guiState);
		this.uiState.getSavedVisibleStages().clear();
		this.uiState.getSavedVisibleStages().addAll(configData.visibleStages);

		this.uiState.getSavedStageBoxes().putAll(configData.stageBoxes);

		for (String pane : configData.expandedTitledPanes) {
			this.uiState.getExpandedTitledPanes().add(pane);
		}
		

		this.injector.getInstance(PreferencesStage.class).getTabPersistenceHandler().setCurrentTab(configData.currentPreference);
		this.injector.getInstance(MainView.class).getTabPersistenceHandler().setCurrentTab(configData.currentMainTab);
		this.injector.getInstance(VerificationsView.class).getTabPersistenceHandler().setCurrentTab(configData.currentVerificationTab);
		
		groovyConsole.applySettings(configData.groovyConsoleSettings);
		bConsole.applySettings(configData.bConsoleSettings);

		this.uiState.setStatesViewColumnsWidth(configData.statesViewColumnsWidth);
		this.uiState.setStatesViewColumnsOrder(configData.statesViewColumnsOrder);

		this.uiState.setHorizontalDividerPositions(configData.horizontalDividerPositions);<commit_msg>Replace a loop with addAll in Config<commit_after>package de.prob2.ui.config;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.Singleton;

import de.prob.Main;
import de.prob2.ui.MainController;
import de.prob2.ui.consoles.Console;
import de.prob2.ui.consoles.b.BConsole;
import de.prob2.ui.consoles.groovy.GroovyConsole;
import de.prob2.ui.internal.StopActions;
import de.prob2.ui.layout.FontSize;
import de.prob2.ui.menu.MainView;
import de.prob2.ui.menu.RecentProjects;
import de.prob2.ui.operations.OperationsView;
import de.prob2.ui.persistence.TablePersistenceHandler;
import de.prob2.ui.persistence.UIState;
import de.prob2.ui.plugin.ProBPluginManager;
import de.prob2.ui.preferences.GlobalPreferences;
import de.prob2.ui.preferences.PreferencesStage;
import de.prob2.ui.prob2fx.CurrentProject;
import de.prob2.ui.states.StatesView;
import de.prob2.ui.verifications.VerificationsView;

import javafx.geometry.BoundingBox;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Singleton
public final class Config {
	/**
	 * A subset of the full {@link ConfigData}, which is used to load parts of the config before the injector is set up. This is needed to apply the locale override for example.
	 */
	private static class BasicConfigData {
		Locale localeOverride;

		private BasicConfigData() {}
	}

	/*
	 * The full set of config settings, used when the injector is available.
	 */
	private static final class ConfigData extends BasicConfigData {
		int maxRecentProjects;
		int fontSize;
		List<String> recentProjects;
		Console.ConfigData groovyConsoleSettings;
		Console.ConfigData bConsoleSettings;
		String guiState;
		List<String> visibleStages;
		Map<String, BoundingBox> stageBoxes;
		String currentPreference;
		String currentMainTab;
		String currentVerificationTab;
		List<String> expandedTitledPanes;
		String defaultProjectLocation;
		double[] horizontalDividerPositions;
		double[] verticalDividerPositions;
		double[] statesViewColumnsWidth;
		String[] statesViewColumnsOrder;
		OperationsView.SortMode operationsSortMode;
		boolean operationsShowNotEnabled;
		Map<String, String> globalPreferences;
		private Path pluginDirectory;
		private Map<FileChooserManager.Kind, Path> fileChooserInitialDirectories;
		
		
		private ConfigData() {}
	}

	// This Gson instance is for loadBasicConfig only. Everywhere else, injection should be used to get a GSON object.
	private static final Gson BASIC_GSON = new GsonBuilder().create();
	private static final Path LOCATION = Paths.get(Main.getProBDirectory(), "prob2ui", "config.json");

	private static final Logger logger = LoggerFactory.getLogger(Config.class);

	private final Gson gson;
	private final RecentProjects recentProjects;
	private final GroovyConsole groovyConsole;
	private final BConsole bConsole;
	private final Injector injector;
	private final UIState uiState;
	private final CurrentProject currentProject;
	private final GlobalPreferences globalPreferences;
	private final RuntimeOptions runtimeOptions;
	private final ProBPluginManager proBPluginManager;
	private final FileChooserManager fileChooserManager;
	

	@Inject
	private Config(
		final Gson gson,
		final RecentProjects recentProjects,
		final UIState uiState,
		final GroovyConsole groovyConsole,
		final BConsole bConsole,
		final Injector injector,
		final CurrentProject currentProject,
		final GlobalPreferences globalPreferences,
		final RuntimeOptions runtimeOptions,
		final StopActions stopActions,
		final ProBPluginManager proBPluginManager,
		final FileChooserManager fileChooserManager
	) {
		this.gson = gson;
		this.recentProjects = recentProjects;
		this.uiState = uiState;
		this.groovyConsole = groovyConsole;
		this.bConsole = bConsole;
		this.injector = injector;
		this.currentProject = currentProject;
		this.globalPreferences = globalPreferences;
		this.runtimeOptions = runtimeOptions;
		this.proBPluginManager = proBPluginManager;
		this.fileChooserManager = fileChooserManager;

		try {
			Files.createDirectories(LOCATION.getParent());
		} catch (IOException e) {
			logger.warn("Failed to create the parent directory for the config file {}", LOCATION, e);
		}

		this.load();
		
		stopActions.add(this::save);
	}

	/**
	 * Load basic settings from the config file. This method is static so it can be called before all of {@link Config}'s dependencies are available.
	 *
	 * @return basic settings from the config file
	 */
	private static BasicConfigData loadBasicConfig() {
		try (final Reader reader = Files.newBufferedReader(LOCATION)) {
			final BasicConfigData data = BASIC_GSON.fromJson(reader, BasicConfigData.class);
			if (data == null) {
				// Config file is empty, use defaults instead.
				return new BasicConfigData();
			}
			return data;
		} catch (FileNotFoundException | NoSuchFileException exc) {
			logger.info("Config file not found while loading basic config, loading default settings", exc);
			return new BasicConfigData();
		} catch (IOException exc) {
			logger.warn("Failed to open config file while loading basic config", exc);
			return new BasicConfigData();
		}
	}

	/**
	 * Get the locale override from the config file. This method is static so it can be called before all of {@link Config}'s dependencies are available.
	 *
	 * @return the locale override
	 */
	public static Locale getLocaleOverride() {
		return loadBasicConfig().localeOverride;
	}

	private void replaceMissingWithDefaults(final ConfigData configData) {
		// If some keys are null (for example when loading a config from a
		// previous version that did not have those keys), replace them with
		// their values from the default config.
		if (configData.maxRecentProjects <= 0) {
			configData.maxRecentProjects = 10;
		}
		if (configData.recentProjects == null) {
			configData.recentProjects = new ArrayList<>();
		}
		if (configData.fontSize <= 0) {
			configData.fontSize = FontSize.DEFAULT_FONT_SIZE;
		}
		if (configData.guiState == null || configData.guiState.isEmpty()) {
			configData.guiState = "main.fxml";
		}
		if (configData.visibleStages == null) {
			configData.visibleStages = new ArrayList<>();
		}
		if (configData.stageBoxes == null) {
			configData.stageBoxes = new HashMap<>();
		}
		if (configData.currentPreference == null) {
			configData.currentPreference = "general";
		}
		if (configData.currentMainTab == null) {
			configData.currentMainTab = "states";
		}
		if (configData.currentVerificationTab == null) {
			configData.currentVerificationTab = "modelchecking";
		}
		if (configData.expandedTitledPanes == null) {
			configData.expandedTitledPanes = new ArrayList<>();
		}
		if (configData.defaultProjectLocation == null) {
			configData.defaultProjectLocation = System.getProperty("user.home");
		}

		MainController main = injector.getInstance(MainController.class);
		if (configData.horizontalDividerPositions == null) {
			configData.horizontalDividerPositions = main.getHorizontalDividerPositions();
		}
		if (configData.verticalDividerPositions == null) {
			configData.verticalDividerPositions = main.getVerticalDividerPositions();
		}

		if (configData.statesViewColumnsWidth == null) {
			configData.statesViewColumnsWidth = new double[]{0.3333333333333333, 0.3333333333333333, 0.3333333333333333};
		}

		if (configData.statesViewColumnsOrder == null) {
			configData.statesViewColumnsOrder = new String[]{"name", "value", "previousValue"};
		}

		if (configData.operationsSortMode == null) {
			configData.operationsSortMode = OperationsView.SortMode.MODEL_ORDER;
		}

		if (configData.globalPreferences == null) {
			configData.globalPreferences = new HashMap<>();
		}

		if (configData.fileChooserInitialDirectories == null) {
			configData.fileChooserInitialDirectories = new EnumMap<>(FileChooserManager.Kind.class);
		}
		// Gson represents unknown kinds (from older or newer configs) as null.
		// We simply remove them here, because there's nothing meaningful we can do with them.
		configData.fileChooserInitialDirectories.remove(null);
	}

	public void load() {
		ConfigData configData;
		if (this.runtimeOptions.isLoadConfig()) {
			try (final Reader reader = Files.newBufferedReader(LOCATION)) {
				configData = gson.fromJson(reader, ConfigData.class);
				if (configData == null) {
					// Config file is empty, use default config.
					configData = new ConfigData();
				}
			} catch (FileNotFoundException | NoSuchFileException exc) {
				logger.info("Config file not found, loading default settings");
				configData = new ConfigData();
			} catch (IOException exc) {
				logger.warn("Failed to open config file", exc);
				return;
			}
		} else {
			logger.info("Config loading disabled via runtime options, loading default config");
			configData = new ConfigData();
		}
		
		this.replaceMissingWithDefaults(configData);

		this.uiState.setLocaleOverride(configData.localeOverride);

		this.recentProjects.setMaximum(configData.maxRecentProjects);
		this.recentProjects.setAll(configData.recentProjects);

		this.currentProject.setDefaultLocation(Paths.get(configData.defaultProjectLocation));

		this.uiState.setGuiState(configData.guiState);
		this.uiState.getSavedVisibleStages().clear();
		this.uiState.getSavedVisibleStages().addAll(configData.visibleStages);

		this.uiState.getSavedStageBoxes().putAll(configData.stageBoxes);

		this.uiState.getExpandedTitledPanes().addAll(configData.expandedTitledPanes);

		this.injector.getInstance(PreferencesStage.class).getTabPersistenceHandler().setCurrentTab(configData.currentPreference);
		this.injector.getInstance(MainView.class).getTabPersistenceHandler().setCurrentTab(configData.currentMainTab);
		this.injector.getInstance(VerificationsView.class).getTabPersistenceHandler().setCurrentTab(configData.currentVerificationTab);
		
		groovyConsole.applySettings(configData.groovyConsoleSettings);
		bConsole.applySettings(configData.bConsoleSettings);

		this.uiState.setStatesViewColumnsWidth(configData.statesViewColumnsWidth);
		this.uiState.setStatesViewColumnsOrder(configData.statesViewColumnsOrder);

		this.uiState.setHorizontalDividerPositions(configData.horizontalDividerPositions);
{'fraction_non_alphanumeric': 0.09407599716780742, 'fraction_numerical': 0.007269294312013217, 'mean_word_length': 5.087931034482758, 'pattern_counts': {'":': 0, '<': 31, '<?xml version=': 0, '>': 27, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 2}, 'pii_count': 0, 'substrings_counts': 6, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2357682', 'n_tokens_mistral': 6802, 'n_tokens_neox': 6079, 'n_words': 1489}