/*******************************************************************************
 * Copyright (c) 2000, 2021 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
 *******************************************************************************/
package org.eclipse.jdt.internal.ui.preferences.formatter;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Scanner;
import java.util.function.Consumer;
import java.util.function.Predicate;

import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.SashForm;
import org.eclipse.swt.events.FocusAdapter;
import org.eclipse.swt.events.FocusEvent;
import org.eclipse.swt.events.FocusListener;
import org.eclipse.swt.events.MouseAdapter;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.FontData;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.layout.RowLayout;
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.Display;
import org.eclipse.swt.widgets.FileDialog;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.MenuItem;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Spinner;
import org.eclipse.swt.widgets.Text;
import org.eclipse.swt.widgets.ToolBar;
import org.eclipse.swt.widgets.ToolItem;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.content.IContentType;

import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.jface.dialogs.IDialogSettings;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.dialogs.StatusDialog;
import org.eclipse.jface.layout.PixelConverter;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.resource.JFaceResources;

import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.forms.widgets.ExpandableComposite;
import org.eclipse.ui.forms.widgets.Twistie;

import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.formatter.CodeFormatter;
import org.eclipse.jdt.core.formatter.DefaultCodeFormatterConstants;

import org.eclipse.jdt.internal.core.manipulation.util.BasicElementLabels;
import org.eclipse.jdt.internal.corext.util.Messages;

import org.eclipse.jdt.ui.JavaUI;

import org.eclipse.jdt.internal.ui.JavaPlugin;
import org.eclipse.jdt.internal.ui.JavaPluginImages;
import org.eclipse.jdt.internal.ui.dialogs.StatusInfo;
import org.eclipse.jdt.internal.ui.preferences.FilteredPreferenceTree;
import org.eclipse.jdt.internal.ui.preferences.FilteredPreferenceTree.PreferenceTreeNode;
import org.eclipse.jdt.internal.ui.preferences.PreferenceHighlight;
import org.eclipse.jdt.internal.ui.preferences.PreferencesMessages;
import org.eclipse.jdt.internal.ui.preferences.formatter.IModifyDialogTabPage.IModificationListener;
import org.eclipse.jdt.internal.ui.preferences.formatter.ProfileManager.CustomProfile;
import org.eclipse.jdt.internal.ui.preferences.formatter.ProfileManager.Profile;
import org.eclipse.jdt.internal.ui.util.ExceptionHandler;
import org.eclipse.jdt.internal.ui.util.SWTUtil;
import org.eclipse.jdt.internal.ui.wizards.dialogfields.LayoutUtil;
import org.eclipse.jdt.internal.ui.wizards.dialogfields.StringDialogField;

public abstract class ModifyDialog extends StatusDialog implements IModificationListener {

	protected static class Section extends PreferenceTreeNode<ExpandableComposite> {
		protected final Composite fInnerComposite;

		private final String fPreviewKey;

		private final Control fToggle;

		public static Section create(Composite parentComposite, String label, String previewKey) {
			final ExpandableComposite excomposite= new FormatterPreferenceSectionComposite(parentComposite, SWT.NONE, ExpandableComposite.TWISTIE | ExpandableComposite.CLIENT_INDENT);
			excomposite.clientVerticalSpacing= 0;
			excomposite.setText(label);
			excomposite.setExpanded(false);
			excomposite.setFont(JFaceResources.getFontRegistry().getBold(JFaceResources.DIALOG_FONT));
			excomposite.setLayoutData(new GridData(GridData.FILL, GridData.FILL, true, false, GRID_COLUMNS, 1));

			Composite inner= new Composite(excomposite, SWT.NONE);
			inner.setFont(parentComposite.getFont());
			GridLayout layout= new GridLayout(GRID_COLUMNS, false);
			layout.marginWidth= 0;
			layout.marginLeft= 5;
			inner.setLayout(layout);
			excomposite.setClient(inner);
			return new Section(label, excomposite, inner, previewKey);
		}

		private Section(String label, ExpandableComposite control, Composite innerComposite, String previewKey) {
			super(label, control, false);
			fInnerComposite= innerComposite;
			fPreviewKey= previewKey;

			Control toggle= null;
			for (Control child : control.getChildren()) {
				if (child instanceof Twistie)
					toggle= child;
			}
			assert toggle != null;
			fToggle= toggle;

			// clicking inside section should bring it to focus
			final MouseAdapter mouseListener= new MouseAdapter() {
				@Override
				public void mouseDown(MouseEvent e) {
					if (e.getSource() == fControl && e.x < fControl.getClient().getLocation().x)
						return;
					if (e.getSource() == fControl.getClient() && e.x < getChildren().get(0).getControl().getLocation().x)
						return;
					fToggle.setFocus();
				}
			};
			control.addMouseListener(mouseListener);
			control.getClient().addMouseListener(mouseListener);
		}

		public String getKey() {
			return fPreviewKey;
		}

		public Control getToggle() {
			return fToggle;
		}

		public Preference<?> findChildPreference(String key) {
			for (PreferenceTreeNode<?> child : getChildren()) {
				if (child instanceof Preference && key.equals(((Preference<?>) child).getKey()))
					return (Preference<?>) child;
			}
			throw new IllegalArgumentException(key);
		}
	}

	protected abstract static class ModifyAll<T extends Control> {
		private final Section fSection;

		private final Composite fModifyAllPanel;

		protected final T fControl;

		public ModifyAll(Section section, Images images) {
			fSection= section;

			final ExpandableComposite excomposite= section.getControl();
			Composite modifyAllParent= new Composite(excomposite, SWT.NONE);
			excomposite.setTextClient(modifyAllParent);
			RowLayout rowLayout= new RowLayout();
			rowLayout.marginTop= rowLayout.marginBottom= rowLayout.marginRight= 0;
			rowLayout.center= true;
			rowLayout.spacing= 1;
			modifyAllParent.setLayout(rowLayout);

			fModifyAllPanel= new Composite(modifyAllParent, SWT.NONE);
			fModifyAllPanel.setLayout(rowLayout);
			fModifyAllPanel.setVisible(false);
			fControl= createControl(fModifyAllPanel);

			ToolItem item= createToolItem(modifyAllParent, images);
			item.addSelectionListener(new SelectionAdapter() {
				@Override
				public void widgetSelected(SelectionEvent e) {
					fModifyAllPanel.setVisible(!fModifyAllPanel.isVisible());
					if (fModifyAllPanel.isVisible()) {
						excomposite.setFocus(); // force preview update
						prepareControl();
						fControl.requestLayout();
						fControl.setFocus();
					}
				}
			});
			fControl.addFocusListener(new FocusAdapter() {
				@Override
				public void focusLost(FocusEvent e) {
					fModifyAllPanel.setVisible(false);
				}
			});
		}

		protected abstract T createControl(Composite parent);

		protected abstract void prepareControl();

		protected <P extends Preference<T>> List<P> findPreferences(Class<P> prefClass) {
			List<P> result= new ArrayList<>();
			ArrayDeque<PreferenceTreeNode<?>> queue= new ArrayDeque<>();
			queue.add(fSection);
			while (!queue.isEmpty()) {
				PreferenceTreeNode<?> node= queue.removeFirst();
				if (!node.isVisible())
					continue;
				if (prefClass.isInstance(node))
					result.add(prefClass.cast(node));
				queue.addAll(node.getChildren());
			}
			return result;
		}

		static ToolItem createToolItem(Composite parent, Images images) {
			ToolBar toolBar= new ToolBar(parent, SWT.FLAT);
			ToolItem item= new ToolItem(toolBar, SWT.PUSH);
			item.setToolTipText(FormatterMessages.ModifyDialog_modifyAll_tooltip);
			item.setImage(images.get(JavaPluginImages.DESC_ELCL_MODIFYALL));
			return item;
		}
	}

	protected abstract static class Preference<T extends Control> extends PreferenceTreeNode<T> {

		private Map<String, String> fWorkingValues;

		private Predicate<String> fValueValidator;
		protected Runnable fValueChangeListener;

		private String fKey;

		private Map<PreferenceTreeNode<?>, Predicate<String>> fDependants= new HashMap<>();

		protected PreferenceHighlight fHighlight;

		public Preference(T control, String label, String key, ValueMatcher<T> valueMatcher) {
			super(label, control, true, valueMatcher);
			assert key != null;
			fKey= key;
		}

		protected void init(Map<String, String> workingValues, Predicate<String> valueValidator, Runnable valueChangeListener) {
			fWorkingValues= workingValues;
			fValueValidator= valueValidator;
			fValueChangeListener= valueChangeListener;
			updateWidget();

			fControl.addFocusListener(new FocusAdapter() {
				@Override
				public void focusLost(FocusEvent e) {
					fValueValidator.test(null);
				}
			});
		}

		protected Map<String, String> getPreferences() {
			return fWorkingValues;
		}

		public void setValueValidator(Predicate<String> valueValidator) {
			fValueValidator = valueValidator;
		}

		/**
		 * Set the key which is used to store the value.
		 *
		 * @param key New value
		 */
		public final void setKey(String key) {
			assert key != null;
			fKey= key;
			updateWidget();
		}

		/**
		 * @return Gets the currently used key which is used to store the value.
		 */
		public final String getKey() {
			return fKey;
		}

		public void addDependant(PreferenceTreeNode<?> dependentChild, Predicate<String> dependencyChecker) {
			fDependants.put(dependentChild, dependencyChecker);
			if (fWorkingValues != null)
				updateDependants();
		}

		/**
		 * To be implemented in subclasses. Update the SWT widgets when the state of this object has changed
		 * (enabled, key, ...).
		 */
		protected abstract void updateWidget();

		protected abstract String getValue();

		protected void updateValue() {
			String newValue= getValue();
			String oldValue= fWorkingValues.put(fKey, newValue);
			if (fValueValidator.test(newValue)) {
				updateDependants();
				fValueChangeListener.run();
			} else {
				fWorkingValues.put(fKey, oldValue);
			}
		}

		private void updateDependants() {
			for (Entry<PreferenceTreeNode<?>, Predicate<String>> entry : fDependants.entrySet()) {
				PreferenceTreeNode<?> dependant= entry.getKey();
				Predicate<String> dependencyChecker= entry.getValue();
				dependant.setEnabled(dependencyChecker.test(fWorkingValues.get(fKey)));
			}
		}

		@Override
		public void setEnabled(boolean enabled) {
			super.setEnabled(enabled);
			if (enabled)
				updateDependants();
		}

		protected void addLabel(String label, boolean highlight, int indent) {
			if (label == null)
				return;
			Label labelControl= createLabel(GRID_COLUMNS - 2, fControl.getParent(), label, indent);
			labelControl.moveAbove(fControl);
			if (highlight)
				fHighlight= PreferenceHighlight.addHighlight(labelControl, fControl, false);
			addChild(new PreferenceTreeNode<>(label, labelControl, true));
		}
	}

	/**
	 * Wrapper around a checkbox with a label.
	 */
	protected static final class CheckboxPreference extends Preference<Button> {
		/**
		 * Constant array for boolean false/true selection.
		 */
		public static final String[] FALSE_TRUE= { DefaultCodeFormatterConstants.FALSE, DefaultCodeFormatterConstants.TRUE };

		/**
		 * Constant array for boolean true/false selection.
		 */
		public static final String[] TRUE_FALSE= { DefaultCodeFormatterConstants.TRUE, DefaultCodeFormatterConstants.FALSE };

		/**
		 * Constant array for insert / not_insert.
		 */
		public static final String[] DO_NOT_INSERT_INSERT= { JavaCore.DO_NOT_INSERT, JavaCore.INSERT };

		private final String[] fValues;

		/**
		 * Create a new CheckboxPreference.
		 *
		 * @param parentComposite The composite on which the SWT widgets are added.
		 * @param indent how many levels of indentation to apply.
		 * @param label The label text for this Preference.
		 * @param key The key to store the values.
		 * @param values An array of two elements indicating the values to store when unchecked/checked.
		 * @return a newly created CheckboxPreference.
		 */
		public static CheckboxPreference create(Composite parentComposite, int indent, String label, String key, String[] values) {
			Button checkbox= new Button(parentComposite, SWT.CHECK);
			checkbox.setText(label);
			checkbox.setLayoutData(createGridData(GRID_COLUMNS - 1, GridData.FILL_HORIZONTAL, SWT.DEFAULT, indent));
			checkbox.setFont(parentComposite.getFont());
			return new CheckboxPreference(checkbox, label, key, values);
		}

		private CheckboxPreference(Button button, String label, String key, String[] values) {
			super(button, label, key, FilteredPreferenceTree.CHECK_BOX_MATCHER);
			fValues= values;

			button.addSelectionListener(new SelectionAdapter() {
				@Override
				public void widgetSelected(SelectionEvent e) {
					updateValue();
				}
			});
		}

		@Override
		protected String getValue() {
			boolean state= fControl.getSelection();
			return state ? fValues[1] : fValues[0];
		}

		@Override
		protected void updateWidget() {
			boolean checked= fValues[1].equals(getPreferences().get(getKey()));
			fControl.setSelection(checked);
		}

		public static ModifyAll<Button> addModifyAll(Section section, Images images) {
			return new ModifyAll<>(section, images) {
				@Override
				protected Button createControl(Composite parent) {
					Button checkBox= new Button(parent, SWT.CHECK);
					checkBox.setFont(parent.getFont());
					checkBox.addSelectionListener(new SelectionAdapter() {
						@Override
						public void widgetSelected(SelectionEvent e) {
							boolean selected= fControl.getSelection();
							for (CheckboxPreference pref : findPreferences(CheckboxPreference.class)) {
								pref.getControl().setSelection(selected);
								pref.updateValue();
							}
							prepareControl();
						}
					});
					return checkBox;
				}

				@Override
				protected void prepareControl() {
					List<CheckboxPreference> preferences= findPreferences(CheckboxPreference.class);
					int count= 0;
					for (CheckboxPreference pref : preferences) {
						if (pref.getControl().getSelection())
							count++;
					}
					fControl.setSelection(count == preferences.size());
					fControl.setText(Messages.format(FormatterMessages.ModifyDialog_modifyAll_checkBox, new Object[] { count, preferences.size() }));
					fControl.requestLayout();
				}
			};
		}
	}

	/**
	 * Wrapper around a combo box and a label.
	 */
	protected static final class ComboPreference extends Preference<Combo> {
		private final List<String> fValues;

		/**
		 * @param parentComposite The composite on which the SWT widgets are added.
		 * @param indent how many levels of indentation to apply.
		 * @param label The label text for this Preference or {@code null} if none.
		 * @param key The key to store the values.
		 * @param values An array of n elements indicating the values to store for each selection.
		 * @param items An array of n elements indicating the text to be written in the combo box.
		 * @param highlight whether highlight arrow should be added.
		 * @return a newly created ComboPreference.
		 */
		public static ComboPreference create(Composite parentComposite, int indent, String label, String key, String[] values, String[] items, boolean highlight) {
			Combo combo= new Combo(parentComposite, SWT.SINGLE | SWT.READ_ONLY);
			combo.setFont(parentComposite.getFont());
			SWTUtil.setDefaultVisibleItemCount(combo);
			combo.setItems(items);
			combo.setLayoutData(createGridData(1, GridData.HORIZONTAL_ALIGN_FILL, combo.computeSize(SWT.DEFAULT, SWT.DEFAULT).x, 0));

			ComboPreference comboPreference= new ComboPreference(combo, label, key, values);
			comboPreference.addLabel(label, highlight, indent);

			return comboPreference;
		}

		private ComboPreference(Combo combo, String label, String key, String[] values) {
			super(combo, label, key, FilteredPreferenceTree.COMBO_VALUE_MATCHER);

			fValues= Arrays.asList(values);

			combo.addSelectionListener(new SelectionAdapter() {
				@Override
				public void widgetSelected(SelectionEvent e) {
					updateValue();
				}
			});
		}

		@Override
		protected void updateWidget() {
			int valueIndex= fValues.indexOf(getPreferences().get(getKey()));
			if (valueIndex == -1) {
				final String message= Messages.format(FormatterMessages.ModifyDialog_ComboPreference_error_invalid_key, new Object[] { getKey(), getPreferences().get(getKey()) });
				JavaPlugin.log(new Status(IStatus.ERROR, JavaPlugin.getPluginId(), IStatus.OK, message, null));
				valueIndex= 0;
			}
			fControl.select(valueIndex);
		}

		@Override
		protected String getValue() {
			return fValues.get(fControl.getSelectionIndex());
		}

		public static ModifyAll<Combo> addModifyAll(Section section, Images images) {
			return new ModifyAll<>(section, images) {
				ArrayList<String> fItems= new ArrayList<>();

				@Override
				protected Combo createControl(Composite parent) {
					Combo combo= new Combo(parent, SWT.SINGLE | SWT.READ_ONLY);
					combo.setFont(parent.getFont());
					SWTUtil.setDefaultVisibleItemCount(combo);
					combo.addSelectionListener(new SelectionAdapter() {
						@Override
						public void widgetSelected(SelectionEvent e) {
							String selected= fItems.get(fControl.getSelectionIndex());
							for (ComboPreference pref : findPreferences(ComboPreference.class)) {
								int index= pref.fControl.indexOf(selected);
								if (index >= 0)
									pref.fControl.select(index);
								pref.updateValue();
							}
							prepareControl();
						}
					});
					return combo;
				}

				@Override
				protected void prepareControl() {
					LinkedHashMap<String, Integer> itemCounts= new LinkedHashMap<>();
					List<ComboPreference> preferences= findPreferences(ComboPreference.class);
					for (ComboPreference pref : preferences) {
						String[] items= pref.getControl().getItems();
						for (String item : items) {
							if (!itemCounts.containsKey(item))
								itemCounts.put(item, 0);
						}
						String selected= items[pref.getControl().getSelectionIndex()];
						itemCounts.put(selected, itemCounts.get(selected) + 1);
					}

					String[] items= new String[itemCounts.size()];
					fItems.clear();
					int i= 0;
					int maxCount= 0;
					int maxCountIndex= 0;
					for (Entry<String, Integer> entry : itemCounts.entrySet()) {
						String item= entry.getKey();
						int count= entry.getValue();
						fItems.add(item);
						if (count > 0) {
							item+= Messages.format(FormatterMessages.ModifyDialog_modifyAll_summary, new Object[] { count, preferences.size() });
						}
						if (count > maxCount) {
							maxCount= count;
							maxCountIndex= i;
						}
						items[i++]= item;
					}
					fControl.setItems(items);
					fControl.select(maxCountIndex);
					fControl.requestLayout();
				}
			};
		}
	}

	/**
	 * Wrapper around a textfied which requests an integer input of a given range.
	 */
	protected static final class NumberPreference extends Preference<Spinner> {

		/**
		 * @param parentComposite The composite on which the SWT widgets are added.
		 * @param indent how many levels of indentation to apply.
		 * @param label The label text for this Preference or {@code null} if none.
		 * @param key The key to store the values.
		 * @param minValue The minimum value which is valid input.
		 * @param maxValue The maximum value which is valid input.
		 * @param highlight whether highlight arrow should be added.
		 * @return a newly created NumberPreference.
		 */
		public static NumberPreference create(Composite parentComposite, int indent, String label, String key, int minValue, int maxValue, boolean highlight) {
			Spinner spinner= createSpinner(parentComposite, minValue, maxValue);
			NumberPreference numberPreference= new NumberPreference(spinner, label, key);
			numberPreference.addLabel(label, highlight, indent);

			return numberPreference;
		}

		static Spinner createSpinner(Composite parentComposite, int minValue, int maxValue) {
			Spinner spinner= new Spinner(parentComposite, SWT.BORDER);
			spinner.setFont(parentComposite.getFont());
			spinner.setMinimum(minValue);
			spinner.setMaximum(maxValue);

			spinner.setLayoutData(createGridData(1, GridData.HORIZONTAL_ALIGN_END, SWT.DEFAULT, 0));
			return spinner;
		}

		private NumberPreference(Spinner spinner, String label, String key) {
			super(spinner, label, key, FilteredPreferenceTree.SPINNER_VALUE_MATCHER);

			spinner.addSelectionListener(new SelectionAdapter() {
				@Override
				public void widgetSelected(SelectionEvent e) {
					updateValue();
				}
			});
		}

		@Override
		protected void updateWidget() {
			try {
				String s= getPreferences().get(getKey());
				int number= Integer.parseInt(s);
				number= Math.max(fControl.getMinimum(), Math.min(fControl.getMaximum(), number));
				fControl.setSelection(number);
			} catch (NumberFormatException x) {
				final String message= Messages.format(FormatterMessages.ModifyDialogTabPage_NumberPreference_error_invalid_key, getKey());
				JavaPlugin.log(new Status(IStatus.ERROR, JavaPlugin.getPluginId(), IStatus.OK, message, null));
				fControl.setSelection(fControl.getMinimum());
			}
		}

		@Override
		protected String getValue() {
			return Integer.toString(fControl.getSelection());
		}

		public static ModifyAll<Spinner> addModifyAll(final int minValue, final int maxValue, Section section, Images images) {
			return new ModifyAll<>(section, images) {
				private Label fLabel;

				@Override
				protected Spinner createControl(Composite parent) {
					GridLayout layout= new GridLayout(2, false);
					layout.marginWidth= layout.marginHeight= 0;
					parent.setLayout(layout);

					fLabel= createLabel(1, parent, "", 0); //$NON-NLS-1$
					Spinner spinner= createSpinner(parent, minValue, maxValue);
					spinner.addSelectionListener(new SelectionAdapter() {
						@Override
						public void widgetSelected(SelectionEvent e) {
							int selected= fControl.getSelection();
							for (NumberPreference pref : findPreferences(NumberPreference.class)) {
								pref.getControl().setSelection(selected);
								pref.updateValue();
							}
							prepareControl();
						}
					});
					return spinner;
				}

				@Override
				protected void prepareControl() {
					int modeValue= 0;
					int modeCount= -1;
					HashMap<Integer, Integer> counts= new HashMap<>();
					List<NumberPreference> preferences= findPreferences(NumberPreference.class);
					for (NumberPreference pref : preferences) {
						int value= pref.getControl().getSelection();
						int count= counts.merge(value, 1, Integer::sum);
						if (count > modeCount) {
							modeValue= value;
							modeCount= count;
						}
					}
					fControl.setSelection(modeValue);
					fLabel.setText(Messages.format(FormatterMessages.ModifyDialog_modifyAll_summary, new Object[] { modeCount, preferences.size() }));
					fLabel.requestLayout();
				}
			};
		}
	}

	/**
	 * Wrapper around a text field which requests a string input.
	 */
	protected static final class StringPreference extends Preference<Text> {

		/**
		 * @param parentComposite The composite on which the SWT widgets are added.
		 * @param indent how many levels of indentation to apply.
		 * @param label the label text for this Preference.
		 * @param key the key to store the values.
		 * @param highlight whether highlight arrow should be added
		 * @return a newly created StringPreference
		 */
		public static StringPreference create(Composite parentComposite, int indent, String label, String key, boolean highlight) {
			Text text= new Text(parentComposite, SWT.SINGLE | SWT.BORDER);
			text.setFont(parentComposite.getFont());
			final int length= 30;
			PixelConverter pixelConverter= new PixelConverter(parentComposite);
			GridData gridData= createGridData(1, GridData.HORIZONTAL_ALIGN_BEGINNING, pixelConverter.convertWidthInCharsToPixels(length), 0);
			gridData.grabExcessHorizontalSpace= true;
			text.setLayoutData(gridData);

			StringPreference stringPreference= new StringPreference(text, label, key);
			stringPreference.addLabel(label, highlight, indent);

			return stringPreference;
		}

		private StringPreference(Text control, String label, String key) {
			super(control, label, key, FilteredPreferenceTree.TEXT_VALUE_MATCHER);

			fControl.addModifyListener(e -> updateValue());

			fControl.addFocusListener(new FocusAdapter() {
				@Override
				public void focusGained(FocusEvent e) {
					fControl.setSelection(0, fControl.getCharCount());
				}
			});
		}

		@Override
		protected void updateWidget() {
			String value= getPreferences().get(getKey());
			fControl.setText(value);
		}

		@Override
		protected String getValue() {
			return fControl.getText();
		}
	}

	@FunctionalInterface
	public interface PreferenceBuilder {
		Preference<?> buildPreference(Section parent, String label, String key);
	}

	protected class ProfilePreferenceTree extends FilteredPreferenceTree {

		/**
		 * Helper class for easy, call-chain based building of subtrees that contain only Sections and one
		 * type of preference.
		 *
		 * @param <T> Type of preference or section built
		 */
		public abstract class SimpleTreeBuilder<T extends PreferenceTreeNode<?>> {
			protected final String fLabel, fKey;

			protected boolean fGap;

			protected Consumer<T> fCustomizer;

			protected SimpleTreeBuilder(String label, String key, Consumer<T> customizer) {
				fLabel= label;
				fKey= key;
				fCustomizer= customizer;
			}

			protected abstract T build(Section parent, PreferenceBuilder prefBuilder);
		}

		public class SectionBuilder extends SimpleTreeBuilder<Section> {
			private ArrayList<SimpleTreeBuilder<?>> fChildren= new ArrayList<>();

			SectionBuilder(String label, String key, Consumer<Section> customizer) {
				super(label, key, customizer);
			}

			public SectionBuilder node(SimpleTreeBuilder<?> child) {
				fChildren.add(child);
				return this;
			}

			public SectionBuilder pref(String label, String key) {
				return pref(label, key, null);
			}

			public SectionBuilder pref(String label, String key, Consumer<Preference<?>> customizer) {
				return node(new PrefBuilder(label, key, customizer));
			}

			public SectionBuilder gap() {
				fChildren.get(fChildren.size() - 1).fGap= true;
				return this;
			}

			@Override
			public Section build(Section parent, PreferenceBuilder prefBuilder) {
				String key= fKey;
				if (key != null) {
					Section ancestorWithKey= parent;
					while (ancestorWithKey != null && ancestorWithKey.getKey() == null)
						ancestorWithKey= (Section) ancestorWithKey.getParent();
					if (ancestorWithKey != null)
						key= ancestorWithKey.getKey() + key;
				}

				Section section= addSection(parent, fLabel, key);
				for (SimpleTreeBuilder<?> child : fChildren) {
					child.build(section, prefBuilder);
					if (child.fGap)
						addGap(section);
				}
				if (fCustomizer != null)
					fCustomizer.accept(section);
				return section;
			}
		}

		private class PrefBuilder extends SimpleTreeBuilder<Preference<?>> {
			public PrefBuilder(String label, String key, Consumer<Preference<?>> customizer) {
				super(label, key, customizer);
			}

			@Override
			protected Preference<?> build(Section parent, PreferenceBuilder prefBuilder) {
				Preference<?> pref= prefBuilder.buildPreference(parent, fLabel, fKey);
				if (fCustomizer != null)
					fCustomizer.accept(pref);
				return pref;
			}
		}

		private boolean fFilterEmpty;
		private int fRightMargin;

		public ProfilePreferenceTree(Composite parentComposite) {
			super(parentComposite, FormatterMessages.ModifyDialog_filter_label, FormatterMessages.ModifyDialog_filter_hint);

			// calculate rigth margin width
			ToolBar modifyAllToolbar= ModifyAll.createToolItem(parentComposite, fImages).getParent();
			fRightMargin= modifyAllToolbar.computeSize(SWT.DEFAULT, SWT.DEFAULT).x - 12;
			modifyAllToolbar.dispose();
		}

		public Section addSection(Section parent, String label, String previewKey) {
			Section section= Section.create(getParentComposite(parent), label, previewKey);

			ExpandableComposite excomposite= section.getControl();
			getScrolledPageContent().adaptChild(excomposite);

			Menu expandAllMenu= new Menu(excomposite);
			MenuItem expandAllItem= new MenuItem(expandAllMenu, SWT.NONE);
			expandAllItem.setText(PreferencesMessages.FilteredPreferencesTree_expandAll_tooltip);
			expandAllItem.addSelectionListener(new SelectionAdapter() {
				@Override
				public void widgetSelected(SelectionEvent e) {
					setAllExpanded(section, true);
				}
			});
			excomposite.setMenu(expandAllMenu);

			return addChild(parent, section);
		}

		public CheckboxPreference addCheckbox(PreferenceTreeNode<?> parent, String label, String key, String[] values) {
			CheckboxPreference button= CheckboxPreference.create(getParentComposite(parent), getIndent(parent), label, key, values);
			return addChild(parent, button);
		}

		public ComboPreference addComboPref(PreferenceTreeNode<?> parent, String label, String key, String[] values, String[] items) {
			ComboPreference combo= ComboPreference.create(getParentComposite(parent), getIndent(parent), label, key, values, items, true);
			return addChild(parent, combo);
		}

		public NumberPreference addNumberPref(PreferenceTreeNode<?> parent, String label, String key, int minValue, int maxValue) {
			NumberPreference number= NumberPreference.create(getParentComposite(parent), getIndent(parent), label, key, minValue, maxValue, true);
			return addChild(parent, number);
		}

		public StringPreference addStringPreference(PreferenceTreeNode<?> parent, String label, String key) {
			StringPreference stringPreference= StringPreference.create(getParentComposite(parent), getIndent(parent), label, key, true);
			return addChild(parent, stringPreference);
		}

		@Override
		public <T extends PreferenceTreeNode<?>> T addChild(PreferenceTreeNode<?> parent, T node) {
			super.addChild(parent, node);
			fFocusManager.add(node);
			if (node instanceof Preference<?>) {
				Predicate<String> validator = v -> {
					doValidate();
					return true;
				};
				((Preference<?>) node).init(fWorkingValues, validator, ModifyDialog.this::valuesModified);
			}

			if (!(node instanceof Section)) {
				Label margin= new Label(node.getControl().getParent(), SWT.NONE);
				margin.setLayoutData(new GridData(fRightMargin, SWT.DEFAULT));
				node.addChild(new PreferenceTreeNode<>("", margin, true)); //$NON-NLS-1$
			}
			return node;
		}

		public void addGap(PreferenceTreeNode<?> parent) {
			Composite gap= new Composite(getParentComposite(parent), SWT.NONE);
			GridData gd= new GridData(0, 4);
			gd.horizontalSpan= GRID_COLUMNS;
			gap.setLayoutData(gd);
			parent.addChild(new PreferenceTreeNode<>("", gap, true)); //$NON-NLS-1$
		}

		public SectionBuilder builder(String label, String key) {
			return builder(label, key, null);
		}

		public SectionBuilder builder(String label, String key, Consumer<Section> customizer) {
			return new SectionBuilder(label, key, customizer);
		}

		private Composite getParentComposite(PreferenceTreeNode<?> parent) {
			while (parent != null) {
				if (parent instanceof Section)
					return ((Section) parent).fInnerComposite;
				parent= parent.getParent();
			}
			return getScrolledPageContent().getBody();
		}

		protected int getIndent(PreferenceTreeNode<?> parent) {
			int indent= 0;
			while (parent != null) {
				if (parent instanceof Section)
					return indent;
				indent++;
				parent= parent.getParent();
			}
			return indent;
		}

		protected void saveState() {
			try (ByteArrayOutputStream out= new ByteArrayOutputStream()) {
				writeExpansionState(fRoot, out);
				fDialogSettings.put(fKeyPreferenceTreeExpansion, out.toString("UTF-8")); //$NON-NLS-1$
			} catch (IOException e) {
				throw new AssertionError(e);
			}

			int scrollPosition= getScrolledPageContent().getOrigin().y;
			fDialogSettings.put(fKeyPreferenceScrollPosition, scrollPosition);
		}

		private void writeExpansionState(PreferenceTreeNode<?> node, OutputStream output) throws IOException {
			for (PreferenceTreeNode<?> child : node.getChildren()) {
				if (child instanceof Section) {
					output.write(((Section) child).getControl().isExpanded() ? '1' : '0');
					writeExpansionState(child, output);
				}
			}
			output.write('.');
		}

		protected void restoreExpansionState() {
			String treeState= fDialogSettings.get(fKeyPreferenceTreeExpansion);
			if (treeState == null)
				return;
			try (ByteArrayInputStream in= new ByteArrayInputStream(treeState.getBytes("UTF-8"))) { //$NON-NLS-1$
				fScrolledPageContent.setReflow(false);
				readExpansionState(fRoot, in);
				fScrolledPageContent.setReflow(true);
			} catch (IOException e) {
				throw new AssertionError(e);
			}
		}

		private void readExpansionState(PreferenceTreeNode<?> node, InputStream in) throws IOException {
			for (PreferenceTreeNode<?> child : node.getChildren()) {
				if (child instanceof Section) {
					int state= in.read();
					((Section) child).getControl().setExpanded(state == '1');
					readExpansionState(child, in);
					if (state == '.' || state == -1)
						return; // some nodes missing in stored tree
				}
			}
			int c= in.read();
			while (c != '.' && c != -1) { // some extra nodes in stored tree
				readExpansionState(new PreferenceTreeNode<>(null, null, false), in);
				c= in.read();
			}
		}

		protected void restoreScrollPosition() {
			try {
				int scrollPosition= fDialogSettings.getInt(fKeyPreferenceScrollPosition);
				getScrolledPageContent().setOrigin(0, scrollPosition);
			} catch (NumberFormatException e) {
				// ignore invalid/undefined value
			}
		}

		@Override
		public void doFilter(String filterText) {
			fFilterEmpty= filterText.trim().isEmpty();
			super.doFilter(filterText);
		}

		@Override
		protected void updateUI(PreferenceTreeNode<?> node) {
			super.updateUI(node);
			if (node == fRoot && fFilterEmpty)
				restoreExpansionState();
		}

		public void unifySectionTitlesHeights(Section section) {
			List<PreferenceTreeNode<?>> children= section == null ? fRoot.getChildren() : section.getChildren();
			int maxHeightDiff= 0;
			for (PreferenceTreeNode<?> child : children) {
				if (child instanceof Section)
					maxHeightDiff= Math.max(maxHeightDiff, ((Section) child).getControl().getTextClientHeightDifference());
			}
			int nextChildIndent= 0;
			for (PreferenceTreeNode<?> child : children) {
				int indent= nextChildIndent;
				nextChildIndent= 0;
				if (child instanceof Section) {
					Section sectionChild= (Section) child;
					int diff= maxHeightDiff - sectionChild.getControl().getTextClientHeightDifference();
					nextChildIndent= diff / 2;
					indent+= diff - diff / 2;

					unifySectionTitlesHeights(sectionChild);
				}
				GridData gd= (GridData) child.getControl().getLayoutData();
				gd.verticalIndent+= indent;
			}
		}
	}

	public static class Images {
		private Map<ImageDescriptor, Image> imagesMap= new HashMap<>();

		protected Images(Composite rootComposite) {
			rootComposite.addDisposeListener(e -> {
				for (Image image : imagesMap.values())
					image.dispose();
				imagesMap.clear();
			});
		}

		public Image get(ImageDescriptor descriptor) {
			return imagesMap.computeIfAbsent(descriptor, ImageDescriptor::createImage);
		}
	}

	/**
	 * The default focus manager. It knows all widgets which can have the focus and listens for
	 * focusGained events, on which it stores the index of the current focus holder. When the dialog
	 * is restarted, <code>restoreFocus()</code> sets the focus to the last control which had it.
	 *
	 * Focus manager also makes sure that proper preview is displayed for currently focused
	 * preference.
	 *
	 * The standard Preference objects are managed by this focus manager if they are created using
	 * the respective factory methods. Other SWT widgets can be added in subclasses when they are
	 * created.
	 */
	protected final class FocusManager implements FocusListener {
		private HashMap<Control, PreferenceTreeNode<?>> fControl2node= new HashMap<>();
		private final Map<Control, Integer> fItemMap= new HashMap<>();
		private final List<Control> fItemList= new ArrayList<>();
		private int fIndex= 0;
		private PreferenceTreeNode<?> fCurrentlyFocused;

		public void add(PreferenceTreeNode<?> node) {
			Control control= node.getControl();
			if (node instanceof Section) {
				// workaround: can't add focus listener directly to ExpadableComposite
				control= ((Section) node).getToggle();
			}
			fControl2node.put(control, node);
			add(control);
		}

		private void add(Control control) {
			control.addFocusListener(this);
			fItemList.add(fIndex, control);
			fItemMap.put(control, fIndex++);
		}

		@Override
		public void focusGained(FocusEvent e) {
			PreferenceTreeNode<?> focusNode= fControl2node.get(e.getSource());
			if (focusNode != null && focusNode != fCurrentlyFocused) {
				highlightCurrent(false);
				fCurrentlyFocused= focusNode;
				highlightCurrent(true);
				updatePreviewCode();
			}

			fDialogSettings.put(fKeyLastFocusIndex, fItemMap.get(e.widget));
		}

		private void highlightCurrent(boolean focus) {
			if (fCurrentlyFocused instanceof Preference) {
				Preference<?> pref= (Preference<?>) fCurrentlyFocused;
				if (pref.fHighlight != null) {
					pref.fHighlight.setFocus(focus);
				} else {
					changeFont(pref.getControl(), focus);
				}
			} else if (fCurrentlyFocused instanceof Section) {
				changeFont(((Section) fCurrentlyFocused).getControl(), focus);
			}
		}

		private void changeFont(Control control, boolean italic) {
			Display.getCurrent().asyncExec(() -> {
				FontData fd= control.getFont().getFontData()[0];
				int style= italic ? (fd.getStyle() | SWT.ITALIC) : (fd.getStyle() & ~SWT.ITALIC);
				FontData fontData= new FontData(fd.getName(), fd.getHeight(), style);
				Font font= new Font(control.getDisplay(), fontData);
				control.addDisposeListener(e -> font.dispose());
				control.setFont(font);
				if (control instanceof Composite)
					((Composite) control).layout();
			});
		}

		@Override
		public void focusLost(FocusEvent e) {
			doValidate();
		}

		public void restoreFocus() {
			try {
				int index= fDialogSettings.getInt(fKeyLastFocusIndex);
				if (index >= 0 && index < fItemList.size()) {
					fItemList.get(index).forceFocus();
				}
			} catch (NumberFormatException ex) {
				// this is the first time
				updatePreviewCode();
			}
		}
	}

	protected static final int GRID_COLUMNS= 4;

	/* The keys to retrieve the preferred area from the dialog settings */
	private static final String DS_KEY_PREFERRED_WIDTH= "modify_dialog.preferred_width"; //$NON-NLS-1$
	private static final String DS_KEY_PREFERRED_HEIGHT= "modify_dialog.preferred_height"; //$NON-NLS-1$
	private static final String DS_KEY_PREFERRED_X= "modify_dialog.preferred_x"; //$NON-NLS-1$
	private static final String DS_KEY_PREFERRED_Y= "modify_dialog.preferred_y"; //$NON-NLS-1$
	private static final String DS_KEY_SASH_FORM_LEFT_WIDTH= "modify_dialog.sash_form_left_width"; //$NON-NLS-1$
	private static final String DS_KEY_SASH_FORM_RIGHT_WIDTH= "modify_dialog.sash_form_rigth_width"; //$NON-NLS-1$

	private static final String DS_KEY_PREFERENCE_TREE_EXPANSION= ".preference_tree_expansion"; //$NON-NLS-1$
	private static final String DS_KEY_PREFERENCE_SCROLL_POSITION= ".preference_scroll_position"; //$NON-NLS-1$
	private static final String DS_KEY_LAST_FOCUS_INDEX= ".last_focus_index"; //$NON-NLS-1$

	private static final int APPLAY_BUTTON_ID= IDialogConstants.CLIENT_ID;
	private static final int SAVE_BUTTON_ID= IDialogConstants.CLIENT_ID + 1;

	protected final Map<String, String> fWorkingValues;
	protected final IDialogSettings fDialogSettings;
	protected final boolean fNewProfile;

	protected final String fKeyPreferredWidth;
	protected final String fKeyPreferredHight;
	private final String fKeyPreferredX;
	private final String fKeyPreferredY;
	private final String fKeySashFormLeftWidth;
	private final String fKeySashFormRightWidth;
	protected final String fKeyPreferenceTreeExpansion;
	protected final String fKeyPreferenceScrollPosition;
	protected final String fKeyLastFocusIndex;
	private final String fLastSaveLoadPathKey;
	private final ProfileStore fProfileStore;
	private Profile fProfile;
	private final ProfileManager fProfileManager;
	private Button fApplyButton;
	private Button fSaveButton;
	private StringDialogField fProfileNameField;

	private SashForm fSashForm;
	protected JavaPreview fPreview;
	protected ProfilePreferenceTree fTree;
	protected final Images fImages;
	protected final FocusManager fFocusManager= new FocusManager();
	private String fPreviewSources= ""; //$NON-NLS-1$
	private int fCurrentPreviewType;

	public ModifyDialog(Shell parentShell, Profile profile, ProfileManager profileManager, ProfileStore profileStore, boolean newProfile, String dialogPreferencesKey, String lastSavePathKey) {
		super(parentShell);

		fProfileStore= profileStore;
		fLastSaveLoadPathKey= lastSavePathKey;

		fKeyPreferredWidth= JavaUI.ID_PLUGIN + dialogPreferencesKey + DS_KEY_PREFERRED_WIDTH;
		fKeyPreferredHight= JavaUI.ID_PLUGIN + dialogPreferencesKey + DS_KEY_PREFERRED_HEIGHT;
		fKeyPreferredX= JavaUI.ID_PLUGIN + dialogPreferencesKey + DS_KEY_PREFERRED_X;
		fKeyPreferredY= JavaUI.ID_PLUGIN + dialogPreferencesKey + DS_KEY_PREFERRED_Y;
		fKeySashFormLeftWidth= JavaUI.ID_PLUGIN + dialogPreferencesKey + DS_KEY_SASH_FORM_LEFT_WIDTH;
		fKeySashFormRightWidth= JavaUI.ID_PLUGIN + dialogPreferencesKey + DS_KEY_SASH_FORM_RIGHT_WIDTH;
		fKeyPreferenceTreeExpansion= JavaUI.ID_PLUGIN + dialogPreferencesKey + DS_KEY_PREFERENCE_TREE_EXPANSION;
		fKeyPreferenceScrollPosition= JavaUI.ID_PLUGIN + dialogPreferencesKey + DS_KEY_PREFERENCE_SCROLL_POSITION;
		fKeyLastFocusIndex= JavaUI.ID_PLUGIN + dialogPreferencesKey + DS_KEY_LAST_FOCUS_INDEX;

		fProfileManager= profileManager;
		fNewProfile= newProfile;

		fProfile= profile;
		setTitle(Messages.format(FormatterMessages.ModifyDialog_dialog_title, profile.getName()));
		fWorkingValues= new HashMap<>(fProfile.getSettings());
		setStatusLineAboveButtons(false);
		fDialogSettings= JavaPlugin.getDefault().getDialogSettings();

		fImages= new Images(parentShell);
	}

	/*
	 * @see org.eclipse.jface.dialogs.Dialog#isResizable()
	 * @since 3.4
	 */
	@Override
	protected boolean isResizable() {
		return true;
	}

	@Override
	protected Control createDialogArea(Composite parent) {

		final Composite composite= (Composite) super.createDialogArea(parent);
		createNameArea(composite);
		createMainArea(composite);

		doValidate();

		PlatformUI.getWorkbench().getHelpSystem().setHelp(composite, getHelpContextId());

		fFocusManager.restoreFocus();

		return composite;
	}

	protected void createMainArea(Composite parent) {
		fSashForm= new SashForm(parent, SWT.HORIZONTAL);
		fSashForm.setFont(parent.getFont());
		fSashForm.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));

		createPreferenceTree(fSashForm);
		fTree.unifySectionTitlesHeights(null);
		fTree.restoreExpansionState();
		createPreviewPane(fSashForm);

		try {
			fSashForm.setWeights(new int[] { fDialogSettings.getInt(fKeySashFormLeftWidth), fDialogSettings.getInt(fKeySashFormRightWidth) });
		} catch (NumberFormatException e) {
			Control[] children= fSashForm.getChildren();
			int treeWidth= children[0].computeSize(SWT.DEFAULT, SWT.DEFAULT).x;
			int previewWidth= children[1].computeSize(SWT.DEFAULT, SWT.DEFAULT).x;
			fSashForm.setWeights(new int[] { treeWidth, previewWidth });
		}
	}

	protected void createPreferenceTree(Composite parent) {
		Composite mainComp= new Composite(parent, SWT.NONE);
		mainComp.setFont(parent.getFont());
		mainComp.setLayout(new GridLayout(2, false));

		fTree= new ProfilePreferenceTree(mainComp);
		GridLayout layout= new GridLayout();
		layout.verticalSpacing= 0;
		layout.marginLeft= layout.marginWidth - 1;
		layout.marginWidth= 1;
		fTree.getScrolledPageContent().getBody().setLayout(layout);
		fTree.setConcatAncestorLabels(true);
		fTree.setExpectMultiWordValueMatch(true);

		// restoring scroll position must wait until layout is complete
		Display.getCurrent().asyncExec(fTree::restoreScrollPosition);
	}

	private void createNameArea(final Composite parent) {
		Composite nameComposite= new Composite(parent, SWT.NONE);
		nameComposite.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false));
		nameComposite.setLayout(new GridLayout(3, false));
		nameComposite.setFont(parent.getFont());

		fProfileNameField= new StringDialogField();
		fProfileNameField.setLabelText(FormatterMessages.ModifyDialog_ProfileName_Label);
		fProfileNameField.setText(fProfile.getName());
		fProfileNameField.getLabelControl(nameComposite).setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false));
		fProfileNameField.getTextControl(nameComposite).setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));
		fProfileNameField.setDialogFieldListener(field -> doValidate());

		fSaveButton= createButton(nameComposite, SAVE_BUTTON_ID, FormatterMessages.ModifyDialog_Export_Button, false);
	}

	/**
	 * Returns the context ID for the Help system
	 *
	 * @return the string used as ID for the Help context
	 * @since 3.5
	 */
	protected abstract String getHelpContextId();

	@Override
	public void updateStatus(IStatus status) {
		if (status == null) {
			doValidate();
		} else {
			super.updateStatus(status);
		}
	}

	@Override
	protected Point getInitialLocation(Point initialSize) {
		try {
			return new Point(fDialogSettings.getInt(fKeyPreferredX), fDialogSettings.getInt(fKeyPreferredY));
		} catch (NumberFormatException ex) {
			return super.getInitialLocation(initialSize);
		}
	}

	@Override
	public boolean close() {
		final Rectangle shell= getShell().getBounds();
		fDialogSettings.put(fKeyPreferredWidth, shell.width);
		fDialogSettings.put(fKeyPreferredHight, shell.height);
		fDialogSettings.put(fKeyPreferredX, shell.x);
		fDialogSettings.put(fKeyPreferredY, shell.y);
		if (fSashForm != null) {
			Control[] children= fSashForm.getChildren();
			fDialogSettings.put(fKeySashFormLeftWidth, children[0].getSize().x);
			fDialogSettings.put(fKeySashFormRightWidth, children[1].getSize().x);
		}

		if (fTree != null)
			fTree.saveState();

		return super.close();
	}

	@Override
	protected void okPressed() {
		applyPressed();
		super.okPressed();
	}

	@Override
	protected void buttonPressed(int buttonId) {
		if (buttonId == APPLAY_BUTTON_ID) {
			applyPressed();
			setTitle(Messages.format(FormatterMessages.ModifyDialog_dialog_title, fProfile.getName()));
		} else if (buttonId == SAVE_BUTTON_ID) {
			saveButtonPressed();
		} else {
			super.buttonPressed(buttonId);
		}
	}

	private void applyPressed() {
		if (!fProfile.getName().equals(fProfileNameField.getText())) {
			fProfile= fProfile.rename(fProfileNameField.getText(), fProfileManager);
		}
		fProfile.setSettings(new HashMap<>(fWorkingValues));
		fProfileManager.setSelected(fProfile);
		doValidate();
	}

	private void saveButtonPressed() {
		Profile selected= new CustomProfile(fProfileNameField.getText(), new HashMap<>(fWorkingValues), fProfile.getVersion(), fProfileManager.getProfileVersioner().getProfileKind());

		final FileDialog dialog= new FileDialog(getShell(), SWT.SAVE | SWT.SHEET);
		dialog.setText(FormatterMessages.CodingStyleConfigurationBlock_save_profile_dialog_title);
		dialog.setFilterExtensions(new String [] {"*.xml"}); //$NON-NLS-1$

		final String lastPath= JavaPlugin.getDefault().getDialogSettings().get(fLastSaveLoadPathKey + ".savepath"); //$NON-NLS-1$
		if (lastPath != null) {
			dialog.setFilterPath(lastPath);
		}
		final String path= dialog.open();
		if (path == null)
			return;

		JavaPlugin.getDefault().getDialogSettings().put(fLastSaveLoadPathKey + ".savepath", dialog.getFilterPath()); //$NON-NLS-1$

		final File file= new File(path);
		if (file.exists() && !MessageDialog.openQuestion(getShell(), FormatterMessages.CodingStyleConfigurationBlock_save_profile_overwrite_title, Messages.format(FormatterMessages.CodingStyleConfigurationBlock_save_profile_overwrite_message, BasicElementLabels.getPathLabel(file)))) {
			return;
		}
		String encoding= ProfileStore.ENCODING;
		final IContentType type= Platform.getContentTypeManager().getContentType("org.eclipse.core.runtime.xml"); //$NON-NLS-1$
		if (type != null)
			encoding= type.getDefaultCharset();
		final Collection<Profile> profiles= new ArrayList<>();
		profiles.add(selected);
		try {
			fProfileStore.writeProfilesToFile(profiles, file, encoding);
		} catch (CoreException e) {
			final String title= FormatterMessages.CodingStyleConfigurationBlock_save_profile_error_title;
			final String message= FormatterMessages.CodingStyleConfigurationBlock_save_profile_error_message;
			ExceptionHandler.handle(e, getShell(), title, message);
		}
	}

	@Override
	protected void createButtonsForButtonBar(Composite parent) {
		fApplyButton= createButton(parent, APPLAY_BUTTON_ID, FormatterMessages.ModifyDialog_apply_button, false);
		fApplyButton.setEnabled(false);

		GridLayout layout= (GridLayout) parent.getLayout();
		layout.numColumns++;
		layout.makeColumnsEqualWidth= false;
		Label label= new Label(parent, SWT.NONE);
		GridData data= new GridData();
		data.widthHint= layout.horizontalSpacing;
		label.setLayoutData(data);
		super.createButtonsForButtonBar(parent);
	}

	@Override
	public void valuesModified() {
		doValidate();
		if (fPreview != null)
			fPreview.update();
	}

	@Override
	protected void updateButtonsEnableState(IStatus status) {
		super.updateButtonsEnableState(status);
		if (fApplyButton != null && !fApplyButton.isDisposed()) {
			fApplyButton.setEnabled(hasChanges() && !status.matches(IStatus.ERROR));
		}
		if (fSaveButton != null && !fSaveButton.isDisposed()) {
			fSaveButton.setEnabled(!validateProfileName().matches(IStatus.ERROR));
		}
	}

	private void doValidate() {
		String name= fProfileNameField.getText().trim();
		if (name.equals(fProfile.getName()) && fProfile.hasEqualSettings(fWorkingValues, fWorkingValues.keySet())) {
			updateStatus(StatusInfo.OK_STATUS);
			return;
		}

		IStatus status= validateProfileName();
		if (status.matches(IStatus.ERROR)) {
			updateStatus(status);
			return;
		}

		if (!name.equals(fProfile.getName()) && fProfileManager.containsName(name)) {
			updateStatus(new Status(IStatus.ERROR, JavaUI.ID_PLUGIN, FormatterMessages.ModifyDialog_Duplicate_Status));
			return;
		}

		if (fProfile.isBuiltInProfile() || fProfile.isSharedProfile()) {
			updateStatus(new Status(IStatus.INFO, JavaUI.ID_PLUGIN, FormatterMessages.ModifyDialog_NewCreated_Status));
			return;
		}

		updateStatus(StatusInfo.OK_STATUS);
	}

	private IStatus validateProfileName() {
		final String name= fProfileNameField.getText().trim();

		if (fProfile.isBuiltInProfile()) {
			if (fProfile.getName().equals(name)) {
				return new Status(IStatus.ERROR, JavaUI.ID_PLUGIN, FormatterMessages.ModifyDialog_BuiltIn_Status);
			}
		}

		if (fProfile.isSharedProfile()) {
			if (fProfile.getName().equals(name)) {
				return new Status(IStatus.ERROR, JavaUI.ID_PLUGIN, FormatterMessages.ModifyDialog_Shared_Status);
			}
		}

		if (name.length() == 0) {
			return new Status(IStatus.ERROR, JavaUI.ID_PLUGIN, FormatterMessages.ModifyDialog_EmptyName_Status);
		}

		return StatusInfo.OK_STATUS;
	}

	private boolean hasChanges() {
		if (!fProfileNameField.getText().trim().equals(fProfile.getName()))
			return true;

		Iterator<Entry<String, String>> iter= fProfile.getSettings().entrySet().iterator();
		for (;iter.hasNext();) {
			Entry<String, String> curr= iter.next();
			if (!fWorkingValues.get(curr.getKey()).equals(curr.getValue())) {
				return true;
			}
		}
		return false;
	}

	protected Composite createPreviewPane(Composite parent) {
		final Composite previewPane= new Composite(parent, SWT.NONE);
		createGridLayout(previewPane, 1, true);
		previewPane.setFont(parent.getFont());

		createLabel(1, previewPane, FormatterMessages.ModifyDialogTabPage_preview_label_text, 0);

		fPreview= new JavaPreview(fWorkingValues, previewPane);
		fPreview.getControl().setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));

		return previewPane;
	}

	protected void updatePreviewCode() {
		if (fPreview != null) {
			String previewCode= getPreviewCode();
			if (previewCode == null || previewCode.trim().isEmpty())
				previewCode= "// " + FormatterMessages.ModifyDialog_previewMissing_comment; //$NON-NLS-1$
			fPreview.setPreviewText(previewCode, fCurrentPreviewType);
		}
	}

	private String getPreviewCode() {
		PreferenceTreeNode<?> currentNode= fFocusManager.fCurrentlyFocused;
		if (currentNode == null)
			return null;

		// try this node
		String previewCode= doGetPreviewCode(currentNode);
		if (previewCode != null)
			return previewCode;

		// combine previews from children, if any
		StringBuilder sb= new StringBuilder();
		List<PreferenceTreeNode<?>> currentLevel= new ArrayList<>(currentNode.getChildren());
		List<PreferenceTreeNode<?>> nextLevel= new ArrayList<>();
		while (!currentLevel.isEmpty()) {
			boolean onlyModuleInfos= true;
			for (PreferenceTreeNode<?> node : currentLevel) {
				previewCode= doGetPreviewCode(node);
				onlyModuleInfos= onlyModuleInfos && fCurrentPreviewType == CodeFormatter.K_MODULE_INFO;
				if (previewCode != null && sb.indexOf(previewCode) == -1)
					sb.append("\n\n").append(previewCode); //$NON-NLS-1$
				nextLevel.addAll(node.getChildren());
			}
			if (sb.length() > 0) {
				fCurrentPreviewType= onlyModuleInfos ? CodeFormatter.K_MODULE_INFO : CodeFormatter.K_UNKNOWN;
				return sb.toString();
			}

			currentLevel= nextLevel;
			nextLevel= new ArrayList<>();
		}

		// try its ancestors
		PreferenceTreeNode<?> node= currentNode.getParent();
		while (node != null) {
			previewCode= doGetPreviewCode(node);
			if (previewCode != null)
				return previewCode;
			node= node.getParent();
		}

		return null;
	}

	private String doGetPreviewCode(PreferenceTreeNode<?> node) {
		if (!(node instanceof Section) && !(node instanceof Preference))
			return null;
		String key= node instanceof Section ? ((Section) node).getKey() : ((Preference<?>) node).getKey();
		final String START_PREFIX= "//--PREVIEW--START--"; //$NON-NLS-1$
		final String END_PREFIX= "//--PREVIEW--END--"; //$NON-NLS-1$
		int startIndex= fPreviewSources.indexOf(START_PREFIX + key);
		if (startIndex < 0)
			return null;

		fCurrentPreviewType= CodeFormatter.K_UNKNOWN;
		int nextPos= startIndex + START_PREFIX.length() + key.length();
		switch (fPreviewSources.charAt(nextPos)) {
			case '\n':
				break;
			case ':':
				if (fPreviewSources.indexOf("COMPILATION_UNIT\n", nextPos) == nextPos + 1) { //$NON-NLS-1$
					fCurrentPreviewType= CodeFormatter.K_COMPILATION_UNIT;
				} else if (fPreviewSources.indexOf("EXPRESSION\n", nextPos) == nextPos + 1) { //$NON-NLS-1$
					fCurrentPreviewType= CodeFormatter.K_EXPRESSION;
				} else if (fPreviewSources.indexOf("CLASS_BODY_DECLARATIONS\n", nextPos) == nextPos + 1) { //$NON-NLS-1$
					fCurrentPreviewType= CodeFormatter.K_CLASS_BODY_DECLARATIONS;
				} else if (fPreviewSources.indexOf("STATEMENTS\n", nextPos) == nextPos + 1) { //$NON-NLS-1$
					fCurrentPreviewType= CodeFormatter.K_STATEMENTS;
				} else if (fPreviewSources.indexOf("MODULE_INFO\n", nextPos) == nextPos + 1) { //$NON-NLS-1$
					fCurrentPreviewType= CodeFormatter.K_MODULE_INFO;
				}
				break;
			default:
				return null;
		}

		int endIndex= fPreviewSources.indexOf(END_PREFIX + key + '\n');
		String previewCode= fPreviewSources.substring(startIndex, endIndex);
		previewCode= previewCode.replaceAll(START_PREFIX + ".*\\R", ""); //$NON-NLS-1$ //$NON-NLS-2$
		previewCode= previewCode.replaceAll(END_PREFIX + ".*\\R", ""); //$NON-NLS-1$ //$NON-NLS-2$
		return previewCode;
	}

	protected void loadPreviews(String previewFile) {
		String resource= "/preview/" + previewFile; //$NON-NLS-1$
		try (Scanner s= new Scanner(getClass().getResourceAsStream(resource), "UTF-8")) { //$NON-NLS-1$
			fPreviewSources= s.useDelimiter("\\A").next(); //$NON-NLS-1$
		}
	}

	protected static Composite createGridComposite(Composite parent, int numColumns) {
		final Composite grid= new Composite(parent, SWT.NONE);
		createGridLayout(parent, numColumns, true);
		grid.setFont(parent.getFont());
		return grid;
	}

	/*
	 * Create a GridLayout with the default margin and spacing settings, as
	 * well as the specified number of columns.
	 */
	protected static void createGridLayout(Composite composite, int numColumns, boolean margins) {
		final GridLayout layout= new GridLayout(numColumns, false);
		PixelConverter pixelConverter= new PixelConverter(composite);
		layout.verticalSpacing= pixelConverter.convertVerticalDLUsToPixels(IDialogConstants.VERTICAL_SPACING);
		layout.horizontalSpacing= pixelConverter.convertHorizontalDLUsToPixels(IDialogConstants.HORIZONTAL_SPACING);
		if (margins) {
			layout.marginHeight= pixelConverter.convertVerticalDLUsToPixels(IDialogConstants.VERTICAL_MARGIN);
			layout.marginWidth= pixelConverter.convertHorizontalDLUsToPixels(IDialogConstants.HORIZONTAL_MARGIN);
		} else {
			layout.marginHeight= 0;
			layout.marginWidth= 0;
		}
		composite.setLayout(layout);
	}

	/*
	 * Convenience method to create a GridData.
	 */
	protected static GridData createGridData(int numColumns, int style, int widthHint, int indent) {
		final GridData gd= new GridData(style);
		gd.horizontalSpan= numColumns;
		gd.widthHint= widthHint;
		gd.horizontalIndent= indent * LayoutUtil.getIndent();
		return gd;
	}

	/*
	 * Convenience method to create a label
	 */
	protected static Label createLabel(int numColumns, Composite parent, String text, int indent) {
		final Label label= new Label(parent, SWT.WRAP);
		label.setFont(parent.getFont());
		label.setText(text);
		GridData gd= new GridData(GridData.BEGINNING, GridData.CENTER, true, false, numColumns, 1);
		gd.horizontalIndent= indent * LayoutUtil.getIndent();
		label.setLayoutData(gd);
		return label;
	}

	/*
	 * Helper to be used with Preference.addDependant(), dependency checker that accepts specific values.
	 */
	protected static Predicate<String> valueAcceptor(String... values) {
		final List<String> valuesList= Arrays.asList(values);
		return valuesList::contains;
	}
}
