/*******************************************************************************
 * Copyright (c) 2008, 2024 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *     Code 9 Corporation - on going enhancements and maintenance
 *     Johannes Ahlers <Johannes.Ahlers@gmx.de> - bug 477677
 *******************************************************************************/
package org.eclipse.pde.internal.ui.preferences;

import static org.eclipse.swt.events.SelectionListener.widgetSelectedAdapter;

import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;

import org.eclipse.core.internal.resources.Project;
import org.eclipse.core.resources.ICommand;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IncrementalProjectBuilder;
import org.eclipse.core.resources.ProjectScope;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.core.runtime.preferences.DefaultScope;
import org.eclipse.core.runtime.preferences.IEclipsePreferences;
import org.eclipse.core.runtime.preferences.IScopeContext;
import org.eclipse.core.runtime.preferences.InstanceScope;
import org.eclipse.jface.dialogs.IDialogSettings;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.jface.util.Util;
import org.eclipse.jface.window.Window;
import org.eclipse.pde.internal.core.PDECore;
import org.eclipse.pde.internal.core.builders.CompilerFlags;
import org.eclipse.pde.internal.core.natures.BaseProject;
import org.eclipse.pde.internal.core.natures.FeatureProject;
import org.eclipse.pde.internal.core.natures.PluginProject;
import org.eclipse.pde.internal.ui.PDEPlugin;
import org.eclipse.pde.internal.ui.PDEUIMessages;
import org.eclipse.pde.internal.ui.SWTFactory;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.ScrolledComposite;
import org.eclipse.swt.events.ModifyListener;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Combo;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.TabFolder;
import org.eclipse.swt.widgets.TabItem;
import org.eclipse.swt.widgets.Text;
import org.eclipse.ui.forms.events.ExpansionAdapter;
import org.eclipse.ui.forms.events.ExpansionEvent;
import org.eclipse.ui.forms.widgets.ExpandableComposite;
import org.eclipse.ui.preferences.IWorkbenchPreferenceContainer;
import org.eclipse.ui.preferences.IWorkingCopyManager;
import org.eclipse.ui.preferences.WorkingCopyManager;
import org.osgi.service.prefs.BackingStoreException;

/**
 * A configuration block for setting PDE compiler preferences
 *
 * @since 3.4
 */
public class PDECompilersConfigurationBlock extends ConfigurationBlock {

	/**
	 * Provides data information for created controls
	 */
	protected static class ControlData {
		private final Key key;
		private final String[] values;

		/**
		 * Constructor
		 */
		public ControlData(Key key, String[] values) {
			this.key = key;
			this.values = values;
		}

		public Key getKey() {
			return key;
		}

		public String getValue(boolean selection) {
			int index = selection ? 0 : 1;
			return values[index];
		}

		public String getValue(int index) {
			return values[index];
		}

		public int getSelection(String value) {
			if (value != null) {
				for (int i = 0; i < values.length; i++) {
					if (value.equals(values[i])) {
						return i;
					}
				}
			}
			return values.length - 1; // assume the last option is the least severe
		}
	}

	/**
	 * Provides management for changed/stored values for a given preference key
	 */
	public static class Key {

		private final String qualifier;
		private final String key;

		/**
		 * Constructor
		 */
		public Key(String qualifier, String key) {
			this.qualifier = qualifier;
			this.key = key;
		}

		/**
		 * Returns the {@link IEclipsePreferences} node for the given context and {@link IWorkingCopyManager}
		 * @return the {@link IEclipsePreferences} node or <code>null</code>
		 */
		private IEclipsePreferences getNode(IScopeContext context, IWorkingCopyManager manager) {
			IEclipsePreferences node = context.getNode(this.qualifier);
			if (manager != null) {
				return manager.getWorkingCopy(node);
			}
			return node;
		}

		/**
		 * Returns the value stored in the {@link IEclipsePreferences} node from the given context and working copy manager
		 * @return the value from the {@link IEclipsePreferences} node or <code>null</code>
		 */
		public String getStoredValue(IScopeContext context, IWorkingCopyManager manager) {
			IEclipsePreferences node = getNode(context, manager);
			if (node != null) {
				return node.get(this.key, null);
			}
			return null;
		}

		/**
		 * Returns the stored value of this {@link IEclipsePreferences} node using a given lookup order, and allowing the
		 * top scope to be ignored
		 * @return the value from the {@link IEclipsePreferences} node or <code>null</code>
		 */
		public String getStoredValue(IScopeContext[] lookupOrder, boolean ignoreTopScope, IWorkingCopyManager manager) {
			for (int i = ignoreTopScope ? 1 : 0; i < lookupOrder.length; i++) {
				String value = getStoredValue(lookupOrder[i], manager);
				if (value != null) {
					return value;
				}
			}
			return null;
		}

		/**
		 * Sets the value of this key
		 */
		public void setStoredValue(IScopeContext context, String value, IWorkingCopyManager manager) {
			IEclipsePreferences node = getNode(context, manager);
			if (value != null) {
				node.put(this.key, value);
			} else {
				node.remove(this.key);
			}
		}

		@Override
		public String toString() {
			return qualifier + '/' + this.key;
		}

		public Object getName() {
			return key;
		}

		public Object getQualifier() {
			return qualifier;
		}
	}

	/**
	 * Returns a new {@link Key} for the {@link PDEPlugin} preference store
	 * @return the new {@link Key} for the {@link PDEPlugin} preference store
	 */
	protected final static Key getPDEPrefKey(String key) {
		return new Key(BaseProject.PDE_PLUGIN_ID, key);
	}

	//unresolved
	private static final Key KEY_P_UNRESOLVED_IMPORTS = getPDEPrefKey(CompilerFlags.P_UNRESOLVED_IMPORTS);
	private static final Key KEY_P_UNRESOLVED_EX_POINTS = getPDEPrefKey(CompilerFlags.P_UNRESOLVED_EX_POINTS);
	private static final Key KEY_F_UNRESOLVED_PLUGINS = getPDEPrefKey(CompilerFlags.F_UNRESOLVED_PLUGINS);
	private static final Key KEY_F_UNRESOLVED_FEATURES = getPDEPrefKey(CompilerFlags.F_UNRESOLVED_FEATURES);

	//unknown elements
	private static final Key KEY_P_UNKNOWN_ELEMENT = getPDEPrefKey(CompilerFlags.P_UNKNOWN_ELEMENT);
	private static final Key KEY_P_UNKNOWN_ATTRIBUTE = getPDEPrefKey(CompilerFlags.P_UNKNOWN_ATTRIBUTE);
	private static final Key KEY_P_UNKNOWN_CLASS = getPDEPrefKey(CompilerFlags.P_UNKNOWN_CLASS);
	private static final Key KEY_P_UNKNOWN_RESOURCE = getPDEPrefKey(CompilerFlags.P_UNKNOWN_RESOURCE);
	private static final Key KEY_P_UNKNOWN_IDENTIFIER = getPDEPrefKey(CompilerFlags.P_UNKNOWN_IDENTIFIER);

	//general
	private static final Key KEY_P_DISCOURAGED_CLASS = getPDEPrefKey(CompilerFlags.P_DISCOURAGED_CLASS);
	private static final Key KEY_P_NO_REQUIRED_ATT = getPDEPrefKey(CompilerFlags.P_NO_REQUIRED_ATT);
	private static final Key KEY_P_NOT_EXTERNALIZED = getPDEPrefKey(CompilerFlags.P_NOT_EXTERNALIZED);
	private static final Key KEY_P_INCOMPATIBLE_ENV = getPDEPrefKey(CompilerFlags.P_INCOMPATIBLE_ENV);
	private static final Key KEY_P_MISSING_EXPORT_PKGS = getPDEPrefKey(CompilerFlags.P_MISSING_EXPORT_PKGS);
	private static final Key KEY_P_DEPRECATED = getPDEPrefKey(CompilerFlags.P_DEPRECATED);
	private static final Key KEY_P_INTERNAL = getPDEPrefKey(CompilerFlags.P_INTERNAL);
	private static final Key KEY_P_SERVICE_COMP_WITHOUT_LAZY = getPDEPrefKey(CompilerFlags.P_SERVICE_COMP_WITHOUT_LAZY_ACT);
	private static final Key KEY_P_NO_AUTOMATIC_MODULE_NAME = getPDEPrefKey(CompilerFlags.P_NO_AUTOMATIC_MODULE);

	// build
	private static final Key KEY_P_BUILD_SOURCE_LIBRARY = getPDEPrefKey(CompilerFlags.P_BUILD_SOURCE_LIBRARY);
	private static final Key KEY_P_BUILD_OUTPUT_LIBRARY = getPDEPrefKey(CompilerFlags.P_BUILD_OUTPUT_LIBRARY);
	private static final Key KEY_P_BUILD_MISSING_OUTPUT = getPDEPrefKey(CompilerFlags.P_BUILD_MISSING_OUTPUT);
	private static final Key KEY_P_BUILD_SRC_INCLUDES = getPDEPrefKey(CompilerFlags.P_BUILD_SRC_INCLUDES);
	private static final Key KEY_P_BUILD_BIN_INCLUDES = getPDEPrefKey(CompilerFlags.P_BUILD_BIN_INCLUDES);
	private static final Key KEY_P_BUILD_JAVA_COMPLIANCE = getPDEPrefKey(CompilerFlags.P_BUILD_JAVA_COMPLIANCE);
	private static final Key KEY_P_BUILD_JAVA_COMPILER = getPDEPrefKey(CompilerFlags.P_BUILD_JAVA_COMPILER);
	private static final Key KEY_P_BUILD_ENCODINGS = getPDEPrefKey(CompilerFlags.P_BUILD_ENCODINGS);
	private static final Key KEY_P_BUILD = getPDEPrefKey(CompilerFlags.P_BUILD);

	// versioning
	private static final Key KEY_P_VERSION_EXP_PKG = getPDEPrefKey(CompilerFlags.P_MISSING_VERSION_EXP_PKG);
	private static final Key KEY_P_VERSION_IMP_PKG = getPDEPrefKey(CompilerFlags.P_MISSING_VERSION_IMP_PKG);
	private static final Key KEY_P_VERSION_REQ_BUNDLE = getPDEPrefKey(CompilerFlags.P_MISSING_VERSION_REQ_BUNDLE);
	private static final Key KEY_P_VERSION_EXEC_ENV_TOO_LOW = getPDEPrefKey(CompilerFlags.P_EXEC_ENV_TOO_LOW);

	private static final Key KEY_S_CREATE_DOCS = getPDEPrefKey(CompilerFlags.S_CREATE_DOCS);
	private static final Key KEY_S_DOC_FOLDER = getPDEPrefKey(CompilerFlags.S_DOC_FOLDER);
	private static final Key KEY_S_OPEN_TAGS = getPDEPrefKey(CompilerFlags.S_OPEN_TAGS);

	private static String[] SEVERITIES = {PDEUIMessages.PDECompilersConfigurationBlock_error,
			PDEUIMessages.PDECompilersConfigurationBlock_warning, PDEUIMessages.PDECompilersConfigurationBlock_info,
			PDEUIMessages.PDECompilersConfigurationBlock_ignore
			};

	private static Key[] fgAllKeys = {KEY_F_UNRESOLVED_FEATURES, KEY_F_UNRESOLVED_PLUGINS, KEY_P_BUILD, KEY_P_BUILD_MISSING_OUTPUT, KEY_P_BUILD_SOURCE_LIBRARY, KEY_P_BUILD_OUTPUT_LIBRARY, KEY_P_BUILD_SRC_INCLUDES, KEY_P_BUILD_BIN_INCLUDES, KEY_P_BUILD_JAVA_COMPLIANCE, KEY_P_BUILD_JAVA_COMPILER, KEY_P_BUILD_ENCODINGS, KEY_P_INTERNAL, KEY_P_SERVICE_COMP_WITHOUT_LAZY, KEY_P_NO_AUTOMATIC_MODULE_NAME, KEY_P_DEPRECATED, KEY_P_DISCOURAGED_CLASS, KEY_P_INCOMPATIBLE_ENV, KEY_P_MISSING_EXPORT_PKGS, KEY_P_NO_REQUIRED_ATT, KEY_P_NOT_EXTERNALIZED, KEY_P_UNKNOWN_ATTRIBUTE, KEY_P_UNKNOWN_CLASS, KEY_P_UNKNOWN_ELEMENT, KEY_P_UNKNOWN_IDENTIFIER, KEY_P_UNKNOWN_RESOURCE, KEY_P_UNRESOLVED_EX_POINTS, KEY_P_UNRESOLVED_IMPORTS, KEY_P_VERSION_EXP_PKG, KEY_P_VERSION_IMP_PKG, KEY_P_VERSION_REQ_BUNDLE, KEY_P_VERSION_EXEC_ENV_TOO_LOW, KEY_S_CREATE_DOCS, KEY_S_DOC_FOLDER, KEY_S_OPEN_TAGS };

	/**
	 * Constant representing the {@link IDialogSettings} section for this block
	 */
	private static final String SETTINGS = "pde_compiler_errorwarnings_block"; //$NON-NLS-1$

	/**
	 * The context of settings locations to search for values in
	 */
	private IScopeContext[] fLookupOrder = null;

	/**
	 * The project this block is working on settings for. Only applies in the
	 * case of project specific settings
	 */
	private IProject fProject = null;

	/**
	 * the working copy manager to work with settings
	 */
	private IWorkingCopyManager fManager = null;

	/**
	 * The main composite for the configuration block, used for enabling/disabling the block
	 */
	private Composite fMainComp = null;

	/**
	 * The tab folder for the various tabs
	 */
	private TabFolder fTabFolder = null;

	/**
	 * Stored old fProject specific settings.
	 */
	private IdentityHashMap<Key, String> fOldProjectSettings = null;

	/**
	 * Map of controls to the tab they appear on. Allows for optimizing which builders
	 * are used if changes are made
	 */
	private final HashMap<Integer, HashSet<Control>> fControlMap = new HashMap<>(3);
	/**
	 * Map of combo and label
	 */
	private final HashMap<Combo, Label> fComboLabelMap = new HashMap<>();

	/**
	 * Listing of all of the {@link ExpandableComposite}s in the block
	 */
	private final ArrayList<ExpandableComposite> fExpComps = new ArrayList<>();

	/**
	 * Flag used to know if the page needs saving or not
	 */
	private boolean fDirty = false;

	/**
	 * counter to know how many times we have prompted users' to rebuild
	 */
	private int fRebuildcount = 0;

	/**
	 * Set of builders to use when building if there are changes
	 */
	private final HashSet<String> fBuilders = new HashSet<>(4);

	/**
	 * The parent this block has been added to
	 */
	private Composite fParent = null;

	public static Key[] getAllKeys() {
		return fgAllKeys;
	}
	/**
	 * Default selection listener for combo and check controls
	 */
	private final SelectionListener selectionlistener = widgetSelectedAdapter(e -> {
		if (e.widget instanceof Combo) {
			Combo combo = (Combo) e.widget;
			ControlData data = (ControlData) combo.getData();
			int selectionIndex = combo.getSelectionIndex();
			selectionIndex = adjustIndex(selectionIndex);
			data.key.setStoredValue(fLookupOrder[0], Integer.toString(selectionIndex), fManager);
			fDirty = true;
			fRebuildcount = 0;
		} else if (e.widget instanceof Button button) {
			ControlData data = (ControlData) button.getData();
			data.key.setStoredValue(fLookupOrder[0], Boolean.toString(button.getSelection()), fManager);
			fDirty = true;
			fRebuildcount = 0;
		}
		addBuilder((Control) e.widget);
	});

	/**
	 * Default modify listener for text controls
	 */
	private final ModifyListener modifylistener = e -> {
		if (e.widget instanceof Text) {
			Text text = (Text) e.widget;
			ControlData data = (ControlData) text.getData();
			data.key.setStoredValue(fLookupOrder[0], text.getText().trim(), fManager);
			fDirty = true;
			fRebuildcount = 0;
		}
	};

	public PDECompilersConfigurationBlock(IProject project, IWorkbenchPreferenceContainer container) {
		fProject = project;
		if (fProject != null) {
			fLookupOrder = new IScopeContext[] {new ProjectScope(fProject), InstanceScope.INSTANCE, DefaultScope.INSTANCE};
		} else {
			fLookupOrder = new IScopeContext[] {InstanceScope.INSTANCE, DefaultScope.INSTANCE};
		}
		if (container == null) {
			fManager = new WorkingCopyManager();
		} else {
			fManager = container.getWorkingCopyManager();
		}
		if (fProject == null || hasProjectSpecificSettings(fProject)) {
			fOldProjectSettings = null;
		} else {
			fOldProjectSettings = new IdentityHashMap<>();
			for (Key key : fgAllKeys) {
				fOldProjectSettings.put(key, key.getStoredValue(fLookupOrder, false, fManager));
			}
		}
		//make it load so we have access to the pde preferences initialized via pde core preferences
		//https://bugs.eclipse.org/bugs/show_bug.cgi?id=273017
		PDECore.getDefault().getPreferencesManager();
	}

	private int adjustIndex(int selectionIndex) {
		// ignore is stored as 2 in org.eclipse.pde.prefs but its index is 3
		// info is stored as 3 in org.eclipse.pde.prefs but its index is 2
		// if info is put as 2, then all previous settings of ignore will become
		// info.
		if (selectionIndex == 2)
			return 3;
		else if (selectionIndex == 3)
			return 2;
		return selectionIndex; // nothing to do for error and warning
	}

	/**
	 * returns if this block has fProject specific settings
	 * @return true if there are fProject specific settings, false otherwise
	 */
	public boolean hasProjectSpecificSettings(IProject project) {
		if (project != null) {
			IScopeContext projectContext = new ProjectScope(project);
			for (Key key : fgAllKeys) {
				if (key.getStoredValue(projectContext, fManager) != null) {
					return true;
				}
			}
		}
		return false;
	}

	/**
	 * Sets using project specific settings
	 */
	public void useProjectSpecificSettings(boolean enable) {
		boolean disabled = fOldProjectSettings == null;
		if (enable != disabled && fProject != null) {
			if (enable) {
				for (Key key : fgAllKeys) {
					key.setStoredValue(fLookupOrder[0], fOldProjectSettings.get(key), fManager);
				}
				fOldProjectSettings = null;
				updateControls();
			} else {
				fOldProjectSettings = new IdentityHashMap<>();
				String old = null;
				for (Key key : fgAllKeys) {
					old = key.getStoredValue(fLookupOrder, false, fManager);
					fOldProjectSettings.put(key, old);
					key.setStoredValue(fLookupOrder[0], null, fManager);
				}
			}
		}
		fDirty = true;
		enableControl(fMainComp, enable);
	}

	/**
	 * Updates all of the registered {@link Control}s on the page.
	 * Registration implies that the {@link Control} was added to the map
	 * fControlMap
	 */
	private void updateControls() {
		for (Set<Control> controls : fControlMap.values()) {
			if (controls == null) {
				continue;
			}
			for (Control control : controls) {
				if (control instanceof Combo combo) {
					ControlData data = (ControlData) combo.getData();
					int index = 0;
					try {
						index = Integer.parseInt(data.key.getStoredValue(fLookupOrder, false, fManager));
					} catch (Exception e) {
						//set the default if something goes wrong
						index = Integer.parseInt(data.key.getStoredValue(fLookupOrder, true, fManager));
					}
					index = adjustIndex(index);
					combo.select(data.getSelection(SEVERITIES[index]));
				} else if (control instanceof Button button) {
					ControlData data = (ControlData) button.getData();
					button.setSelection(Boolean.parseBoolean(data.key.getStoredValue(fLookupOrder, false, fManager)));
				} else if (control instanceof Text text) {
					ControlData data = (ControlData) text.getData();
					text.setText(data.key.getStoredValue(fLookupOrder, false, fManager));
				}
			}
		}
	}

	/**
	 * recursive method to enable/disable all of the controls on the main page
	 */
	private void enableControl(Control ctrl, boolean enabled) {
		ctrl.setEnabled(enabled);
		if (ctrl instanceof Composite comp) {
			Control[] children = comp.getChildren();
			for (Control child : children) {
				enableControl(child, enabled);
			}
		}
	}

	/**
	 * Creates the control in the parent control
	 *
	 * @param parent the parent control
	 */
	public Control createControl(Composite parent) {
		fParent = parent;
		fMainComp = SWTFactory.createComposite(parent, 1, 1, GridData.FILL_BOTH, 0, 0);
		SWTFactory.createVerticalSpacer(parent, 1);
		fTabFolder = new TabFolder(fMainComp, SWT.NONE);
		GridData gd = new GridData(GridData.FILL_BOTH);
		gd.heightHint = 375;
		fTabFolder.setLayoutData(gd);

		Composite main = (fTabFolder == null ? fMainComp : fTabFolder);
		//plugins page
		createPage(CompilerFlags.PLUGIN_FLAGS, main, PDEUIMessages.CompilersConfigurationBlock_plugins,
				PDEUIMessages.CompilersConfigurationBlock_label);

		// the sharing property page now use these two tabs too
		// schema page
		createPage(CompilerFlags.SCHEMA_FLAGS, main, PDEUIMessages.CompilersConfigurationBlock_schemas,
				PDEUIMessages.CompilersConfigurationBlock_altlabel);

		// features / sites page
		createPage(CompilerFlags.FEATURE_FLAGS, main, PDEUIMessages.CompilersConfigurationBlock_features,
				PDEUIMessages.CompilersConfigurationBlock_label);
		restoreExpansionState();
		org.eclipse.jface.dialogs.Dialog.applyDialogFont(fParent);
		return fMainComp;
	}

	/**
	 * Creates a tab page parented in the folder
	 * @return a new composite to act as the page for a tab
	 */
	private Composite createPage(int kind, Composite folder, String name, String description) {
		Composite page = SWTFactory.createComposite(folder, 1, 1, GridData.FILL_BOTH);
		Composite parent = page;
		TabItem tab = new TabItem((TabFolder) folder, SWT.NONE);
		tab.setText(name);
		tab.setControl(page);
		parent = SWTFactory.createComposite(page, 2, 1, GridData.FILL_BOTH);
		SWTFactory.createWrapLabel(parent, description, 2);
		SWTFactory.createVerticalSpacer(parent, 1);
		switch (kind) {
			case CompilerFlags.PLUGIN_FLAGS : {
				ScrolledComposite scomp = createScrolledComposite(parent, 1);
				Composite sbody = SWTFactory.createComposite(scomp, 1, 1, GridData.FILL_BOTH);
				scomp.setContent(sbody);
				// General
				Composite client = createExpansibleComposite(sbody, PDEUIMessages.PDECompilersConfigurationBlock_general);
				initializeComboControls(client, new String[] {PDEUIMessages.compilers_p_no_required_att, PDEUIMessages.CompilersConfigurationTab_incompatEnv, PDEUIMessages.compilers_p_exported_pkgs, PDEUIMessages.compilers_p_no_automatic_module_name}, new Key[] {KEY_P_NO_REQUIRED_ATT, KEY_P_INCOMPATIBLE_ENV, KEY_P_MISSING_EXPORT_PKGS, KEY_P_NO_AUTOMATIC_MODULE_NAME}, CompilerFlags.PLUGIN_FLAGS);
				//build.properties
				client = createExpansibleComposite(sbody, PDEUIMessages.PDECompilersConfigurationBlock_build);
				initializeComboControls(client, new String[] {PDEUIMessages.PDECompilersConfigurationBlock_1, PDEUIMessages.PDECompilersConfigurationBlock_2, PDEUIMessages.PDECompilersConfigurationBlock_0, PDEUIMessages.PDECompilersConfigurationBlock_3, PDEUIMessages.PDECompilersConfigurationBlock_4, PDEUIMessages.PDECompilersConfigurationBlock_5, PDEUIMessages.PDECompilersConfigurationBlock_6, PDEUIMessages.PDECompilersConfigurationBlock_7, PDEUIMessages.CompilersConfigurationTab_buildPropertiesErrors}, new Key[] {KEY_P_BUILD_SOURCE_LIBRARY, KEY_P_BUILD_OUTPUT_LIBRARY, KEY_P_BUILD_MISSING_OUTPUT, KEY_P_BUILD_BIN_INCLUDES, KEY_P_BUILD_SRC_INCLUDES, KEY_P_BUILD_JAVA_COMPLIANCE, KEY_P_BUILD_JAVA_COMPILER, KEY_P_BUILD_ENCODINGS, KEY_P_BUILD}, CompilerFlags.PLUGIN_FLAGS);
				// Versioning
				client = createExpansibleComposite(sbody, PDEUIMessages.PDECompilersConfigurationBlock_versioning);
				initializeComboControls(client, new String[] {PDEUIMessages.compilers_p_missing_require_bundle, PDEUIMessages.compilers_p_missing_exp_pkg, PDEUIMessages.compilers_p_missing_imp_pkg, PDEUIMessages.compilers_p_exec_env_too_low}, new Key[] {KEY_P_VERSION_REQ_BUNDLE, KEY_P_VERSION_EXP_PKG, KEY_P_VERSION_IMP_PKG, KEY_P_VERSION_EXEC_ENV_TOO_LOW}, CompilerFlags.PLUGIN_FLAGS);
				// Usage
				client = createExpansibleComposite(sbody, PDEUIMessages.PDECompilersConfigurationBlock_usage);
				initializeComboControls(client, new String[] { PDEUIMessages.compilers_p_unresolved_import, PDEUIMessages.compilers_p_unresolved_ex_points, PDEUIMessages.compilers_p_internal, PDEUIMessages.compilers_p_deprecated, PDEUIMessages.compilers_p_not_externalized_att, PDEUIMessages.compilers_p_service_component_without_lazy_act }, new Key[] { KEY_P_UNRESOLVED_IMPORTS, KEY_P_UNRESOLVED_EX_POINTS, KEY_P_INTERNAL, KEY_P_DEPRECATED, KEY_P_NOT_EXTERNALIZED, KEY_P_SERVICE_COMP_WITHOUT_LAZY }, CompilerFlags.PLUGIN_FLAGS);
				// References
				client = createExpansibleComposite(sbody, PDEUIMessages.PDECompilersConfigurationBlock_references);
				initializeComboControls(client, new String[] {PDEUIMessages.compilers_p_unknown_element, PDEUIMessages.compilers_p_unknown_attribute, PDEUIMessages.compilers_p_unknown_class, PDEUIMessages.compilers_p_discouraged_class, PDEUIMessages.compilers_p_unknown_resource, PDEUIMessages.compilers_p_unknown_identifier}, new Key[] {KEY_P_UNKNOWN_ELEMENT, KEY_P_UNKNOWN_ATTRIBUTE, KEY_P_UNKNOWN_CLASS, KEY_P_DISCOURAGED_CLASS, KEY_P_UNKNOWN_RESOURCE, KEY_P_UNKNOWN_IDENTIFIER,}, CompilerFlags.PLUGIN_FLAGS);

				break;
			}
			case CompilerFlags.SCHEMA_FLAGS : {
				createCheckControl(parent, PDEUIMessages.compilers_s_create_docs, KEY_S_CREATE_DOCS, CompilerFlags.SCHEMA_FLAGS);
				Composite comp = SWTFactory.createComposite(parent, 2, 2, GridData.FILL_HORIZONTAL, 0, 0);
				createTextControl(comp, PDEUIMessages.compilers_s_doc_folder, KEY_S_DOC_FOLDER, CompilerFlags.SCHEMA_FLAGS);
				SWTFactory.createVerticalSpacer(parent, 1);
				initializeComboControls(parent, new String[] {PDEUIMessages.compilers_s_open_tags}, new Key[] {KEY_S_OPEN_TAGS}, CompilerFlags.SCHEMA_FLAGS);
				break;
			}
			case CompilerFlags.FEATURE_FLAGS : {
				initializeComboControls(parent, new String[] {PDEUIMessages.compilers_f_unresolved_features, PDEUIMessages.compilers_f_unresolved_plugins}, new Key[] {KEY_F_UNRESOLVED_FEATURES, KEY_F_UNRESOLVED_PLUGINS}, CompilerFlags.FEATURE_FLAGS);
				break;
			}
		}
		return page;
	}

	/**
	 * Creates a set of combo boxes for the given string/ key pairs
	 */
	private void initializeComboControls(Composite composite, String[] labels, Key[] keys, int tabkind) {
		for (int i = 0, max = labels.length; i < max; i++) {
			createComboControl(composite, labels[i], keys[i], tabkind);
		}
	}

	/**
	 * Creates a checkbox button control in the parent
	 */
	private void createCheckControl(Composite parent, String label, Key key, int tabkind) {
		Button button = SWTFactory.createCheckButton(parent, label, null, false, 2);
		ControlData data = new ControlData(key, new String[] {Boolean.toString(false)});
		button.setData(data);
		button.setSelection(Boolean.parseBoolean(data.key.getStoredValue(fLookupOrder, false, fManager)));
		button.addSelectionListener(selectionlistener);
		Integer mapkey = Integer.valueOf(tabkind);
		HashSet<Control> controls = fControlMap.get(mapkey);
		if (controls == null) {
			controls = new HashSet<>(8);
			fControlMap.put(mapkey, controls);
		}
		controls.add(button);
	}

	/**
	 * Disposes the controls from this page
	 */
	public void dispose() {
		fMainComp.getParent().dispose();
		fExpComps.clear();
		fControlMap.clear();
		fComboLabelMap.clear();
	}

	/**
	 * Creates a {@link Label} | {@link Combo} control. The combo is initialized from the given {@link Key}
	 */
	protected void createComboControl(Composite parent, String label, Key key, int tabkind) {
		Label lbl = new Label(parent, SWT.NONE);
		GridData gd = new GridData(GridData.BEGINNING, GridData.CENTER, true, false);
		lbl.setLayoutData(gd);
		lbl.setText(label);
		if (key.equals(KEY_P_NO_AUTOMATIC_MODULE_NAME)) {
			lbl.setToolTipText(PDEUIMessages.PDECompilersConfigurationBlock_8);
		}
		Combo combo = new Combo(parent, SWT.DROP_DOWN | SWT.READ_ONLY);
		gd = new GridData(GridData.END, GridData.CENTER, false, false);
		ControlData data = new ControlData(key, SEVERITIES);
		combo.setData(data);
		combo.setItems(SEVERITIES);
		combo.addSelectionListener(selectionlistener);
		int index = 0;
		String value = key.getStoredValue(fLookupOrder, false, fManager);
		if (value == null)
			value = key.getStoredValue(fLookupOrder, true, fManager);

		if (value != null)
			index = Integer.parseInt(value);
		index = adjustIndex(index);
		combo.select(data.getSelection(SEVERITIES[index]));
		Integer mapkey = Integer.valueOf(tabkind);
		HashSet<Control> controls = fControlMap.get(mapkey);
		if (controls == null) {
			controls = new HashSet<>(8);
			fControlMap.put(mapkey, controls);
		}
		controls.add(combo);
		addHighlight(parent, lbl, combo);
		if (Util.isMac())
			fComboLabelMap.put(combo, lbl);
	}

	/**
	 * Creates a new text control on the parent for the given pref key
	 */
	private void createTextControl(Composite parent, String label, Key key, int tabkind) {
		SWTFactory.createLabel(parent, label, 1);
		Text text = SWTFactory.createSingleText(parent, 1);
		ControlData data = new ControlData(key, new String[0]);
		text.setData(data);
		String value = data.key.getStoredValue(fLookupOrder, false, fManager);
		if (value != null)
			text.setText(value);
		text.addModifyListener(modifylistener);
		Integer mapkey = Integer.valueOf(tabkind);
		HashSet<Control> controls = fControlMap.get(mapkey);
		if (controls == null) {
			controls = new HashSet<>(8);
			fControlMap.put(mapkey, controls);
		}
		controls.add(text);
	}

	/**
	 * Creates a scrolled composite
	 * @return a new scrolled composite
	 */
	protected ScrolledComposite createScrolledComposite(Composite parent, int columns) {
		ScrolledComposite scomp = new ScrolledComposite(parent, SWT.H_SCROLL | SWT.V_SCROLL);
		scomp.setExpandHorizontal(true);
		scomp.setExpandVertical(true);
		scomp.setLayout(new GridLayout(1, false));
		scomp.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
		scomp.addListener(SWT.Resize, event -> handleExpand(getScrollingParent(event.widget)));
		return scomp;
	}

	/**
	 * Creates an {@link ExpandableComposite} with a client composite and a default grid layout
	 * @return a new expandable composite
	 */
	private Composite createExpansibleComposite(Composite parent, String title) {
		ExpandableComposite ecomp = SWTFactory.createExpandibleComposite(parent, title, 1, GridData.FILL_HORIZONTAL);
		ecomp.addExpansionListener(new ExpansionAdapter() {
			@Override
			public void expansionStateChanged(ExpansionEvent e) {
				Object obj = e.getSource();
				handleExpand(getScrollingParent(obj));
			}
		});
		ecomp.setFont(JFaceResources.getFontRegistry().getBold(JFaceResources.DIALOG_FONT));
		fExpComps.add(ecomp);
		Composite client = SWTFactory.createComposite(ecomp, 2, 1, GridData.FILL_BOTH);
		ecomp.setClient(client);
		return client;
	}

	/**
	 * Returns the scrolling parent for the given ExpandibleComposite object
	 * @return the scrolling parent of the given object or <code>null</code> if there isn't one
	 */
	private ScrolledComposite getScrollingParent(Object obj) {
		if (obj instanceof ExpandableComposite ecomp) {
			Composite parent = ecomp.getParent();
			while (parent != null && !(parent instanceof ScrolledComposite)) {
				parent = parent.getParent();
			}
			if (parent != null) {
				return (ScrolledComposite) parent;
			}
		}
		if (obj instanceof ScrolledComposite) {
			return (ScrolledComposite) obj;
		}
		return null;
	}

	/**
	 * Handles one of the expandable composites being expanded
	 */
	private void handleExpand(ScrolledComposite composite) {
		if (composite == null) {
			return;
		}
		try {
			composite.setRedraw(false);
			Composite c = (Composite) composite.getContent();
			if (c == null) {
				return;
			}
			Point newSize = c.computeSize(SWT.DEFAULT, SWT.DEFAULT);
			composite.setMinSize(newSize);
			c.layout(true);
		} finally {
			composite.setRedraw(true);
		}
	}

	/**
	 * Saves all of the changes on the page
	 */
	public void performOK() {
		save();
		persistExpansionState();
	}

	/**
	 * Directly applies all of the changes on the page
	 */
	public void performApply() {
		save();
	}

	/**
	 * Performs the save operation on the working cop manager
	 */
	private void save() {
		if (fDirty) {
			try {
				ArrayList<Key> changes = new ArrayList<>();
				collectChanges(fLookupOrder[0], changes);
				if (!changes.isEmpty()) {
					if (fRebuildcount < 1) {
						fRebuildcount++;
						fManager.applyChanges();
						String message = PDEUIMessages.PDECompilersConfigurationBlock_settings_changed_all;
						if (fProject != null) {
							message = MessageFormat.format(PDEUIMessages.PDECompilersConfigurationBlock_setting_changed_project, fProject.getName());
						}
						int open = MessageDialog.open(MessageDialog.QUESTION, fParent.getShell(),
								PDEUIMessages.PDECompilersConfigurationBlock_settings_changed, message, SWT.NONE,
								PDEUIMessages.PDECompilersConfigurationBlock_buildButtonLabel,
								PDEUIMessages.PDECompilersConfigurationBlock_dontBuildButtonLabel);
						if (open == Window.OK) {
							doFullBuild();
						}
					}
				}
				fDirty = false;
			} catch (BackingStoreException bse) {
				PDEPlugin.log(bse);
			}
		}
	}

	/**
	 * Collects the keys that have changed on the page into the specified list
	 * @param changes the {@link List} to collect changed keys into
	 */
	private void collectChanges(IScopeContext context, List<Key> changes) {
		Key key = null;
		String origval = null, newval = null;
		boolean complete = fOldProjectSettings == null && fProject != null;
		for (Key fgKey : fgAllKeys) {
			key = fgKey;
			origval = key.getStoredValue(context, null);
			newval = key.getStoredValue(context, fManager);
			if (newval == null) {
				if (origval != null) {
					changes.add(key);
				} else if (complete) {
					key.setStoredValue(context, key.getStoredValue(fLookupOrder, true, fManager), fManager);
					changes.add(key);
				}
			} else if (!newval.equals(origval)) {
				changes.add(key);
			}
		}
	}

	/**
	 * Cancels all of the changes on the page
	 */
	public void performCancel() {
		persistExpansionState();
	}

	/**
	 * Reverts all of the settings back to their defaults
	 */
	public void performDefaults() {
		String defval = null;
		for (Key key : fgAllKeys) {
			defval = key.getStoredValue(fLookupOrder, true, fManager);
			key.setStoredValue(fLookupOrder[0], defval, fManager);
		}
		updateControls();
		fDirty = true;
		fRebuildcount = 0;
	}

	/**
	 * Stores the expansion state of the composites
	 */
	private void persistExpansionState() {
		IDialogSettings settings = PDEPlugin.getDefault().getDialogSettings().addNewSection(SETTINGS);
		for (int i = 0; i < fExpComps.size(); i++) {
			settings.put(Integer.toString(i), fExpComps.get(i).isExpanded());
		}
	}

	/**
	 * Restores the expansion state of the composites in this block.
	 * If there are no settings, than the first composite is expanded by default
	 */
	private void restoreExpansionState() {
		IDialogSettings settings = PDEPlugin.getDefault().getDialogSettings().getSection(SETTINGS);
		if (settings != null) {
			for (int i = 0; i < fExpComps.size(); i++) {
				fExpComps.get(i).setExpanded(settings.getBoolean(Integer.toString(i)));
			}
		} else {
			fExpComps.get(0).setExpanded(true);
		}
	}

	/**
	 * Adds the associated builder for the tab this control lives on. Lookups are done using
	 * hash codes for constant time containment checks
	 */
	private void addBuilder(Control control) {
		for (Entry<Integer, HashSet<Control>> entry : fControlMap.entrySet()) {
			Integer key = entry.getKey();
			HashSet<Control> controls = entry.getValue();
			if (controls == null) {
				continue;
			}
			if (controls.contains(control)) {
				switch (key.intValue()) {
					case CompilerFlags.PLUGIN_FLAGS -> fBuilders.add(PluginProject.MANIFEST_BUILDER_ID);
					case CompilerFlags.SCHEMA_FLAGS -> fBuilders.add(PluginProject.SCHEMA_BUILDER_ID);
					case CompilerFlags.FEATURE_FLAGS, CompilerFlags.SITE_FLAGS -> fBuilders.add(FeatureProject.BUILDER_ID);
				}
			}
			return;
		}
	}

	/**
	 * Performs a full build of the workspace
	 */
	private void doFullBuild() {
		Job buildJob = new Job(PDEUIMessages.CompilersConfigurationBlock_building) {
			@Override
			public boolean belongsTo(Object family) {
				return ResourcesPlugin.FAMILY_MANUAL_BUILD == family;
			}

			@Override
			protected IStatus run(IProgressMonitor monitor) {
				try {
					IProject[] projects = null;
					if (fProject == null) {
						projects = PDEPlugin.getWorkspace().getRoot().getProjects();
					} else {
						projects = new IProject[] {fProject};
					}
					SubMonitor subMonitor = SubMonitor.convert(monitor, projects.length * 2);
					for (IProject projectToBuild : projects) {
						SubMonitor iterationMonitor = subMonitor.split(2).setWorkRemaining(2);
						if (!projectToBuild.isOpen()) {
							continue;
						}
						@SuppressWarnings("restriction")
						ICommand[] commands = ((Project) projectToBuild).internalGetDescription().getBuildSpec(false);
						if (PluginProject.isPluginProject(projectToBuild)) {
							if (fBuilders.contains(PluginProject.MANIFEST_BUILDER_ID)
									&& hasBuilder(commands, PluginProject.MANIFEST_BUILDER_ID)) {
								projectToBuild.build(IncrementalProjectBuilder.FULL_BUILD, PluginProject.MANIFEST_BUILDER_ID,
										null, iterationMonitor.split(1));
							}
							if (fBuilders.contains(PluginProject.SCHEMA_BUILDER_ID)
									&& hasBuilder(commands, PluginProject.SCHEMA_BUILDER_ID)) {
								projectToBuild.build(IncrementalProjectBuilder.FULL_BUILD, PluginProject.SCHEMA_BUILDER_ID, null,
										iterationMonitor.split(1));
							}
						} else if (FeatureProject.isFeatureProject(projectToBuild)
								&& hasBuilder(commands, FeatureProject.BUILDER_ID)) {
							if (fBuilders.contains(FeatureProject.BUILDER_ID)) {
								projectToBuild.build(IncrementalProjectBuilder.FULL_BUILD, FeatureProject.BUILDER_ID, null,
										iterationMonitor.split(2));
							}
						}
					}
				} catch (CoreException e) {
					return e.getStatus();
				} catch (OperationCanceledException e) {
					return Status.CANCEL_STATUS;
				}
				return Status.OK_STATUS;
			}

			private boolean hasBuilder(ICommand[] commands, String string) {
				if(commands == null)
					return false;
				if(commands.length == 0)
					return false;
				for (ICommand iCommand : commands) {
					if (iCommand.getBuilderName().equals(string))
						return true;
				}
				return false;
			}
		};
		buildJob.setRule(ResourcesPlugin.getWorkspace().getRuleFactory().buildRule());
		buildJob.setUser(true);
		buildJob.schedule();
	}

	public void selectOption(String key, String qualifier) {
		Key[] allKeys = fgAllKeys;
		for (int i = 0; i < allKeys.length; i++) {
			Key curr = allKeys[i];
			if (curr.getName().equals(key) && curr.getQualifier().equals(qualifier)) {
				selectOption(curr);
			}
		}

	}

	protected ExpandableComposite getParentExpandableComposite(Control control) {
		Control parent = control.getParent();
		while (!(parent instanceof ExpandableComposite) && parent != null) {
			parent = parent.getParent();
		}
		if (parent instanceof ExpandableComposite) {
			return (ExpandableComposite) parent;
		}
		return null;
	}
	private void selectOption(Key key) {
		int tabId = findTab(key);
		if (tabId == -1)
			return;
		if (fTabFolder != null)
			fTabFolder.setSelection(tabId);
		HashSet<Control> controls = fControlMap.get(Integer.valueOf(tabId));// 0 is tab
		Control curr = null;
		boolean found = false;
		for (Control con : controls) {
			curr = con;
			ControlData data = (ControlData) con.getData();
			if (key.equals(data.getKey())) {
				found = true;
				break;
			}

		}
		if (found) {
			ExpandableComposite expandable = getParentExpandableComposite(curr);
			if (expandable != null) {
				HashSet<Control> controls2 = fControlMap.get(Integer.valueOf(tabId));
				//collapse other expandable composites
				for (Control con : controls2) {
					ExpandableComposite expandableOthers = getParentExpandableComposite(con);
					if (expandableOthers != null)
						expandableOthers.setExpanded(false);

				}
				expandable.setExpanded(true);
			}
			curr.setFocus();
			if (Util.isMac()) {
				Label labelControl = fComboLabelMap.get(curr);
				if (labelControl != null && curr instanceof Combo) {
					highlight(curr.getParent(), labelControl, (Combo) curr, ConfigurationBlock.HIGHLIGHT_FOCUS);
				}
			}
		}
	}

	private int findTab(Key key) {
		int tabId = -1;
		for (int i = 0; i < 3; i++) {
			HashSet<Control> controls = fControlMap.get(Integer.valueOf(i));
			if (controls == null)
				continue;
			for (Control con : controls) {
				ControlData data = (ControlData) con.getData();
				if (key.equals(data.getKey())) {
					tabId = i;
					break;
				}
			}
		}
		return tabId;
	}
}
