/*******************************************************************************
 * Copyright (c) 2022 Simeon Andreev 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:
 *     Simeon Andreev - initial API and implementation
 *******************************************************************************/
package org.eclipse.ui.internal.ide;

import static org.eclipse.ui.internal.LargeFileLimitsPreferenceHandler.PROMPT_EDITOR_PREFERENCE_VALUE;
import static org.eclipse.ui.internal.LargeFileLimitsPreferenceHandler.disableDefaultLimit;
import static org.eclipse.ui.internal.LargeFileLimitsPreferenceHandler.getConfiguredExtensionTypes;
import static org.eclipse.ui.internal.LargeFileLimitsPreferenceHandler.getDefaultLimit;
import static org.eclipse.ui.internal.LargeFileLimitsPreferenceHandler.getDisabledExtensionTypes;
import static org.eclipse.ui.internal.LargeFileLimitsPreferenceHandler.getFileLimitsForExtension;
import static org.eclipse.ui.internal.LargeFileLimitsPreferenceHandler.isDefaultLimitEnabled;
import static org.eclipse.ui.internal.LargeFileLimitsPreferenceHandler.isLargeDocumentLegacyPreferenceSet;
import static org.eclipse.ui.internal.LargeFileLimitsPreferenceHandler.isPromptPreferenceValue;
import static org.eclipse.ui.internal.LargeFileLimitsPreferenceHandler.removeFileLimitsForExtension;
import static org.eclipse.ui.internal.LargeFileLimitsPreferenceHandler.restoreDefaults;
import static org.eclipse.ui.internal.LargeFileLimitsPreferenceHandler.setConfiguredExtensionTypes;
import static org.eclipse.ui.internal.LargeFileLimitsPreferenceHandler.setDefaultLimit;
import static org.eclipse.ui.internal.LargeFileLimitsPreferenceHandler.setDisabledExtensionTypes;
import static org.eclipse.ui.internal.LargeFileLimitsPreferenceHandler.setFileLimitsForExtension;
import static org.eclipse.ui.internal.ide.IDEWorkbenchMessages.LargeFileAssociationsPreferencePage_ColumnHeader_Editor;
import static org.eclipse.ui.internal.ide.IDEWorkbenchMessages.LargeFileAssociationsPreferencePage_ColumnHeader_Size;
import static org.eclipse.ui.internal.ide.IDEWorkbenchMessages.LargeFileAssociationsPreferencePage_DefaultLimitGroup_Label;
import static org.eclipse.ui.internal.ide.IDEWorkbenchMessages.LargeFileAssociationsPreferencePage_DefaultPreference_Label;
import static org.eclipse.ui.internal.ide.IDEWorkbenchMessages.LargeFileAssociationsPreferencePage_DisabledPageText;
import static org.eclipse.ui.internal.ide.IDEWorkbenchMessages.LargeFileAssociationsPreferencePage_EditEditorButton_Label;
import static org.eclipse.ui.internal.ide.IDEWorkbenchMessages.LargeFileAssociationsPreferencePage_EditorDialog_Error_DuplicateSize;
import static org.eclipse.ui.internal.ide.IDEWorkbenchMessages.LargeFileAssociationsPreferencePage_EditorDialog_Error_NoEditorSelected;
import static org.eclipse.ui.internal.ide.IDEWorkbenchMessages.LargeFileAssociationsPreferencePage_EditorDialog_Label_Editor;
import static org.eclipse.ui.internal.ide.IDEWorkbenchMessages.LargeFileAssociationsPreferencePage_EditorDialog_Label_FileSize;
import static org.eclipse.ui.internal.ide.IDEWorkbenchMessages.LargeFileAssociationsPreferencePage_EditorDialog_Message;
import static org.eclipse.ui.internal.ide.IDEWorkbenchMessages.LargeFileAssociationsPreferencePage_EditorDialog_Radio_Prompt;
import static org.eclipse.ui.internal.ide.IDEWorkbenchMessages.LargeFileAssociationsPreferencePage_EditorDialog_Radio_Specific;
import static org.eclipse.ui.internal.ide.IDEWorkbenchMessages.LargeFileAssociationsPreferencePage_EditorDialog_ShellTitle;
import static org.eclipse.ui.internal.ide.IDEWorkbenchMessages.LargeFileAssociationsPreferencePage_EditorDialog_Title;
import static org.eclipse.ui.internal.ide.IDEWorkbenchMessages.LargeFileAssociationsPreferencePage_EditorTable_DialogPrompt;
import static org.eclipse.ui.internal.ide.IDEWorkbenchMessages.LargeFileAssociationsPreferencePage_ExtensionDialog_Error_WildcardAndDot;
import static org.eclipse.ui.internal.ide.IDEWorkbenchMessages.LargeFileAssociationsPreferencePage_ExtensionDialog_Label;
import static org.eclipse.ui.internal.ide.IDEWorkbenchMessages.LargeFileAssociationsPreferencePage_ExtensionDialog_Message;
import static org.eclipse.ui.internal.ide.IDEWorkbenchMessages.LargeFileAssociationsPreferencePage_ExtensionsTable_DisabledClarification;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.eclipse.core.runtime.Assert;
import org.eclipse.jface.dialogs.Dialog;
import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.jface.dialogs.TitleAreaDialog;
import org.eclipse.jface.layout.GridLayoutFactory;
import org.eclipse.jface.layout.LayoutConstants;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.preference.PreferencePage;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.resource.JFaceColors;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.jface.window.Window;
import org.eclipse.osgi.util.NLS;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.ModifyEvent;
import org.eclipse.swt.events.ModifyListener;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.events.VerifyEvent;
import org.eclipse.swt.events.VerifyListener;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.layout.FillLayout;
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.Event;
import org.eclipse.swt.widgets.Group;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.MessageBox;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.TableColumn;
import org.eclipse.swt.widgets.TableItem;
import org.eclipse.swt.widgets.Text;
import org.eclipse.ui.IEditorDescriptor;
import org.eclipse.ui.IEditorRegistry;
import org.eclipse.ui.ISharedImages;
import org.eclipse.ui.IWorkbench;
import org.eclipse.ui.IWorkbenchPreferencePage;
import org.eclipse.ui.dialogs.EditorSelectionDialog;
import org.eclipse.ui.forms.events.HyperlinkAdapter;
import org.eclipse.ui.forms.events.HyperlinkEvent;
import org.eclipse.ui.forms.widgets.Hyperlink;
import org.eclipse.ui.internal.IPreferenceConstants;
import org.eclipse.ui.internal.IWorkbenchHelpContextIds;
import org.eclipse.ui.internal.LargeFileLimitsPreferenceHandler.FileLimit;
import org.eclipse.ui.internal.WorkbenchImages;
import org.eclipse.ui.internal.WorkbenchMessages;
import org.eclipse.ui.internal.WorkbenchPlugin;
import org.eclipse.ui.internal.dialogs.DialogUtil;
import org.eclipse.ui.internal.registry.EditorDescriptor;
import org.eclipse.ui.internal.util.PrefUtil;

/**
 * Preference page to configure large file editors based on file type and file
 * size.
 */
public class LargeFileAssociationsPreferencePage extends PreferencePage implements IWorkbenchPreferencePage, Listener {

	private static final String DATA_EDITOR = "editor"; //$NON-NLS-1$
	private static final String DATA_PROMPT = "prompt"; //$NON-NLS-1$

	private static final String DIALOG_PROMPT_EDITOR_TABLE_TEXT = LargeFileAssociationsPreferencePage_EditorTable_DialogPrompt;

	private static final int EDITOR_TABLE_CELL_INDEX_EDITOR = 0;
	private static final int EDITOR_TABLE_CELL_INDEX_SIZE = 1;

	private static final String[] SIZE_UNITS = { "B", "KB", "MB", "GB" }; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
	private static final int MEGABYTE_UNIT_INDEX = 2;
	private static final long B = 1L;
	private static final long KB = 1024L;
	private static final long MB = KB * KB;
	private static final long GB = MB * KB;
	private static final long[] SIZE_MULTIPLIERS = { B, KB, MB, GB };

	private static final String SIZE_TEXT_PREFIX = "> "; //$NON-NLS-1$
	private static final int DEFAULT_SIZE_UNIT = MEGABYTE_UNIT_INDEX;

	private static final Comparator<EditorForSize> EDITOR_COMPARATOR = LargeFileAssociationsPreferencePage::compareEditors;

	private Table resourceTypeTable;
	private Button addResourceTypeButton;
	private Button removeResourceTypeButton;

	private Table editorTable;
	private Button addEditorButton;
	private Button editEditorButton;
	private Button removeEditorButton;

	private Button enableDefaultPreference;
	private Text defaultPreferenceFileSize;
	private Combo defaultPreferenceUnit;

	private Label editorLabel;

	private IWorkbench workbench;

	private List<Image> imagesToDispose;

	private Map<IEditorDescriptor, Image> editorsToImages;

	/**
	 * Add a new resource type to the collection shown in the top of the page. This
	 * is typically called after the extension dialog is shown to the user.
	 *
	 * @param newExtension the new extension
	 */
	public void addResourceType(String newExtension) {
		Assert.isTrue(newExtension != null && newExtension.length() != 0);

		// Find the index at which to insert the new entry.
		LargeFileAssociation association;
		TableItem[] items = resourceTypeTable.getItems();
		boolean found = false;
		int i = 0;

		while (i < items.length && !found) {
			association = (LargeFileAssociation) items[i].getData();
			int result = newExtension.compareToIgnoreCase(association.extension);
			if (result == 0) {
				// Same resource type not allowed!
				MessageBox msgBox = new MessageBox(getShell(), SWT.ICON_INFORMATION | SWT.OK);
				msgBox.setMessage(WorkbenchMessages.FileEditorPreference_existsMessage);
				msgBox.setText(WorkbenchMessages.FileEditorPreference_existsTitle);
				msgBox.open();
				return;
			}

			if (result < 0) {
				found = true;
			} else {
				i++;
			}
		}

		// Create the new type and insert it
		association = new LargeFileAssociation(newExtension);
		// per default add a value for an editor prompt
		EditorForSize defaultPromptEditor = defaultPromptEditor();
		association.addEditor(defaultPromptEditor);
		TableItem item = newResourceTableItem(association, i, true);
		resourceTypeTable.setFocus();
		resourceTypeTable.showItem(item);
		fillEditorTable();
	}

	/**
	 * Creates the page's UI content.
	 */
	@Override
	protected Control createContents(Composite parent) {
		boolean isLegacyPreferenceSet = isLargeDocumentLegacyPreferenceSet();

		Composite pageComponent = new Composite(parent, SWT.NONE);
		if (isLegacyPreferenceSet) {
			createDisabledPreferencePageContents(pageComponent);
		} else {
			createPreferencePageContents(pageComponent);
		}

		workbench.getHelpSystem().setHelp(parent, IWorkbenchHelpContextIds.LARGE_FILE_ASSOCIATIONS_PREFERENCE_PAGE);

		return pageComponent;
	}

	private static void createDisabledPreferencePageContents(Composite pageComponent) {
		pageComponent.setLayout(new FillLayout());
		String disabledPageText = NLS.bind(LargeFileAssociationsPreferencePage_DisabledPageText,
				IPreferenceConstants.LARGE_DOC_SIZE_FOR_EDITORS);
		Label legacyPreferenceSetLabel = new Label(pageComponent, SWT.NONE);
		legacyPreferenceSetLabel.setText(disabledPageText);
	}

	private void createPreferencePageContents(Composite pageComponent) {
		imagesToDispose = new ArrayList<>();
		editorsToImages = new HashMap<>(50);

		GridLayout layout = new GridLayout();
		layout.numColumns = 2;
		layout.marginWidth = 0;
		layout.marginHeight = 0;
		pageComponent.setLayout(layout);
		GridData data = new GridData();
		data.verticalAlignment = GridData.FILL;
		data.horizontalAlignment = GridData.FILL;
		pageComponent.setLayoutData(data);

		Group defaultLimitGroup = new Group(pageComponent, SWT.NONE);
		defaultLimitGroup.setText(LargeFileAssociationsPreferencePage_DefaultLimitGroup_Label);
		GridLayout groupLayout = new GridLayout();
		groupLayout.marginWidth = 0;
		groupLayout.marginHeight = 0;
		groupLayout.numColumns = 3;
		defaultLimitGroup.setLayout(groupLayout);
		data = new GridData();
		data.verticalAlignment = GridData.FILL;
		data.horizontalAlignment = GridData.FILL;
		data.horizontalSpan = 2;
		defaultLimitGroup.setLayoutData(data);
		enableDefaultPreference = new Button(defaultLimitGroup, SWT.CHECK);
		enableDefaultPreference.setText(LargeFileAssociationsPreferencePage_DefaultPreference_Label);
		defaultPreferenceFileSize = createFileSizeText(defaultLimitGroup);
		data = new GridData();
		data.horizontalAlignment = GridData.FILL;
		data.grabExcessHorizontalSpace = true;
		data.minimumWidth = 6;
		defaultPreferenceFileSize.setLayoutData(data);
		defaultPreferenceUnit = createSizeUnitCombo(defaultLimitGroup);
		enableDefaultPreference.addSelectionListener(new SelectionListener() {
			@Override
			public void widgetSelected(SelectionEvent e) {
				updateDefaultPreferenceWidgetsEnabledState();
			}

			@Override
			public void widgetDefaultSelected(SelectionEvent e) {
				updateDefaultPreferenceWidgetsEnabledState();
			}
		});
		setDefaultPreferenceWidgetValues();

		// layout the top table & its buttons
		Label label = new Label(pageComponent, SWT.LEFT);
		label.setText(WorkbenchMessages.FileEditorPreference_fileTypes);
		data = new GridData();
		data.horizontalAlignment = GridData.FILL;
		data.horizontalSpan = 2;
		label.setLayoutData(data);

		resourceTypeTable = new Table(pageComponent, SWT.CHECK | SWT.MULTI | SWT.BORDER | SWT.FULL_SELECTION);
		resourceTypeTable.addListener(SWT.Selection, this);
		resourceTypeTable.addListener(SWT.DefaultSelection, this);
		data = new GridData(GridData.FILL_HORIZONTAL);

		int availableRows = DialogUtil.availableRows(pageComponent);

		data.heightHint = resourceTypeTable.getItemHeight() * (availableRows / 16);
		resourceTypeTable.setLayoutData(data);

		resourceTypeTable.addListener(SWT.Selection, event -> {
			if (event.detail == SWT.CHECK) {
				TableItem item = (TableItem) event.item;
				LargeFileAssociation association = (LargeFileAssociation) item.getData();
				boolean newEnabled = item.getChecked();
				boolean oldEnabled = association.enabled;
				association.enabled = newEnabled;
				if (oldEnabled != newEnabled) {
					String extensionText = getExtensionText(association);
					item.setText(extensionText);
					fillEditorTable();
				}
			}
		});

		Composite groupComponent = new Composite(pageComponent, SWT.NONE);
		groupLayout = new GridLayout();
		groupLayout.marginWidth = 0;
		groupLayout.marginHeight = 0;
		groupComponent.setLayout(groupLayout);
		data = new GridData();
		data.verticalAlignment = GridData.FILL;
		data.horizontalAlignment = GridData.FILL;
		groupComponent.setLayoutData(data);

		addResourceTypeButton = new Button(groupComponent, SWT.PUSH);
		addResourceTypeButton.setText(WorkbenchMessages.FileEditorPreference_add);
		addResourceTypeButton.addListener(SWT.Selection, this);
		setButtonLayoutData(addResourceTypeButton);

		removeResourceTypeButton = new Button(groupComponent, SWT.PUSH);
		removeResourceTypeButton.setText(WorkbenchMessages.FileEditorPreference_remove);
		removeResourceTypeButton.addListener(SWT.Selection, this);
		setButtonLayoutData(removeResourceTypeButton);

		// Spacer
		label = new Label(pageComponent, SWT.LEFT);
		data = new GridData();
		data.horizontalAlignment = GridData.FILL;
		data.horizontalSpan = 2;
		label.setLayoutData(data);

		// layout the bottom table & its buttons
		editorLabel = new Label(pageComponent, SWT.LEFT);
		editorLabel.setText(WorkbenchMessages.FileEditorPreference_associatedEditors);
		data = new GridData();
		data.horizontalAlignment = GridData.FILL;
		data.horizontalSpan = 2;
		editorLabel.setLayoutData(data);

		editorTable = new Table(pageComponent, SWT.MULTI | SWT.BORDER);
		TableColumn editorColumn = new TableColumn(editorTable, SWT.NONE);
		editorColumn.setText(LargeFileAssociationsPreferencePage_ColumnHeader_Editor);
		editorColumn.setWidth(300);
		TableColumn sizeColumn = new TableColumn(editorTable, SWT.NONE);
		sizeColumn.setText(LargeFileAssociationsPreferencePage_ColumnHeader_Size);
		sizeColumn.setWidth(100);
		editorTable.addListener(SWT.Selection, this);
		editorTable.addListener(SWT.DefaultSelection, this);
		data = new GridData(GridData.FILL_BOTH);
		data.heightHint = editorTable.getItemHeight() * 7;
		editorTable.setLayoutData(data);
		editorTable.setHeaderVisible(true);

		groupComponent = new Composite(pageComponent, SWT.NONE);
		groupLayout = new GridLayout();
		groupLayout.marginWidth = 0;
		groupLayout.marginHeight = 0;
		groupComponent.setLayout(groupLayout);
		data = new GridData();
		data.verticalAlignment = GridData.FILL;
		data.horizontalAlignment = GridData.FILL;
		groupComponent.setLayoutData(data);

		addEditorButton = new Button(groupComponent, SWT.PUSH);
		addEditorButton.setText(WorkbenchMessages.FileEditorPreference_addEditor);
		addEditorButton.addListener(SWT.Selection, this);
		addEditorButton.setLayoutData(data);
		setButtonLayoutData(addEditorButton);

		editEditorButton = new Button(groupComponent, SWT.PUSH);
		editEditorButton.setText(LargeFileAssociationsPreferencePage_EditEditorButton_Label);
		editEditorButton.addListener(SWT.Selection, this);
		editEditorButton.setLayoutData(data);
		setButtonLayoutData(editEditorButton);

		removeEditorButton = new Button(groupComponent, SWT.PUSH);
		removeEditorButton.setText(WorkbenchMessages.FileEditorPreference_removeEditor);
		removeEditorButton.addListener(SWT.Selection, this);
		setButtonLayoutData(removeEditorButton);

		fillResourceTypeTable();
		if (resourceTypeTable.getItemCount() > 0) {
			resourceTypeTable.setSelection(0);
		}
		fillEditorTable();
		updateEnabledState();

		applyDialogFont(pageComponent);
	}

	private void updateDefaultPreferenceWidgetsEnabledState() {
		boolean enabled = enableDefaultPreference.getSelection();
		defaultPreferenceFileSize.setEnabled(enabled);
		defaultPreferenceUnit.setEnabled(enabled);
	}

	private void setDefaultPreferenceWidgetValues() {
		enableDefaultPreference.setSelection(isDefaultLimitEnabled());
		SizeAndUnit sizeAndUnit = defaultLimit();
		defaultPreferenceFileSize.setText(sizeAndUnit.sizeString);
		defaultPreferenceUnit.select(sizeAndUnit.unitIndex);
		updateDefaultPreferenceWidgetsEnabledState();
	}

	/**
	 * The preference page is going to be disposed. So deallocate all allocated SWT
	 * resources that aren't disposed automatically by disposing the page (i.e
	 * fonts, cursors, etc). Subclasses should re-implement this method to release
	 * their own allocated SWT resources.
	 */
	@Override
	public void dispose() {
		super.dispose();
		if (imagesToDispose != null) {
			for (Image image : imagesToDispose) {
				image.dispose();
			}
			imagesToDispose = null;
		}
		if (editorsToImages != null) {
			for (Image image : editorsToImages.values()) {
				image.dispose();
			}
			editorsToImages = null;
		}
	}

	/**
	 * Hook method to get a page specific preference store. Reimplement this method
	 * if a page don't want to use its parent's preference store.
	 */
	@Override
	protected IPreferenceStore doGetPreferenceStore() {
		return WorkbenchPlugin.getDefault().getPreferenceStore();
	}

	/**
	 * Populate the large file editors for the selected file type.
	 */
	protected void fillEditorTable() {
		editorTable.removeAll();
		LargeFileAssociation association = getSelectedLargeFileAssociation();
		if (association != null) {
			editorTable.setEnabled(association.enabled);
			if (association.enabled) {
				for (EditorForSize editorForSize : association.editors) {
					TableItem item = new TableItem(editorTable, SWT.NONE);
					String label;
					if (editorForSize.isPrompt) {
						label = DIALOG_PROMPT_EDITOR_TABLE_TEXT;
						item.setData(DATA_PROMPT, DATA_PROMPT);
					} else {
						label = editorForSize.editor.getLabel();
						item.setData(DATA_EDITOR, editorForSize.editor);
						item.setImage(getImage(editorForSize.editor));

					}
					String[] text = { label, getSizeText(editorForSize.size) };
					item.setText(text); // $NON-NLS-1$
				}
			}
		}
	}

	/**
	 * Populate the table with file types that have configured large file editors.
	 */
	protected void fillResourceTypeTable() {
		resourceTypeTable.removeAll();
		String[] configured = getConfiguredExtensionTypes();
		String[] disabled = getDisabledExtensionTypes();

		List<String> extensions = new ArrayList<>();
		extensions.addAll(Arrays.asList(configured));
		extensions.addAll(Arrays.asList(disabled));
		Set<String> disabledSet = new HashSet<>(Arrays.asList(disabled));

		List<LargeFileAssociation> associations = new ArrayList<>();

		for (String extension : extensions) {
			List<FileLimit> fileLimits = getFileLimitsForExtension(extension);
			LargeFileAssociation association = new LargeFileAssociation(extension);

			association.enabled = !disabledSet.contains(extension);
			long lowestPromptSize = Long.MAX_VALUE;

			for (FileLimit fileLimit : fileLimits) {
				long size = fileLimit.fileSize;
				String editorIdString = fileLimit.editorId;

				SizeAndUnit promptSize = null;

				IEditorDescriptor editorDescriptor = findEditor(editorIdString);
				if (editorDescriptor != null) {
					SizeAndUnit sizeAndUnit = getSizeAndUnit(size);
					EditorForSize editorForSize = new EditorForSize(editorDescriptor, sizeAndUnit);
					association.addEditor(editorForSize);
				} else if (isPromptPreferenceValue(editorIdString)) {
					// use the smallest size for the "prompt for editor" entry
					long newSize = Long.valueOf(size);
					if (newSize < lowestPromptSize) {
						lowestPromptSize = newSize;
						promptSize = getSizeAndUnit(size);
					}
				}

				if (promptSize != null) {
					EditorForSize promptEditor = new EditorForSize(promptSize);
					association.addEditor(promptEditor);
				}
			}

			associations.add(association);
		}

		int index = 0;
		for (LargeFileAssociation association : associations) {
			newResourceTableItem(association, index, false);
			++index;
		}
	}

	/**
	 * Returns the image associated with the given editor.
	 */
	protected Image getImage(IEditorDescriptor editor) {
		Image image = editorsToImages.get(editor);
		if (image == null) {
			image = editor.getImageDescriptor().createImage();
			editorsToImages.put(editor, image);
		}
		return image;
	}

	protected LargeFileAssociation getSelectedLargeFileAssociation() {
		TableItem[] items = resourceTypeTable.getSelection();
		if (items.length == 1) {
			return (LargeFileAssociation) items[0].getData();
		}
		return null;
	}

	protected IEditorDescriptor[] getAssociatedEditors() {
		if (getSelectedLargeFileAssociation() == null) {
			return null;
		}
		if (editorTable.getItemCount() > 0) {
			List<IEditorDescriptor> editorList = new ArrayList<>();
			for (int i = 0; i < editorTable.getItemCount(); i++) {
				IEditorDescriptor editorDescriptor = (IEditorDescriptor) editorTable.getItem(i).getData(DATA_EDITOR);
				if (editorDescriptor != null) {
					editorList.add(editorDescriptor);
				}
			}

			return editorList.toArray(new IEditorDescriptor[editorList.size()]);
		}
		return null;
	}

	@Override
	public void handleEvent(Event event) {
		if (event.widget == addResourceTypeButton) {
			promptForResourceType();
		} else if (event.widget == removeResourceTypeButton) {
			removeSelectedResourceType();
		} else if (event.widget == addEditorButton) {
			promptForEditor();
		} else if (event.widget == editEditorButton) {
			editSelectedEditor();
		} else if (event.widget == removeEditorButton) {
			removeSelectedEditor();
		} else if (event.widget == resourceTypeTable) {
			fillEditorTable();
		}

		updateEnabledState();

	}

	/**
	 * @see IWorkbenchPreferencePage
	 */
	@Override
	public void init(IWorkbench aWorkbench) {
		this.workbench = aWorkbench;
	}

	/**
	 * Create a new <code>TableItem</code> to represent the resource type editor
	 * description supplied.
	 */
	protected TableItem newResourceTableItem(LargeFileAssociation association, int index, boolean selected) {
		IEditorRegistry editorRegistry = workbench.getEditorRegistry();
		ImageDescriptor imageDescriptor = editorRegistry.getImageDescriptor("tmp." + association.extension); //$NON-NLS-1$
		if (imageDescriptor == null) {
			imageDescriptor = getImageDescriptor();
		}
		Image image = imageDescriptor.createImage(false);
		if (image != null) {
			imagesToDispose.add(image);
		}

		TableItem item = new TableItem(resourceTypeTable, SWT.NONE, index);
		if (image != null) {
			item.setImage(image);
		}
		String extensionText = getExtensionText(association);
		item.setText(extensionText);
		item.setChecked(association.enabled);
		item.setData(association);
		if (selected) {
			resourceTypeTable.setSelection(index);
		}

		return item;
	}

	@Override
	public boolean performOk() {
		updateDefaultPreference();
		updateExtensionPreferences();
		PrefUtil.savePrefs();
		return true;
	}

	@Override
	public void performDefaults() {
		restoreDefaults();
		setDefaultPreferenceWidgetValues();
		fillResourceTypeTable();
		fillEditorTable();
		updateEnabledState();
	}

	private void updateDefaultPreference() {
		boolean isDefaultPreferenceEnabled = enableDefaultPreference.getSelection();
		if (isDefaultPreferenceEnabled) {
			SizeAndUnit size = getSizeAndUnit(defaultPreferenceFileSize, defaultPreferenceUnit);
			setDefaultLimit(size.bytes);
		} else {
			disableDefaultLimit();
		}
	}

	private void updateExtensionPreferences() {
		String[] previousConfiguredExtensions = getConfiguredExtensionTypes();
		String[] previousDisabledExtensions = getDisabledExtensionTypes();
		List<String> configuredExtensions = new ArrayList<>();
		List<String> disabledExtensions = new ArrayList<>();
		List<String> newExtensions = new ArrayList<>();

		TableItem[] items = resourceTypeTable.getItems();
		for (TableItem item : items) {
			LargeFileAssociation association = (LargeFileAssociation) (item.getData());
			String extension = association.extension;
			if (!association.enabled) {
				disabledExtensions.add(extension);
			}
			List<FileLimit> fileLimits = new ArrayList<>();
			for (EditorForSize editorForSize : association.editors) {
				String editorId;
				if (editorForSize.isPrompt) {
					editorId = PROMPT_EDITOR_PREFERENCE_VALUE;
				} else {
					editorId = editorForSize.editor.getId();
				}
				long fileSize = editorForSize.size.bytes;
				FileLimit fileLimit = new FileLimit(editorId, fileSize);
				fileLimits.add(fileLimit);
			}
			if (!fileLimits.isEmpty()) {
				if (association.enabled) {
					configuredExtensions.add(extension);
				}
				newExtensions.add(extension);
				setFileLimitsForExtension(extension, fileLimits);
			}
		}

		setConfiguredExtensionTypes(configuredExtensions.toArray(String[]::new));
		setDisabledExtensionTypes(disabledExtensions.toArray(String[]::new));

		Set<String> removedExtensions = new HashSet<>();
		removedExtensions.addAll(Arrays.asList(previousConfiguredExtensions));
		removedExtensions.addAll(Arrays.asList(previousDisabledExtensions));
		removedExtensions.removeAll(newExtensions);
		for (String removedExtension : removedExtensions) {
			removeFileLimitsForExtension(removedExtension);
		}
	}

	/**
	 * Prompt for editor above a file size.
	 */
	public void promptForEditor() {
		// prevent duplicate editors
		IEditorDescriptor[] filteredEditors = getAssociatedEditors();
		// prevent input of duplicate file sizes for the same extension
		long[] configuredFileSizes = getConfiguredFileSizes();
		Shell shell = getControl().getShell();
		EditorForSize initialValues = defaultPromptEditor();
		String extension = getSelectedLargeFileAssociation().extension;
		EditorForSizeDialog editorForSizeDialog = new EditorForSizeDialog(shell, filteredEditors, configuredFileSizes,
				initialValues, extension);
		if (editorForSizeDialog.open() == Window.OK) {
			addEditor(editorForSizeDialog.chosenValues);
		}
	}

	void editSelectedEditor() {
		LargeFileAssociation association = getSelectedLargeFileAssociation();
		int selectionIndex = editorTable.getSelectionIndex();
		TableItem item = editorTable.getItem(selectionIndex);
		String editorIdString = item.getText(EDITOR_TABLE_CELL_INDEX_EDITOR);
		boolean promptForEditor = LargeFileAssociationsPreferencePage_EditorTable_DialogPrompt.equals(editorIdString);
		String sizeString = item.getText(EDITOR_TABLE_CELL_INDEX_SIZE);
		SizeAndUnit selectedSize = parseSizeString(sizeString);

		// prevent duplicate editors
		IEditorDescriptor[] filteredEditors = getAssociatedEditors();
		IEditorDescriptor selectedEditor = (IEditorDescriptor) item.getData(DATA_EDITOR);
		if (selectedEditor != null) {
			IEditorDescriptor[] filteredEditorsWithoutSelected = Arrays.stream(filteredEditors)
					.filter(e -> e != selectedEditor).toArray(IEditorDescriptor[]::new);
			filteredEditors = filteredEditorsWithoutSelected;
		}
		// prevent input of duplicate file sizes for the same extension
		long[] configuredFileSizes = getConfiguredFileSizes();
		long[] filteredFileSizes = Arrays.stream(configuredFileSizes).filter(l -> l != selectedSize.bytes).toArray();

		EditorForSize initialValues = new EditorForSize(selectedEditor, promptForEditor, selectedSize);
		String extension = getSelectedLargeFileAssociation().extension;
		Shell shell = getControl().getShell();

		EditorForSizeDialog editorForSizeDialog = new EditorForSizeDialog(shell, filteredEditors, filteredFileSizes,
				initialValues, extension);
		if (editorForSizeDialog.open() == Window.OK) {
			EditorDescriptor previousEditor = (EditorDescriptor) item.getData(DATA_EDITOR);
			Object promptData = item.getData(DATA_PROMPT);
			if (previousEditor != null) {
				association.removeEditor(previousEditor);
			} else if (promptData != null) {
				association.removePromptEditor();
			}
			editorTable.remove(selectionIndex);

			addEditor(editorForSizeDialog.chosenValues);
		}
	}

	private void addEditor(EditorForSize editorForSize) {
		LargeFileAssociation association = getSelectedLargeFileAssociation();
		if (!editorForSize.isPrompt) {
			association.addEditor(editorForSize);
		} else {
			int promptIndex = getIndexOfPromptItem();
			if (promptIndex != -1) {
				editorTable.remove(promptIndex);
			}
			association.setPromptEditorSize(editorForSize.size);
		}

		int index = association.indexOfEditor(editorForSize);
		TableItem item;
		if (index >= editorTable.getItemCount()) {
			item = new TableItem(editorTable, SWT.NONE);
		} else {
			item = new TableItem(editorTable, SWT.NONE, index);
		}
		String editorText;
		if (!editorForSize.isPrompt) {
			item.setData(DATA_EDITOR, editorForSize.editor);
			item.setImage(getImage(editorForSize.editor));
			editorText = editorForSize.editor.getLabel();
		} else {
			item.setData(DATA_PROMPT, DATA_PROMPT);
			editorText = DIALOG_PROMPT_EDITOR_TABLE_TEXT;
		}
		String[] text = { editorText, getSizeText(editorForSize.size) };
		item.setText(text);
		editorTable.setSelection(index);
		editorTable.setFocus();
	}

	private static EditorDescriptor promptForEditorWithDialog(Shell shell, IEditorDescriptor[] filteredEditors,
			String extension) {
		EditorDescriptor selectedEditor = null;
		EditorSelectionDialog editorSelectionDialog = new EditorSelectionDialog(shell);
		editorSelectionDialog.setEditorsToFilter(filteredEditors);
		editorSelectionDialog.setMessage(
				NLS.bind(WorkbenchMessages.Choose_the_editor_for_file, extension));
		if (editorSelectionDialog.open() == Window.OK) {
			selectedEditor = (EditorDescriptor) editorSelectionDialog.getSelectedEditor();
		}
		return selectedEditor;
	}

	private IEditorDescriptor findEditor(String editorIdString) {
		IEditorDescriptor editorDescriptor = workbench.getEditorRegistry().findEditor(editorIdString);
		return editorDescriptor;
	}

	private int getIndexOfPromptItem() {
		int promptIndex = -1;
		TableItem[] editorTableItems = editorTable.getItems();
		for (int j = 0; j < editorTableItems.length; ++j) {
			TableItem editorTableItem = editorTableItems[j];
			if (editorTableItem.getData(DATA_PROMPT) != null) {
				promptIndex = j;
				break;
			}
		}
		return promptIndex;
	}

	private long[] getConfiguredFileSizes() {
		LargeFileAssociation association = getSelectedLargeFileAssociation();
		int numberOfSizes = association.editors.size();
		long[] configuredFileSizes = new long[numberOfSizes];
		int index = 0;
		for (EditorForSize editor : association.editors) {
			configuredFileSizes[index] = editor.size.bytes;
			++index;
		}
		return configuredFileSizes;
	}

	/**
	 * Prompt for resource type.
	 */
	public void promptForResourceType() {
		ExtensionDialog dialog = new ExtensionDialog(getControl().getShell());
		if (dialog.open() == Window.OK) {
			String extension = dialog.getExtension();
			addResourceType(extension);
		}
	}

	/**
	 * Remove the editor from the table
	 */
	public void removeSelectedEditor() {
		TableItem[] items = editorTable.getSelection();
		if (items.length > 0) {
			for (TableItem item : items) {
				LargeFileAssociation association = getSelectedLargeFileAssociation();
				IEditorDescriptor editorDescriptor = (IEditorDescriptor) item.getData(DATA_EDITOR);
				if (editorDescriptor != null) {
					association.removeEditor(editorDescriptor);
				} else if (item.getData(DATA_PROMPT) != null) {
					association.removePromptEditor();
				}
				item.dispose();
			}
		}
		editorTable.deselectAll();
	}

	/**
	 * Remove the type from the table
	 */
	public void removeSelectedResourceType() {
		for (TableItem item : resourceTypeTable.getSelection()) {
			item.dispose();
		}
		// Clear out the editors too
		editorTable.removeAll();
	}

	/**
	 * Update the enabled state.
	 */
	public void updateEnabledState() {
		// Update enabled state
		int selectedResources = resourceTypeTable.getSelectionCount();

		removeResourceTypeButton.setEnabled(selectedResources != 0);
		editorLabel.setEnabled(selectedResources == 1);
		addEditorButton.setEnabled(selectedResources == 1);

		int selectedEditors = editorTable.getSelectionCount();
		int editorsCount = editorTable.getItemCount();
		boolean removeEditorsEnabled = editorsCount > 1 && selectedEditors > 0;
		editEditorButton.setEnabled(selectedEditors == 1);
		removeEditorButton.setEnabled(removeEditorsEnabled);
	}

	private static ImageDescriptor getImageDescriptor() {
		return WorkbenchImages.getImageDescriptor(ISharedImages.IMG_OBJ_FILE);
	}

	private static String getExtensionText(LargeFileAssociation association) {
		String text = association.extension;
		if (!association.enabled) {
			text += " " + LargeFileAssociationsPreferencePage_ExtensionsTable_DisabledClarification; //$NON-NLS-1$
		}
		return text;
	}

	private static String getSizeText(SizeAndUnit size) {
		String sizeText = SIZE_TEXT_PREFIX + size.sizeString;
		sizeText += getSizeSuffix(size.unitIndex);
		return sizeText;
	}

	private static Text createFileSizeText(Composite parent) {
		Text text = new Text(parent, SWT.BORDER);
		text.addVerifyListener(new NumberVerifyListener());
		text.addModifyListener(new NumberModifyListener());
		text.setText(defaultLimit().sizeString);
		return text;
	}

	private static Combo createSizeUnitCombo(Composite parent) {
		Combo sizeUnitCombo = new Combo(parent, SWT.READ_ONLY);
		for (String unit : SIZE_UNITS) {
			sizeUnitCombo.add(unit);
		}
		sizeUnitCombo.select(DEFAULT_SIZE_UNIT);
		return sizeUnitCombo;
	}

	private static SizeAndUnit getSizeAndUnit(Text fileSizeText, Combo sizeUnitCombo) {
		String fileSizeString = fileSizeText.getText();
		int sizeUnitSelectionIndex = sizeUnitCombo.getSelectionIndex();
		SizeAndUnit size = new SizeAndUnit(fileSizeString, sizeUnitSelectionIndex);
		return size;
	}

	private static SizeAndUnit getSizeAndUnit(long bytes) {
		long largestSizeMultiplier = 1L;
		int sizeUnitIndex = 0;
		for (int i = 0; i < SIZE_UNITS.length; ++i) {
			long sizeMultiplier = SIZE_MULTIPLIERS[i];
			if (bytes % sizeMultiplier == 0) {
				if (largestSizeMultiplier < sizeMultiplier) {
					largestSizeMultiplier = sizeMultiplier;
					sizeUnitIndex = i;
				}
			}
		}
		long sizeInUnit = bytes / largestSizeMultiplier;
		String sizeString = String.valueOf(sizeInUnit);
		SizeAndUnit sizeAndUnit = new SizeAndUnit(sizeString, sizeUnitIndex);
		return sizeAndUnit;
	}

	private static long computeSize(String sizeString, int unitIndex) {
		long bytesWithoutUnit = Long.valueOf(sizeString);
		long unitMultiplier = SIZE_MULTIPLIERS[unitIndex];
		long size = bytesWithoutUnit * unitMultiplier;
		return size;
	}

	private static SizeAndUnit parseSizeString(String sizeString) {
		if (sizeString.startsWith(SIZE_TEXT_PREFIX)) {
			sizeString = sizeString.substring(SIZE_TEXT_PREFIX.length());
		}
		// remove leading/trailing empty spaces if present
		sizeString = sizeString.strip();
		int unitIndex = 0;
		// handle 'KB', 'MB', etc. in the size string
		for (int i = 0; i < SIZE_UNITS.length; ++i) {
			String sizeSuffix = getSizeSuffix(i);
			if (sizeString.endsWith(sizeSuffix)) {
				int newLength = sizeString.length() - sizeSuffix.length();
				sizeString = sizeString.substring(0, newLength);
				unitIndex = i;
				break;
			}
		}
		long sizeMultiplier = SIZE_MULTIPLIERS[unitIndex];
		// remove leading/trailing empty spaces if present
		sizeString = sizeString.strip();
		long size = Long.valueOf(sizeString);
		size *= sizeMultiplier;
		return new SizeAndUnit(sizeString, unitIndex, size);
	}

	private static String getSizeSuffix(int unitIndex) {
		return " " + SIZE_UNITS[unitIndex]; //$NON-NLS-1$
	}

	private static int compareEditors(EditorForSize e1, EditorForSize e2) {
		return Long.compare(e1.size.bytes, e2.size.bytes);
	}

	private static EditorForSize defaultPromptEditor() {
		SizeAndUnit size = defaultLimit();
		EditorForSize defaultPromptEditor = new EditorForSize(size);
		return defaultPromptEditor;
	}

	private static SizeAndUnit defaultLimit() {
		return getSizeAndUnit(getDefaultLimit());
	}

	private static class NumberVerifyListener implements VerifyListener {
		@Override
		public void verifyText(VerifyEvent e) {
			Text text = (Text) e.getSource();

			String oldText = text.getText();
			String prefix = oldText.substring(0, e.start);
			String suffix = oldText.substring(e.end);
			String newText = prefix + e.text + suffix;

			// only allow non-negative integers or empty string
			if (newText.startsWith("0") && newText.length() > 1) { //$NON-NLS-1$
				e.doit = false;
			} else if (newText.startsWith("-") || newText.startsWith("+")) { //$NON-NLS-1$ //$NON-NLS-2$
				e.doit = false;
			} else if (!newText.isEmpty()) {
				try {
					long size = Long.parseLong(newText);
					if (size < 0) {
						e.doit = false;
					}
				} catch (NumberFormatException ex) {
					e.doit = false;
				}
			}
		}
	}

	private static class NumberModifyListener implements ModifyListener {
		@Override
		public void modifyText(ModifyEvent e) {
			Text text = (Text) e.widget;
			String contents = text.getText();
			if (contents.isBlank()) {
				// to allow deleting selected text, set a '0' for empty text field
				text.setText("0"); //$NON-NLS-1$
			}
		}
	}

	private static class ExtensionDialog extends TitleAreaDialog {

		private static final String ASTERIX_PREFIX = "*."; //$NON-NLS-1$

		private Button okButton;
		private Text extensionText;

		private String extension = ""; //$NON-NLS-1$

		ExtensionDialog(Shell shell) {
			super(shell);
			setShellStyle(getShellStyle() | SWT.SHEET);
		}


		@Override
		protected void configureShell(Shell shell) {
			super.configureShell(shell);
			shell.setText(WorkbenchMessages.FileExtension_shellTitle);
		}

		@Override
		protected Control createDialogArea(Composite parent) {
			Composite parentComposite = (Composite) super.createDialogArea(parent);

			Composite contents = new Composite(parentComposite, SWT.NONE);
			contents.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));

			setTitle(WorkbenchMessages.FileExtension_shellTitle);
			setMessage(LargeFileAssociationsPreferencePage_ExtensionDialog_Message);

			new Label(contents, SWT.LEFT).setText(LargeFileAssociationsPreferencePage_ExtensionDialog_Label);

			extensionText = new Text(contents, SWT.SINGLE | SWT.BORDER);

			extensionText.addModifyListener(event -> {
				if (event.widget == extensionText) {
					extension = extensionText.getText().trim();
					okButton.setEnabled(validateExtension());
				}
			});
			extensionText.setFocus();

			Dialog.applyDialogFont(parentComposite);

			Point defaultMargins = LayoutConstants.getMargins();
			GridLayoutFactory.fillDefaults().numColumns(2).margins(defaultMargins.x, defaultMargins.y)
					.generateLayout(contents);

			return contents;
		}

		@Override
		protected void createButtonsForButtonBar(Composite parent) {
			okButton = createButton(parent, IDialogConstants.OK_ID, IDialogConstants.OK_LABEL, true);
			okButton.setEnabled(false);
			createButton(parent, IDialogConstants.CANCEL_ID, IDialogConstants.CANCEL_LABEL, false);
		}

		private boolean validateExtension() {
			if (extension.isEmpty()) {
				setErrorMessage(WorkbenchMessages.FileExtension_extensionEmptyMessage);
				return false;
			}

			int dotIndex = extension.indexOf('.');
			int asterixIndex = extension.indexOf('*');
			if (dotIndex != -1 || asterixIndex != -1) {
				if (!extension.startsWith(ASTERIX_PREFIX) || extension.equals(ASTERIX_PREFIX)) {
					setErrorMessage(LargeFileAssociationsPreferencePage_ExtensionDialog_Error_WildcardAndDot);
					return false;
				}
			}

			setErrorMessage(null);
			return true;
		}

		String getExtension() {
			if (extension.startsWith(ASTERIX_PREFIX)) {
				return extension.substring(ASTERIX_PREFIX.length());
			}
			return extension;
		}

		@Override
		protected boolean isResizable() {
			return true;
		}
	}

	private static class EditorForSizeDialog extends TitleAreaDialog {

		private final long[] filteredFileSizes;
		private final IEditorDescriptor[] filteredEditors;
		private final EditorForSize initialValues;
		private final String extension;

		private Text sizeText;
		private Combo sizeUnitCombo;
		private Button promptEditorOnOpen;
		private Button chooseEditorWithDialog;
		private Button okButton;

		private IEditorDescriptor selectedEditor;
		EditorForSize chosenValues;

		EditorForSizeDialog(Shell shell, IEditorDescriptor[] filteredEditors, long[] filteredFileSizes,
				EditorForSize initialValues, String extension) {
			super(shell);
			setShellStyle(getShellStyle() | SWT.SHEET);
			this.filteredFileSizes = filteredFileSizes;
			this.filteredEditors = filteredEditors;
			this.initialValues = initialValues;
			this.extension = extension;
			selectedEditor = initialValues.editor;
			chosenValues = initialValues;
		}

		@Override
		protected void configureShell(Shell newShell) {
			super.configureShell(newShell);
			newShell.setText(LargeFileAssociationsPreferencePage_EditorDialog_ShellTitle);
		}

		@Override
		protected Control createDialogArea(Composite parent) {
			Composite parentComposite = (Composite) super.createDialogArea(parent);

			Composite contents = new Composite(parentComposite, SWT.NONE);
			contents.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));

			setTitle(NLS.bind(LargeFileAssociationsPreferencePage_EditorDialog_Title, extension));
			setMessage(NLS.bind(LargeFileAssociationsPreferencePage_EditorDialog_Message, extension));

			Group editorTypeGroup = new Group(contents, SWT.NONE);
			editorTypeGroup.setText(LargeFileAssociationsPreferencePage_EditorDialog_Label_Editor);
			editorTypeGroup.setLayout(new GridLayout(2, false));
			editorTypeGroup.setLayoutData(new GridData(SWT.FILL, SWT.BEGINNING, true, false));
			promptEditorOnOpen = new Button(editorTypeGroup, SWT.RADIO);
			promptEditorOnOpen.setSelection(initialValues.isPrompt);
			promptEditorOnOpen.setText(LargeFileAssociationsPreferencePage_EditorDialog_Radio_Prompt);
			GridData data = new GridData();
			data.horizontalSpan = 2;
			promptEditorOnOpen.setLayoutData(data);
			chooseEditorWithDialog = new Button(editorTypeGroup, SWT.RADIO);
			chooseEditorWithDialog.setLayoutData(new GridData(SWT.FILL, SWT.BEGINNING, false, false));
			chooseEditorWithDialog.setSelection(!initialValues.isPrompt);
			Hyperlink chooseEditorLink = new Hyperlink(editorTypeGroup, SWT.NONE);
			chooseEditorLink.setUnderlined(true);
			chooseEditorLink.setFont(JFaceResources.getDialogFont());
			if (selectedEditor != null) {
				chooseEditorLink.setText(selectedEditor.getLabel());
			} else {
				chooseEditorLink.setText(LargeFileAssociationsPreferencePage_EditorDialog_Radio_Specific);
			}
			chooseEditorLink.setForeground(JFaceColors.getHyperlinkText(chooseEditorLink.getDisplay()));
			chooseEditorLink.setLayoutData(new GridData(SWT.FILL, SWT.BEGINNING, true, false));
			chooseEditorLink.setEnabled(!initialValues.isPrompt);
			data = new GridData();
			data.horizontalAlignment = SWT.FILL;
			data.grabExcessHorizontalSpace = true;
			chooseEditorLink.setLayoutData(data);

			chooseEditorWithDialog.addSelectionListener(new SelectionAdapter() {
				@Override
				public void widgetSelected(SelectionEvent e) {
					boolean selectEditorWithDialog = chooseEditorWithDialog.getSelection();
					chooseEditorLink.setEnabled(selectEditorWithDialog);
					okButton.setEnabled(validateValues());
				}
			});
			chooseEditorLink.addHyperlinkListener(new HyperlinkAdapter() {
				@Override
				public void linkActivated(HyperlinkEvent e) {
					selectedEditor = promptForEditorWithDialog(getShell(), filteredEditors, extension);
					if (selectedEditor != null) {
						chooseEditorLink.setText(selectedEditor.getLabel());
					} else {
						chooseEditorLink.setText(LargeFileAssociationsPreferencePage_EditorDialog_Radio_Specific);
					}
					okButton.setEnabled(validateValues());
				}
			});

			Group fileSizeGroup = new Group(contents, SWT.NONE);
			fileSizeGroup.setText(LargeFileAssociationsPreferencePage_EditorDialog_Label_FileSize);
			fileSizeGroup.setLayout(new FillLayout(SWT.HORIZONTAL));
			fileSizeGroup.setLayoutData(new GridData(SWT.LEFT, SWT.BEGINNING, true, false));
			sizeText = createFileSizeText(fileSizeGroup);
			sizeText.setText(initialValues.size.sizeString);
			sizeUnitCombo = createSizeUnitCombo(fileSizeGroup);
			sizeUnitCombo.select(initialValues.size.unitIndex);
			sizeText.setFocus();

			sizeText.addModifyListener(event -> {
				if (event.widget == sizeText) {
					okButton.setEnabled(validateValues());
				}
			});
			sizeUnitCombo.addModifyListener(event -> {
				if (event.widget == sizeUnitCombo) {
					okButton.setEnabled(validateValues());
				}
			});

			Dialog.applyDialogFont(contents);

			Point defaultMargins = LayoutConstants.getMargins();
			GridLayoutFactory.fillDefaults().numColumns(1).margins(defaultMargins.x, defaultMargins.y)
					.generateLayout(contents);

			return contents;
		}

		@Override
		protected void createButtonsForButtonBar(Composite parent) {
			okButton = createButton(parent, IDialogConstants.OK_ID, IDialogConstants.OK_LABEL, true);
			okButton.setEnabled(validateSize());
			createButton(parent, IDialogConstants.CANCEL_ID, IDialogConstants.CANCEL_LABEL, false);
		}

		@Override
		protected void okPressed() {
			boolean promptForEditor = promptEditorOnOpen.getSelection();
			SizeAndUnit size = getSizeAndUnit(sizeText, sizeUnitCombo);
			chosenValues = new EditorForSize(selectedEditor, promptForEditor, size);
			super.okPressed();
		}

		@Override
		protected boolean isResizable() {
			return true;
		}

		private boolean validateValues() {
			if (!validateSelectedEditor()) {
				return false;
			}
			if (!validateSize()) {
				return false;
			}
			return true;
		}
		private boolean validateSelectedEditor() {
			boolean selectEditorWithDialog = chooseEditorWithDialog.getSelection();
			if (selectEditorWithDialog && selectedEditor == null) {
				setErrorMessage(LargeFileAssociationsPreferencePage_EditorDialog_Error_NoEditorSelected);
				return false;
			}
			setErrorMessage(null);
			return true;
		}

		private boolean validateSize() {
			SizeAndUnit currentSize = getSizeAndUnit(sizeText, sizeUnitCombo);
			long currentFileSize = currentSize.bytes;
			boolean editorConfiguredForBytes = false;
			for (long configuredFileSize : filteredFileSizes) {
				if (currentFileSize == configuredFileSize) {
					editorConfiguredForBytes = true;
					break;
				}
			}
			if (editorConfiguredForBytes) {
				setErrorMessage(LargeFileAssociationsPreferencePage_EditorDialog_Error_DuplicateSize);
				return false;
			}
			setErrorMessage(null);
			return true;
		}
	}

	private static class LargeFileAssociation {
		private final String extension;
		private final List<EditorForSize> editors;
		private boolean enabled;

		LargeFileAssociation(String extension) {
			this.extension = extension;
			editors = new ArrayList<>();
			enabled = true;
		}

		void addEditor(EditorForSize editor) {
			int index = indexOfEditor(editor);
			editors.add(index, editor);
		}

		int indexOfEditor(EditorForSize editor) {
			int index = Collections.binarySearch(editors, editor, EDITOR_COMPARATOR);
			if (index < 0) {
				index = -index - 1;
			}
			return index;
		}

		void removeEditor(IEditorDescriptor editor) {
			if (editor != null) {
				editors.removeIf(e -> e.editor != null && e.editor.equals(editor));
			}
		}

		void removePromptEditor() {
			editors.removeIf(e -> e.isPrompt);
		}

		void setPromptEditorSize(SizeAndUnit size) {
			removePromptEditor();
			EditorForSize promptEditor = new EditorForSize(size);
			addEditor(promptEditor);
		}
	}

	private static class EditorForSize {
		final IEditorDescriptor editor;
		final boolean isPrompt;
		final SizeAndUnit size;

		EditorForSize(SizeAndUnit size) {
			this(null, true, size);
		}

		EditorForSize(IEditorDescriptor editor, SizeAndUnit size) {
			this(editor, false, size);
		}

		private EditorForSize(IEditorDescriptor editor, boolean isPrompt, SizeAndUnit size) {
			this.editor = editor;
			this.isPrompt = isPrompt;
			this.size = size;
		}
	}

	private static class SizeAndUnit {
		final String sizeString;
		final int unitIndex;
		final long bytes;

		SizeAndUnit(String sizeString, int unitIndex) {
			this(sizeString, unitIndex, computeSize(sizeString, unitIndex));
		}

		SizeAndUnit(String sizeString, int unitIndex, long bytes) {
			this.sizeString = sizeString;
			this.unitIndex = unitIndex;
			this.bytes = bytes;
		}
	}
}
