/*******************************************************************************
 * Copyright (c) 2000, 2016 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
 *     Eric Rizzo - Externalize Strings wizard always defaults to the "legacy" mechanism - http://bugs.eclipse.org/271375
 *******************************************************************************/
package org.eclipse.jdt.internal.ui.refactoring.nls;

import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Properties;

import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.SashForm;
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.events.SelectionListener;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.layout.FormAttachment;
import org.eclipse.swt.layout.FormData;
import org.eclipse.swt.layout.FormLayout;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Combo;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.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.swt.widgets.Widget;

import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IStatus;

import org.eclipse.core.resources.IFile;

import org.eclipse.jface.dialogs.Dialog;
import org.eclipse.jface.dialogs.IDialogSettings;
import org.eclipse.jface.dialogs.StatusDialog;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.resource.FontRegistry;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.jface.viewers.ArrayContentProvider;
import org.eclipse.jface.viewers.CellEditor;
import org.eclipse.jface.viewers.ColumnLayoutData;
import org.eclipse.jface.viewers.ColumnPixelData;
import org.eclipse.jface.viewers.ColumnWeightData;
import org.eclipse.jface.viewers.ICellModifier;
import org.eclipse.jface.viewers.IFontProvider;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.ITableLabelProvider;
import org.eclipse.jface.viewers.LabelProvider;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.TableLayout;
import org.eclipse.jface.viewers.TableViewer;
import org.eclipse.jface.viewers.TextCellEditor;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.jface.viewers.ViewerFilter;
import org.eclipse.jface.window.Window;
import org.eclipse.jface.wizard.IWizardPage;

import org.eclipse.jface.text.Document;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.Region;
import org.eclipse.jface.text.source.SourceViewer;

import org.eclipse.ui.PlatformUI;

import org.eclipse.ltk.core.refactoring.RefactoringStatus;
import org.eclipse.ltk.ui.refactoring.UserInputWizardPage;

import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.JavaModelException;

import org.eclipse.jdt.internal.core.manipulation.util.BasicElementLabels;
import org.eclipse.jdt.internal.corext.refactoring.nls.KeyValuePair;
import org.eclipse.jdt.internal.corext.refactoring.nls.NLSRefactoring;
import org.eclipse.jdt.internal.corext.refactoring.nls.NLSSubstitution;

import org.eclipse.jdt.ui.JavaElementImageDescriptor;
import org.eclipse.jdt.ui.PreferenceConstants;
import org.eclipse.jdt.ui.text.JavaSourceViewerConfiguration;
import org.eclipse.jdt.ui.text.JavaTextTools;

import org.eclipse.jdt.internal.ui.IJavaHelpContextIds;
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.javaeditor.JavaSourceViewer;
import org.eclipse.jdt.internal.ui.propertiesfileeditor.PropertiesFileEscapes;
import org.eclipse.jdt.internal.ui.util.ExceptionHandler;
import org.eclipse.jdt.internal.ui.util.SWTUtil;
import org.eclipse.jdt.internal.ui.viewsupport.JavaElementImageProvider;
import org.eclipse.jdt.internal.ui.wizards.dialogfields.DialogField;
import org.eclipse.jdt.internal.ui.wizards.dialogfields.IDialogFieldListener;
import org.eclipse.jdt.internal.ui.wizards.dialogfields.LayoutUtil;
import org.eclipse.jdt.internal.ui.wizards.dialogfields.StringDialogField;

class ExternalizeWizardPage extends UserInputWizardPage {

	private static final String[] PROPERTIES;
	private static final String[] fgTitles;
	private static final int STATE_PROP= 0;
	private static final int VAL_PROP= 1;
	private static final int KEY_PROP= 2;
	private static final int SIZE= 3; //column counter
	private static final int ROW_COUNT= 5;

	public static final String PAGE_NAME= "NLSWizardPage1"; //$NON-NLS-1$
	static {
		PROPERTIES= new String[SIZE];
		PROPERTIES[STATE_PROP]= "task"; //$NON-NLS-1$
		PROPERTIES[KEY_PROP]= "key"; //$NON-NLS-1$
		PROPERTIES[VAL_PROP]= "value"; //$NON-NLS-1$

		fgTitles= new String[SIZE];
		fgTitles[STATE_PROP]= ""; //$NON-NLS-1$
		fgTitles[KEY_PROP]= NLSUIMessages.ExternalizeWizardPage_key;
		fgTitles[VAL_PROP]= NLSUIMessages.ExternalizeWizardPage_value;
	}

	private class CellModifier implements ICellModifier {

		/**
		 * @see ICellModifier#canModify(Object, String)
		 */
		@Override
		public boolean canModify(Object element, String property) {
			if (property == null)
				return false;

			if (!(element instanceof NLSSubstitution))
				return false;

			NLSSubstitution subst= (NLSSubstitution) element;
			if (PROPERTIES[KEY_PROP].equals(property) && subst.getState() != NLSSubstitution.EXTERNALIZED) {
				return false;
			}

			return true;
		}

		/**
		 * @see ICellModifier#getValue(Object, String)
		 */
		@Override
		public Object getValue(Object element, String property) {
			if (element instanceof NLSSubstitution) {
				NLSSubstitution substitution= (NLSSubstitution) element;
				String res= null;
				if (PROPERTIES[KEY_PROP].equals(property)) {
					res= substitution.getKeyWithoutPrefix();
				} else if (PROPERTIES[VAL_PROP].equals(property)) {
					res= substitution.getValue();
				} else if (PROPERTIES[STATE_PROP].equals(property)) {
					return Integer.valueOf(substitution.getState());
				}
				if (res != null) {
					return getEscapedAsciiString(res);
				}
				return ""; //$NON-NLS-1$
			}
			return ""; //$NON-NLS-1$
		}

		/**
		 * @see ICellModifier#modify(Object, String, Object)
		 */
		@Override
		public void modify(Object element, String property, Object value) {
			if (element instanceof TableItem) {
				Object data= ((TableItem) element).getData();
				if (data instanceof NLSSubstitution) {
					NLSSubstitution substitution= (NLSSubstitution) data;
					if (PROPERTIES[KEY_PROP].equals(property)) {
						String string = (String)value;
						try {
							string= PropertiesFileEscapes.unescape(string);
						} catch (CoreException e) {
							setPageComplete(RefactoringStatus.create(e.getStatus()));
							return;
						}
						substitution.setKey(string);
					}
					if (PROPERTIES[VAL_PROP].equals(property)) {
						String string = (String)value;
						try {
							string= PropertiesFileEscapes.unescape(string);
						} catch (CoreException e) {
							setPageComplete(RefactoringStatus.create(e.getStatus()));
							return;
						}
						substitution.setValue(string);
					}
					if (PROPERTIES[STATE_PROP].equals(property)) {
						substitution.setState(((Integer) value));
						if ((substitution.getState() == NLSSubstitution.EXTERNALIZED) && substitution.hasStateChanged()) {
							substitution.generateKey(fSubstitutions, getProperties(fNLSRefactoring.getPropertyFileHandle()));
						}
					}
				}
				validateKeys(false);
				fTableViewer.update(data, null);
			}
		}
	}

	private class NLSSubstitutionLabelProvider extends LabelProvider implements ITableLabelProvider, IFontProvider {

		private FontRegistry fFontRegistry;

		public NLSSubstitutionLabelProvider() {
			fFontRegistry= JFaceResources.getFontRegistry();
		}

		@Override
		public String getColumnText(Object element, int columnIndex) {
			String columnText= ""; //$NON-NLS-1$
			if (element instanceof NLSSubstitution) {
				NLSSubstitution substitution= (NLSSubstitution) element;
				if (columnIndex == KEY_PROP) {
					if (substitution.getState() == NLSSubstitution.EXTERNALIZED) {
						columnText=  BasicElementLabels.getJavaElementName(substitution.getKey());
					}
				} else
					if ((columnIndex == VAL_PROP) && (substitution.getValue() != null)) {
						columnText= substitution.getValue();
					}
			}
			return getEscapedAsciiString(columnText);
		}

		@Override
		public Image getColumnImage(Object element, int columnIndex) {
			if ((columnIndex == STATE_PROP) && (element instanceof NLSSubstitution)) {
				return getNLSImage((NLSSubstitution) element);
			}

			return null;
		}

		@Override
		public Font getFont(Object element) {
			if (element instanceof NLSSubstitution) {
				NLSSubstitution substitution= (NLSSubstitution) element;
				if (substitution.hasPropertyFileChange() || substitution.hasSourceChange()) {
					return fFontRegistry.getBold(JFaceResources.DIALOG_FONT);
				}
			}
			return null;
		}

		private Image getNLSImage(NLSSubstitution sub) {
			if ((sub.getValue() == null) && (sub.getKey() != null)) {
				// Missing keys
				JavaElementImageDescriptor imageDescriptor= new JavaElementImageDescriptor(getNLSImageDescriptor(sub.getState()), JavaElementImageDescriptor.WARNING, JavaElementImageProvider.SMALL_SIZE);
				return JavaPlugin.getImageDescriptorRegistry().get(imageDescriptor);
			} else
				if (sub.isConflicting(fSubstitutions) || !isKeyValid(sub, null)) {
					JavaElementImageDescriptor imageDescriptor= new JavaElementImageDescriptor(getNLSImageDescriptor(sub.getState()), JavaElementImageDescriptor.ERROR, JavaElementImageProvider.SMALL_SIZE);
					return JavaPlugin.getImageDescriptorRegistry().get(imageDescriptor);
				} else {
					return	getNLSImage(sub.getState());
				}
		}

		private Image getNLSImage(int task) {
			switch (task) {
				case NLSSubstitution.EXTERNALIZED :
					return JavaPluginImages.get(JavaPluginImages.IMG_OBJS_NLS_TRANSLATE);
				case NLSSubstitution.IGNORED :
					return JavaPluginImages.get(JavaPluginImages.IMG_OBJS_NLS_NEVER_TRANSLATE);
				case NLSSubstitution.INTERNALIZED :
					return JavaPluginImages.get(JavaPluginImages.IMG_OBJS_NLS_SKIP);
				default :
					Assert.isTrue(false);
					return null;
			}
		}

		private ImageDescriptor getNLSImageDescriptor(int task) {
			switch (task) {
				case NLSSubstitution.EXTERNALIZED :
					return JavaPluginImages.DESC_OBJS_NLS_TRANSLATE;
				case NLSSubstitution.IGNORED :
					return JavaPluginImages.DESC_OBJS_NLS_NEVER_TRANSLATE;
				case NLSSubstitution.INTERNALIZED :
					return JavaPluginImages.DESC_OBJS_NLS_SKIP;
				default :
					Assert.isTrue(false);
					return null;
			}
		}
	}

	private static String getEscapedAsciiString(String s) {
		if (s != null) {
			StringBuilder sb= new StringBuilder(s.length());
			int length= s.length();
			for (int i= 0; i < length; i++) {
				char c= s.charAt(i);
				sb.append(getEscapedAsciiString(c));
			}
			return sb.toString();
		}
		return null;
	}

	private static String getEscapedAsciiString(char c) {
		switch (c) {
			case '\b':
				return "\\b";//$NON-NLS-1$
			case '\t':
				return "\\t";//$NON-NLS-1$
			case '\n':
				return "\\n";//$NON-NLS-1$
			case '\f':
				return "\\f";//$NON-NLS-1$
			case '\r':
				return "\\r";//$NON-NLS-1$
			case '\\':
				return "\\\\";//$NON-NLS-1$
		}
		return String.valueOf(c);
	}

	private class NLSInputDialog extends StatusDialog implements IDialogFieldListener {
		private StringDialogField fKeyField;
		private StringDialogField fValueField;
		private DialogField fMessageField;
		private NLSSubstitution fSubstitution;

		public NLSInputDialog(Shell parent, NLSSubstitution substitution) {
			super(parent);

			setTitle(NLSUIMessages.ExternalizeWizardPage_NLSInputDialog_Title);

			fSubstitution= substitution;

			fMessageField= new DialogField();
			if (substitution.getState() == NLSSubstitution.EXTERNALIZED) {
				fMessageField.setLabelText(NLSUIMessages.ExternalizeWizardPage_NLSInputDialog_ext_Label);
			} else {
				fMessageField.setLabelText(NLSUIMessages.ExternalizeWizardPage_NLSInputDialog_Label);
			}

			fKeyField= new StringDialogField();
			fKeyField.setLabelText(NLSUIMessages.ExternalizeWizardPage_NLSInputDialog_Enter_key);
			fKeyField.setDialogFieldListener(this);

			fValueField= new StringDialogField();
			fValueField.setLabelText(NLSUIMessages.ExternalizeWizardPage_NLSInputDialog_Enter_value);
			fValueField.setDialogFieldListener(this);

			if (substitution.getState() == NLSSubstitution.EXTERNALIZED) {
				fKeyField.setText(substitution.getKeyWithoutPrefix());
			} else {
				fKeyField.setText(""); //$NON-NLS-1$
			}

			fValueField.setText(substitution.getValueNonEmpty());
		}

		public KeyValuePair getResult() {
			KeyValuePair res= new KeyValuePair(fKeyField.getText(), fValueField.getText());
			return res;
		}

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

			Composite inner= new Composite(composite, SWT.NONE);
			inner.setFont(composite.getFont());

			GridLayout layout= new GridLayout();
			layout.marginHeight= 0;
			layout.marginWidth= 0;
			layout.numColumns= 2;
			inner.setLayout(layout);

			fMessageField.doFillIntoGrid(inner, 2);

			if (fSubstitution.getState() == NLSSubstitution.EXTERNALIZED) {
				fKeyField.doFillIntoGrid(inner, 2);
				LayoutUtil.setWidthHint(fKeyField.getTextControl(null), convertWidthInCharsToPixels(45));
			}

			fValueField.doFillIntoGrid(inner, 2);
			LayoutUtil.setWidthHint(fValueField.getTextControl(null), convertWidthInCharsToPixels(45));
			LayoutUtil.setHorizontalGrabbing(fValueField.getTextControl(null));

			fValueField.postSetFocusOnDialogField(parent.getDisplay());

			applyDialogFont(composite);
			return composite;
		}

		@Override
		public void dialogFieldChanged(DialogField field) {
			IStatus keyStatus= validateKey(fKeyField.getText());
			//IStatus valueStatus= StatusInfo.OK_STATUS; // no validation yet

			//updateStatus(StatusUtil.getMoreSevere(valueStatus, keyStatus));
			updateStatus(keyStatus);
		}


		private IStatus validateKey(String val) {
			if (fSubstitution.getState() != NLSSubstitution.EXTERNALIZED) {
				return StatusInfo.OK_STATUS;
			}

			if (val == null || val.length() == 0) {
				return new StatusInfo(IStatus.ERROR, NLSUIMessages.ExternalizeWizardPage_NLSInputDialog_Error_empty_key);
			}

			if (fNLSRefactoring.isEclipseNLS()) {
				if (!Character.isJavaIdentifierStart(val.charAt(0)))
					return new StatusInfo(IStatus.ERROR, NLSUIMessages.ExternalizeWizardPage_NLSInputDialog_Error_invalid_EclipseNLS_key);

				for (int i= 1, length= val.length(); i < length; i++) {
					if (!Character.isJavaIdentifierPart(val.charAt(i)))
						return new StatusInfo(IStatus.ERROR, NLSUIMessages.ExternalizeWizardPage_NLSInputDialog_Error_invalid_EclipseNLS_key);
				}
			} else {
				// validation so keys don't contain spaces
				for (int i= 0; i < val.length(); i++) {
					if (Character.isWhitespace(val.charAt(i))) {
						return new StatusInfo(IStatus.ERROR, NLSUIMessages.ExternalizeWizardPage_NLSInputDialog_Error_invalid_key);
					}
				}
			}
			return StatusInfo.OK_STATUS;
		}
	}

	private static final String SETTINGS_NLS_ACCESSORS= "nls_accessor_history"; //$NON-NLS-1$
	private static final int SETTINGS_MAX_ENTRIES= 5;

	private Text fPrefixField;
	private Button fIsEclipseNLS;
	private Table fTable;
	private TableViewer fTableViewer;
	private SourceViewer fSourceViewer;

	private final ICompilationUnit fCu;
	private NLSSubstitution[] fSubstitutions;
	private Button fExternalizeButton;
	private Button fIgnoreButton;
	private Button fInternalizeButton;
	private Button fRevertButton;
	private Button fEditButton;
	private NLSRefactoring fNLSRefactoring;
	private Button fRenameButton;
	private Combo fAccessorClassField;

	private AccessorDescription[] fAccessorChoices;
	private Button fFilterCheckBox;

	public ExternalizeWizardPage(NLSRefactoring nlsRefactoring) {
		super(PAGE_NAME);
		fCu= nlsRefactoring.getCu();
		fSubstitutions= nlsRefactoring.getSubstitutions();
		fNLSRefactoring= nlsRefactoring;
		fAccessorChoices= null;

		setDescription(NLSUIMessages.ExternalizeWizardPage_description);
		createDefaultExternalization(fSubstitutions);
	}

	/*
	 * @see IDialogPage#createControl(Composite)
	 */
	@Override
	public void createControl(Composite parent) {
		initializeDialogUnits(parent);

		Composite supercomposite= new Composite(parent, SWT.NONE);
		supercomposite.setFont(parent.getFont());
		supercomposite.setLayout(new GridLayout());

		createIsEclipseNLSCheckbox(supercomposite);

		createKeyPrefixField(supercomposite);

		SashForm composite= new SashForm(supercomposite, SWT.VERTICAL);
		composite.setFont(supercomposite.getFont());

		GridData data= new GridData(GridData.FILL_BOTH);
		composite.setLayoutData(data);

		createTableViewer(composite);
		createSourceViewer(composite);

		createAccessorInfoComposite(supercomposite);

		composite.setWeights(new int[]{65, 45});

		validateKeys(false);
		updateButtonStates(StructuredSelection.EMPTY);

		// promote control
		setControl(supercomposite);
		Dialog.applyDialogFont(supercomposite);
		PlatformUI.getWorkbench().getHelpSystem().setHelp(supercomposite, IJavaHelpContextIds.EXTERNALIZE_WIZARD_KEYVALUE_PAGE);
	}

	private void createAccessorInfoComposite(Composite supercomposite) {
		Composite accessorComposite= new Composite(supercomposite, SWT.NONE);
		accessorComposite.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
		GridLayout layout= new GridLayout(2, false);
		layout.marginHeight= 0;
		layout.marginWidth= 0;
		accessorComposite.setLayout(layout);

		Composite composite= new Composite(accessorComposite, SWT.NONE);
		composite.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));

		layout= new GridLayout(1, true);
		layout.marginHeight= 0;
		layout.marginWidth= 0;
		composite.setLayout(layout);

		Label accessorClassLabel= new Label(composite, SWT.NONE);
		accessorClassLabel.setText(NLSUIMessages.ExternalizeWizardPage_accessorclass_label);
		accessorClassLabel.setLayoutData(new GridData());


		SelectionListener listener= new SelectionAdapter() {
			@Override
			public void widgetSelected(SelectionEvent e) {
				if (e.widget instanceof Button) {
					doConfigureButtonPressed();
				} else {
					doAccessorSelectionChanged();
				}
			}
		};


		GridData data= new GridData(GridData.FILL_HORIZONTAL);
		data.widthHint= convertWidthInCharsToPixels(30);
		fAccessorClassField= new Combo(composite, SWT.READ_ONLY);
		SWTUtil.setDefaultVisibleItemCount(fAccessorClassField);
		fAccessorClassField.setLayoutData(data);
		fAccessorClassField.addSelectionListener(listener);


		//new Label(composite, SWT.NONE); // placeholder

		Button configure= new Button(accessorComposite, SWT.PUSH);
		configure.setText(NLSUIMessages.ExternalizeWizardPage_configure_button);
		data= new GridData(GridData.HORIZONTAL_ALIGN_END | GridData.VERTICAL_ALIGN_END);
		data.widthHint= SWTUtil.getButtonWidthHint(configure);
		configure.setLayoutData(data);

		configure.addSelectionListener(listener);

		updateAccessorChoices();

	}

	protected void doAccessorSelectionChanged() {
		int selectionIndex= fAccessorClassField.getSelectionIndex();
		if (fAccessorChoices != null && selectionIndex < fAccessorChoices.length) {
			AccessorDescription selected= fAccessorChoices[selectionIndex];
			fNLSRefactoring.setAccessorClassName(selected.getAccessorClassName());
			fNLSRefactoring.setAccessorClassPackage(selected.getAccessorClassPackage());
			fNLSRefactoring.setResourceBundleName(selected.getResourceBundleName());
			fNLSRefactoring.setResourceBundlePackage(selected.getResourceBundlePackage());
			fNLSRefactoring.setIsEclipseNLS(fNLSRefactoring.detectIsEclipseNLS());

			NLSSubstitution.updateSubtitutions(fSubstitutions, getProperties(fNLSRefactoring.getPropertyFileHandle()), fNLSRefactoring.getAccessorClassName());
			if (fIsEclipseNLS != null) {
				fIsEclipseNLS.setSelection(fNLSRefactoring.isEclipseNLS());
				fIsEclipseNLS.setEnabled(willCreateAccessorClass());
				updatePrefix();
			}
			validateKeys(true);
		}
	}

	private boolean willCreateAccessorClass() {
		try {
			return fNLSRefactoring.willCreateAccessorClass();
		} catch (JavaModelException e) {
			return false;
		}
	}

	private void updateAccessorChoices() {

		AccessorDescription configured= new AccessorDescription(
				fNLSRefactoring.getAccessorClassName(),
				fNLSRefactoring.getAccessorClassPackage(),
				fNLSRefactoring.getResourceBundleName(),
				fNLSRefactoring.getResourceBundlePackage());

		ArrayList<AccessorDescription> currChoices= new ArrayList<>();
		ArrayList<String> currLabels= new ArrayList<>();

		currChoices.add(configured);
		currLabels.add(configured.getLabel());

		AccessorDescription[] choices= fAccessorChoices;
		if (choices == null) {
			choices= loadAccessorDescriptions();
		}

		for (AccessorDescription curr : choices) {
			if (!curr.equals(configured)) {
				currChoices.add(curr);
				currLabels.add(curr.getLabel());
			}
		}

		String[] labels= currLabels.toArray(new String[currLabels.size()]);
		fAccessorChoices= currChoices.toArray(new AccessorDescription[currChoices.size()]);

		fAccessorClassField.setItems(labels);
		fAccessorClassField.select(0);
	}


	private AccessorDescription[] loadAccessorDescriptions() {
		IDialogSettings section= JavaPlugin.getDefault().getDialogSettings().getSection(SETTINGS_NLS_ACCESSORS);
		if (section == null) {
			return new AccessorDescription[0];
		}
		ArrayList<AccessorDescription> res= new ArrayList<>();
		for (int i= 0; i < SETTINGS_MAX_ENTRIES; i++) {
			IDialogSettings serializedDesc= section.getSection(String.valueOf(i));
			if (serializedDesc != null) {
				AccessorDescription accessor= AccessorDescription.deserialize(serializedDesc);
				if (accessor != null) {
					res.add(accessor);
				}
			}
		}
		return res.toArray(new AccessorDescription[res.size()]);
	}



	private void storeAccessorDescriptions() {
		if (fAccessorChoices == null) {
			return;
		}
		IDialogSettings dialogSettings= JavaPlugin.getDefault().getDialogSettings();
		IDialogSettings nlsSection= dialogSettings.getSection(SETTINGS_NLS_ACCESSORS);
		if (nlsSection == null) {
			nlsSection= dialogSettings.addNewSection(SETTINGS_NLS_ACCESSORS);
		}
		int nEntries= Math.min(SETTINGS_MAX_ENTRIES, fAccessorChoices.length);
		for (int i= 0; i < nEntries; i++) {
			IDialogSettings serializedDesc= nlsSection.addNewSection(String.valueOf(i));
			fAccessorChoices[i].serialize(serializedDesc);
		}
	}


	private void doConfigureButtonPressed() {
		NLSAccessorConfigurationDialog dialog= new NLSAccessorConfigurationDialog(getShell(), fNLSRefactoring);
		if (dialog.open() == Window.OK) {
			NLSSubstitution.updateSubtitutions(fSubstitutions, getProperties(fNLSRefactoring.getPropertyFileHandle()), fNLSRefactoring.getAccessorClassName());
			if (fIsEclipseNLS != null) {
				fIsEclipseNLS.setSelection(fNLSRefactoring.isEclipseNLS());
				fIsEclipseNLS.setEnabled(willCreateAccessorClass());
			}
			validateKeys(true);
			updateAccessorChoices();
		}
	}

	private Properties getProperties(IFile propertyFile) {
		Properties props= new Properties();
		try {
			if (propertyFile.exists()) {
				try (InputStream is= propertyFile.getContents()) {
					props.load(is);
				}
			}
		} catch (Exception e) {
			// sorry no property
		}
		return props;
	}

	private void createTableViewer(Composite composite) {
		createTableComposite(composite);

		/*
		 * Feature of CellEditors - double click is ignored.
		 * The workaround is to register my own listener and force the desired
		 * behavior.
		 */
		fTableViewer= new TableViewer(fTable) {
			@Override
			protected void hookControl(Control control) {
				super.hookControl(control);
				((Table) control).addMouseListener(new MouseAdapter() {
					@Override
					public void mouseDoubleClick(MouseEvent e) {
						if (getTable().getSelection().length == 0)
							return;
						TableItem item= getTable().getSelection()[0];
						if (item.getBounds(STATE_PROP).contains(e.x, e.y)) {
							List<?> widgetSel= getSelectionFromWidget();
							if (widgetSel == null || widgetSel.size() != 1)
								return;
							NLSSubstitution substitution= (NLSSubstitution) widgetSel.get(0);
							Integer value= (Integer) getCellModifier().getValue(substitution, PROPERTIES[STATE_PROP]);
							int newValue= MultiStateCellEditor.getNextValue(NLSSubstitution.STATE_COUNT, value);
							getCellModifier().modify(item, PROPERTIES[STATE_PROP], Integer.valueOf(newValue));
						}
					}
				});
			}
		};

		fTableViewer.setUseHashlookup(true);

		final CellEditor[] editors= createCellEditors();
		fTableViewer.setCellEditors(editors);
		fTableViewer.setColumnProperties(PROPERTIES);
		fTableViewer.setCellModifier(new CellModifier());

		fTableViewer.setContentProvider(ArrayContentProvider.getInstance());
		fTableViewer.addFilter(new ViewerFilter() {
			@Override
			public boolean select(Viewer viewer, Object parentElement, Object element) {
				if (!fFilterCheckBox.getSelection()) {
					return true;
				}
				NLSSubstitution curr= (NLSSubstitution) element;
				return (curr.getInitialState() == NLSSubstitution.INTERNALIZED) || (curr.getInitialState() == NLSSubstitution.EXTERNALIZED && curr.getInitialValue() == null);
			}
		});


		fTableViewer.setLabelProvider(new NLSSubstitutionLabelProvider());
		fTableViewer.setInput(fSubstitutions);

		fTableViewer.addSelectionChangedListener(ExternalizeWizardPage.this::selectionChanged);
	}

	private void createDefaultExternalization(NLSSubstitution[] substitutions) {
		for (NLSSubstitution substitution : substitutions) {
			if (substitution.getState() == NLSSubstitution.INTERNALIZED) {
				substitution.setState(NLSSubstitution.EXTERNALIZED);
				substitution.generateKey(substitutions, getProperties(fNLSRefactoring.getPropertyFileHandle()));
			}
		}
	}

	private CellEditor[] createCellEditors() {
		final CellEditor editors[]= new CellEditor[SIZE];
		editors[STATE_PROP]= new MultiStateCellEditor(fTable, NLSSubstitution.STATE_COUNT, NLSSubstitution.DEFAULT);
		editors[KEY_PROP]= new TextCellEditor(fTable);
		editors[VAL_PROP]= new TextCellEditor(fTable);
		return editors;
	}

	private void createSourceViewer(Composite parent) {
		Composite c= new Composite(parent, SWT.NONE);
		c.setLayoutData(new GridData(GridData.FILL_BOTH));
		GridLayout gl= new GridLayout();
		gl.marginHeight= 0;
		gl.marginWidth= 0;
		c.setLayout(gl);

		Label l= new Label(c, SWT.NONE);
		l.setText(NLSUIMessages.ExternalizeWizardPage_context);
		l.setLayoutData(new GridData());

		// source viewer
		JavaTextTools tools= JavaPlugin.getDefault().getJavaTextTools();
		int styles= SWT.V_SCROLL | SWT.H_SCROLL | SWT.MULTI | SWT.BORDER | SWT.FULL_SELECTION;
		IPreferenceStore store= JavaPlugin.getDefault().getCombinedPreferenceStore();
		fSourceViewer= new JavaSourceViewer(c, null, null, false, styles, store);
		fSourceViewer.configure(new JavaSourceViewerConfiguration(tools.getColorManager(), store, null, null));
		fSourceViewer.getControl().setFont(JFaceResources.getFont(PreferenceConstants.EDITOR_TEXT_FONT));

		try {

			String contents= fCu.getBuffer().getContents();
			IDocument document= new Document(contents);
			tools.setupJavaDocumentPartitioner(document);

			fSourceViewer.setDocument(document);
			fSourceViewer.setEditable(false);

			GridData gd= new GridData(GridData.FILL_BOTH);
			gd.heightHint= convertHeightInCharsToPixels(10);
			gd.widthHint= convertWidthInCharsToPixels(40);
			fSourceViewer.getControl().setLayoutData(gd);

		} catch (JavaModelException e) {
			ExceptionHandler.handle(e, NLSUIMessages.ExternalizeWizardPage_exception_title, NLSUIMessages.ExternalizeWizardPage_exception_message);
		}
	}

	private void createKeyPrefixField(Composite parent) {
		Composite composite= new Composite(parent, SWT.NONE);
		composite.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
		GridLayout gl= new GridLayout();
		gl.numColumns= 2;
		gl.marginWidth= 0;
		composite.setLayout(gl);

		Label l= new Label(composite, SWT.NONE);
		l.setText(NLSUIMessages.ExternalizeWizardPage_common_prefix);
		l.setLayoutData(new GridData());

		fPrefixField= new Text(composite, SWT.SINGLE | SWT.BORDER);
		fPrefixField.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
		fPrefixField.setText(fNLSRefactoring.getPrefix());
		fPrefixField.selectAll();

		fPrefixField.addModifyListener(e -> {
			fNLSRefactoring.setPrefix(fPrefixField.getText());
			validateKeys(true);
		});
	}

	private void createIsEclipseNLSCheckbox(Composite parent) {
		if (fNLSRefactoring.isEclipseNLS() || fNLSRefactoring.isEclipseNLSAvailable()) {
			fIsEclipseNLS= new Button(parent, SWT.CHECK);
			fIsEclipseNLS.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
			fIsEclipseNLS.setText(NLSUIMessages.ExternalizeWizardPage_isEclipseNLSCheckbox);
			fIsEclipseNLS.setSelection(fNLSRefactoring.isEclipseNLS());
			fIsEclipseNLS.setEnabled(willCreateAccessorClass());
			fIsEclipseNLS.addSelectionListener(new SelectionAdapter() {
				@Override
				public void widgetSelected(SelectionEvent e) {
					super.widgetDefaultSelected(e);
					boolean isEclipseNLS= fIsEclipseNLS.getSelection();
					fNLSRefactoring.setIsEclipseNLS(isEclipseNLS);
					updatePrefix();
				}
			});
		}
	}

	private void validateKeys(boolean refreshTable) {
		RefactoringStatus status= new RefactoringStatus();
		checkInvalidKeys(status);
		checkDuplicateKeys(status);
		checkMissingKeys(status);
		setPageComplete(status);
		if (refreshTable)
			fTableViewer.refresh(true);
	}

	private void checkInvalidKeys(RefactoringStatus status) {
		for (NLSSubstitution substitution : fSubstitutions) {
			if (!isKeyValid(substitution, status)) {
				return;
			}
		}
	}

	private boolean isKeyValid(NLSSubstitution substitution, RefactoringStatus status) {
		if (substitution == null)
			return false;

		if (substitution.getState() != NLSSubstitution.EXTERNALIZED)
			return true;

		String key= substitution.getKey();

		if (fNLSRefactoring.isEclipseNLS()) {
			if (key == null || key.length() == 0 || !Character.isJavaIdentifierStart(key.charAt(0))) {
				if (status != null)
					status.addFatalError(NLSUIMessages.ExternalizeWizardPage_warning_EclipseNLS_keyInvalid);
				return false;
			}
			for (int i= 1, length= key.length(); i < length; i++) {
				if (!Character.isJavaIdentifierPart(key.charAt(i))) {
					if (status != null)
						status.addFatalError(NLSUIMessages.ExternalizeWizardPage_warning_EclipseNLS_keyInvalid);
					return false;
				}
			}
		} else {
			if (key == null || key.length() == 0) {
				if (status != null)
					status.addFatalError(NLSUIMessages.ExternalizeWizardPage_warning_keyInvalid);
				return false;
			}
			// validation so keys don't contain spaces
			for (int i= 0; i < key.length(); i++) {
				if (Character.isWhitespace(key.charAt(i))) {
					if (status != null)
						status.addFatalError(NLSUIMessages.ExternalizeWizardPage_warning_keyInvalid);
					return false;
				}
			}
		}

		return true;
	}

	private void checkDuplicateKeys(RefactoringStatus status) {
		for (NLSSubstitution substitution : fSubstitutions) {
			if (conflictingKeys(substitution)) {
				status.addFatalError(NLSUIMessages.ExternalizeWizardPage_warning_conflicting);
				return;
			}
		}
	}

	private void checkMissingKeys(RefactoringStatus status) {
		for (NLSSubstitution substitution : fSubstitutions) {
			if ((substitution.getValue() == null) && (substitution.getKey() != null)) {
				status.addWarning(NLSUIMessages.ExternalizeWizardPage_warning_keymissing);
				return;
			}
		}
	}

	private boolean conflictingKeys(NLSSubstitution substitution) {
		if (substitution.getState() == NLSSubstitution.EXTERNALIZED) {
			return substitution.isConflicting(fSubstitutions);
		}
		return false;
	}

	private void createTableComposite(Composite parent) {
		Composite comp= new Composite(parent, SWT.NONE);
		comp.setFont(parent.getFont());

		comp.setLayoutData(new GridData(GridData.FILL_BOTH));

		FormLayout fl= new FormLayout();
		fl.marginWidth= 0;
		fl.marginHeight= 0;
		comp.setLayout(fl);

		Label l= new Label(comp, SWT.NONE);
		l.setText(NLSUIMessages.ExternalizeWizardPage_strings_to_externalize);
		FormData formData = new FormData();
		formData.left = new FormAttachment(0, 0);
		l.setLayoutData(formData);

		Control tableControl= createTable(comp);
		formData = new FormData();
		formData.top = new FormAttachment(l, 5);
		formData.left = new FormAttachment(0, 0);
		formData.right = new FormAttachment(100,0);
		formData.bottom = new FormAttachment(100,0);
		tableControl.setLayoutData(formData);

		fFilterCheckBox= new Button(comp, SWT.CHECK);
		fFilterCheckBox.setText(NLSUIMessages.ExternalizeWizardPage_filter_label);
		formData = new FormData();
		formData.right = new FormAttachment(100,0);
		fFilterCheckBox.setLayoutData(formData);
		fFilterCheckBox.addSelectionListener(new SelectionListener() {
			@Override
			public void widgetSelected(SelectionEvent e) {
				doFilterCheckBoxPressed();
			}

			@Override
			public void widgetDefaultSelected(SelectionEvent e) {
			}
		});
		fFilterCheckBox.setSelection(hasNewOrMissingSubstitutions());
	}

	private boolean hasNewOrMissingSubstitutions() {
		for (NLSSubstitution substitution : fSubstitutions) {
			if (substitution.getInitialState() == NLSSubstitution.INTERNALIZED) {
				return true;
			}
			if (substitution.getInitialState() == NLSSubstitution.EXTERNALIZED && substitution.getInitialValue() == null) {
				return true;
			}
		}
		return false;
	}

	/**
	 *
	 */
	protected void doFilterCheckBoxPressed() {
		fTableViewer.refresh();
	}

	private Control createTable(Composite parent) {
		Composite c= new Composite(parent, SWT.NONE);
		GridLayout gl= new GridLayout();
		gl.numColumns= 2;
		gl.marginWidth= 0;
		gl.marginHeight= 0;
		c.setLayout(gl);


		fTable= new Table(c, SWT.H_SCROLL | SWT.V_SCROLL | SWT.MULTI | SWT.FULL_SELECTION | SWT.HIDE_SELECTION | SWT.BORDER);
		fTable.setFont(parent.getFont());

		GridData tableGD= new GridData(GridData.FILL_BOTH);
		tableGD.heightHint= SWTUtil.getTableHeightHint(fTable, ROW_COUNT);
		//tableGD.widthHint= 40;
		fTable.setLayoutData(tableGD);

		fTable.setLinesVisible(true);

		TableLayout layout= new TableLayout();
		fTable.setLayout(layout);
		fTable.setHeaderVisible(true);

		ColumnLayoutData[] columnLayoutData= new ColumnLayoutData[SIZE];
		columnLayoutData[STATE_PROP]= new ColumnPixelData(18, false, true);
		columnLayoutData[KEY_PROP]= new ColumnWeightData(40, true);
		columnLayoutData[VAL_PROP]= new ColumnWeightData(40, true);

		for (int i= 0; i < fgTitles.length; i++) {
			TableColumn tc= new TableColumn(fTable, SWT.NONE, i);
			tc.setText(fgTitles[i]);
			layout.addColumnData(columnLayoutData[i]);
			tc.setResizable(columnLayoutData[i].resizable);
		}

		createButtonComposite(c);
		return c;
	}

	private void createButtonComposite(Composite parent) {
		Composite buttonComp= new Composite(parent, SWT.NONE);
		GridLayout gl= new GridLayout();
		gl.marginHeight= 0;
		gl.marginWidth= 0;
		buttonComp.setLayout(gl);
		buttonComp.setLayoutData(new GridData(GridData.FILL_VERTICAL));

		SelectionAdapter adapter= new SelectionAdapter() {
			@Override
			public void widgetSelected(SelectionEvent e) {
				handleButtonPressed(e.widget);
			}
		};

		fExternalizeButton= createTaskButton(buttonComp, NLSUIMessages.ExternalizeWizardPage_Externalize_Selected, adapter);
		fIgnoreButton= createTaskButton(buttonComp, NLSUIMessages.ExternalizeWizardPage_Ignore_Selected, adapter);
		fInternalizeButton= createTaskButton(buttonComp, NLSUIMessages.ExternalizeWizardPage_Internalize_Selected, adapter);

		new Label(buttonComp, SWT.NONE); // separator

		fEditButton= createTaskButton(buttonComp, NLSUIMessages.ExternalizeWizardPage_Edit_key_and_value, adapter);
		fRevertButton= createTaskButton(buttonComp, NLSUIMessages.ExternalizeWizardPage_Revert_Selected, adapter);
		fRenameButton= createTaskButton(buttonComp, NLSUIMessages.ExternalizeWizardPage_Rename_Keys, adapter);

		fEditButton.setEnabled(false);
		fRenameButton.setEnabled(false);
		buttonComp.pack();
	}

	protected void handleButtonPressed(Widget widget) {
		fTableViewer.getTable().forceFocus(); // make sure cell editor is applied on all platforms, see https://bugs.eclipse.org/bugs/show_bug.cgi?id=269611
		if (widget == fExternalizeButton) {
			setSelectedTasks(NLSSubstitution.EXTERNALIZED);
		} else if (widget == fIgnoreButton) {
			setSelectedTasks(NLSSubstitution.IGNORED);
		} else if (widget == fInternalizeButton) {
			setSelectedTasks(NLSSubstitution.INTERNALIZED);
		} else if (widget == fEditButton) {
			openEditButton(fTableViewer.getSelection());
		} else if (widget == fRevertButton) {
			revertStateOfSelection();
		} else if (widget == fRenameButton) {
			openRenameDialog();
		}
	}

	/**
	 *
	 */
	private void openRenameDialog() {
		IStructuredSelection sel= (IStructuredSelection) fTableViewer.getSelection();
		List<NLSSubstitution> elementsToRename= getExternalizedElements(sel);
		RenameKeysDialog dialog= new RenameKeysDialog(getShell(), elementsToRename);
		if (dialog.open() == Window.OK) {
			fTableViewer.refresh();
			updateButtonStates((IStructuredSelection) fTableViewer.getSelection());
		}
	}

	private void revertStateOfSelection() {
		for (Object name : getSelectedTableEntries()) {
			NLSSubstitution substitution= (NLSSubstitution) name;
			substitution.revert();
		}
		fTableViewer.refresh();
		updateButtonStates((IStructuredSelection) fTableViewer.getSelection());
	}

	private Button createTaskButton(Composite parent, String label, SelectionAdapter adapter) {
		Button button= new Button(parent, SWT.PUSH);
		button.setText(label);
		button.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
		SWTUtil.setButtonDimensionHint(button);
		button.addSelectionListener(adapter);
		return button;
	}

	private void openEditButton(ISelection selection) {
		try {
			IStructuredSelection sel= (IStructuredSelection) fTableViewer.getSelection();
			NLSSubstitution substitution= (NLSSubstitution) sel.getFirstElement();
			if (substitution == null) {
				return;
			}

			NLSInputDialog dialog= new NLSInputDialog(getShell(), substitution);
			if (dialog.open() == Window.CANCEL)
				return;
			KeyValuePair kvPair= dialog.getResult();
			if (substitution.getState() == NLSSubstitution.EXTERNALIZED) {
				substitution.setKey(kvPair.getKey());
			}
			substitution.setValue(kvPair.getValue());
			validateKeys(false);
		} finally {
			fTableViewer.refresh();
			fTableViewer.getControl().setFocus();
			fTableViewer.setSelection(selection);
		}
	}

	private List<?> getSelectedTableEntries() {
		ISelection sel= fTableViewer.getSelection();
		if (sel instanceof IStructuredSelection)
			return((IStructuredSelection) sel).toList();
		else
			return Collections.EMPTY_LIST;
	}

	private void setSelectedTasks(int state) {
		Assert.isTrue(state == NLSSubstitution.EXTERNALIZED || state == NLSSubstitution.IGNORED || state == NLSSubstitution.INTERNALIZED);
		List<?> selected= getSelectedTableEntries();
		String[] props= new String[]{PROPERTIES[STATE_PROP]};
		for (Object name : selected) {
			NLSSubstitution substitution= (NLSSubstitution) name;
			substitution.setState(state);
			if ((substitution.getState() == NLSSubstitution.EXTERNALIZED) && substitution.hasStateChanged()) {
				substitution.generateKey(fSubstitutions, getProperties(fNLSRefactoring.getPropertyFileHandle()));
			}
		}
		fTableViewer.update(selected.toArray(), props);
		fTableViewer.getControl().setFocus();
		updateButtonStates((IStructuredSelection) fTableViewer.getSelection());
	}

	private void selectionChanged(SelectionChangedEvent event) {
		IStructuredSelection selection= (IStructuredSelection) event.getSelection();
		updateButtonStates(selection);
		updateSourceView(selection);
	}

	private void updateSourceView(IStructuredSelection selection) {
		NLSSubstitution first= (NLSSubstitution) selection.getFirstElement();
		if (first != null) {
			Region region= first.getNLSElement().getPosition();
			fSourceViewer.setSelectedRange(region.getOffset(), region.getLength());
			fSourceViewer.revealRange(region.getOffset(), region.getLength());
		}
	}

	private void updateButtonStates(IStructuredSelection selection) {
		fExternalizeButton.setEnabled(true);
		fIgnoreButton.setEnabled(true);
		fInternalizeButton.setEnabled(true);
		fRevertButton.setEnabled(true);

		if (containsOnlyElementsOfSameState(NLSSubstitution.EXTERNALIZED, selection)) {
			fExternalizeButton.setEnabled(false);
		}

		if (containsOnlyElementsOfSameState(NLSSubstitution.IGNORED, selection)) {
			fIgnoreButton.setEnabled(false);
		}

		if (containsOnlyElementsOfSameState(NLSSubstitution.INTERNALIZED, selection)) {
			fInternalizeButton.setEnabled(false);
		}

		if (!containsElementsWithChange(selection)) {
			fRevertButton.setEnabled(false);
		}

		fRenameButton.setEnabled(getExternalizedElements(selection).size() > 1);
		fEditButton.setEnabled(selection.size() == 1);
	}

	private boolean containsElementsWithChange(IStructuredSelection selection) {
		for (Object name : selection) {
			NLSSubstitution substitution= (NLSSubstitution) name;
			if (substitution.hasPropertyFileChange() || substitution.hasSourceChange()) {
				return true;
			}
		}
		return false;
	}

	private List<NLSSubstitution> getExternalizedElements(IStructuredSelection selection) {
		ArrayList<NLSSubstitution> res= new ArrayList<>();
		for (Object name : selection) {
			NLSSubstitution substitution= (NLSSubstitution) name;
			if (substitution.getState() == NLSSubstitution.EXTERNALIZED && !substitution.hasStateChanged()) {
				res.add(substitution);
			}
		}
		return res;
	}

	private boolean containsOnlyElementsOfSameState(int state, IStructuredSelection selection) {
		for (Object name : selection) {
			NLSSubstitution substitution= (NLSSubstitution) name;
			if (substitution.getState() != state) {
				return false;
			}
		}
		return true;
	}

	@Override
	public boolean performFinish() {
		return super.performFinish();
	}

	@Override
	public IWizardPage getNextPage() {
		return super.getNextPage();
	}

	@Override
	public void dispose() {
		storeAccessorDescriptions();
		//widgets will be disposed. only need to null'em
		fPrefixField= null;
		fSourceViewer= null;
		fTable= null;
		fTableViewer= null;
		fEditButton= null;
		super.dispose();
	}

	/**
	 * Updates the prefix.
	 *
	 * @since 3.4
	 */
	private void updatePrefix() {
		if (fNLSRefactoring.isEclipseNLS()) {
			fNLSRefactoring.setPrefix(fNLSRefactoring.getPrefix().replace('.', '_'));
		} else {
			fNLSRefactoring.setPrefix(fNLSRefactoring.getPrefix().replace('_', '.'));
		}
		fPrefixField.setText(fNLSRefactoring.getPrefix());
		validateKeys(true);
	}
}
