/*******************************************************************************
 * Copyright (c) 2000, 2022 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
 *     Philippe Ombredanne - bug 149382
 *     Microsoft Corporation - read formatting options from the compilation unit
 *******************************************************************************/
package org.eclipse.jdt.ui.actions;

import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.graphics.Image;
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.Link;
import org.eclipse.swt.widgets.Shell;

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

import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.jface.dialogs.IDialogSettings;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.operation.IRunnableContext;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.viewers.CheckboxTreeViewer;
import org.eclipse.jface.viewers.ILabelProvider;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.ITreeContentProvider;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.jface.viewers.ViewerFilter;
import org.eclipse.jface.window.Window;

import org.eclipse.jface.text.IRewriteTarget;
import org.eclipse.jface.text.ITextSelection;

import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IWorkbenchSite;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.dialogs.ISelectionStatusValidator;

import org.eclipse.jdt.core.Flags;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IField;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IMember;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.Signature;
import org.eclipse.jdt.core.dom.CompilationUnit;

import org.eclipse.jdt.internal.core.manipulation.CodeTemplateContextType;
import org.eclipse.jdt.internal.core.manipulation.util.BasicElementLabels;
import org.eclipse.jdt.internal.corext.codemanipulation.AddGetterSetterOperation;
import org.eclipse.jdt.internal.corext.codemanipulation.CodeGenerationSettings;
import org.eclipse.jdt.internal.corext.codemanipulation.GetterSetterUtil;
import org.eclipse.jdt.internal.corext.codemanipulation.IRequestQuery;
import org.eclipse.jdt.internal.corext.dom.IASTSharedValues;
import org.eclipse.jdt.internal.corext.refactoring.util.RefactoringASTParser;
import org.eclipse.jdt.internal.corext.util.JavaModelUtil;
import org.eclipse.jdt.internal.corext.util.JdtFlags;
import org.eclipse.jdt.internal.corext.util.Messages;

import org.eclipse.jdt.ui.JavaElementComparator;
import org.eclipse.jdt.ui.JavaElementImageDescriptor;
import org.eclipse.jdt.ui.JavaElementLabelProvider;
import org.eclipse.jdt.ui.JavaElementLabels;
import org.eclipse.jdt.ui.JavaUI;

import org.eclipse.jdt.internal.ui.IJavaHelpContextIds;
import org.eclipse.jdt.internal.ui.JavaPlugin;
import org.eclipse.jdt.internal.ui.actions.ActionMessages;
import org.eclipse.jdt.internal.ui.actions.ActionUtil;
import org.eclipse.jdt.internal.ui.actions.SelectionConverter;
import org.eclipse.jdt.internal.ui.actions.WorkbenchRunnableAdapter;
import org.eclipse.jdt.internal.ui.dialogs.SourceActionDialog;
import org.eclipse.jdt.internal.ui.dialogs.StatusInfo;
import org.eclipse.jdt.internal.ui.javaeditor.CompilationUnitEditor;
import org.eclipse.jdt.internal.ui.preferences.JavaPreferencesSettings;
import org.eclipse.jdt.internal.ui.util.BusyIndicatorRunnableContext;
import org.eclipse.jdt.internal.ui.util.ElementValidator;
import org.eclipse.jdt.internal.ui.util.ExceptionHandler;
import org.eclipse.jdt.internal.ui.viewsupport.JavaElementImageProvider;

/**
 * Creates getter and setter methods for a type's fields. Opens a dialog with a list of
 * fields for which a setter or getter can be generated. User is able to check or uncheck
 * items before setters or getters are generated.
 * <p>
 * Will open the parent compilation unit in a Java editor. The result is unsaved, so the
 * user can decide if the changes are acceptable.
 * <p>
 * The action is applicable to structured selections containing elements of type
 * <code>IField</code> or <code>IType</code>.
 *
 * <p>
 * This class may be instantiated; it is not intended to be subclassed.
 * </p>
 *
 * @since 2.0
 *
 * @noextend This class is not intended to be subclassed by clients.
 */
public class AddGetterSetterAction extends SelectionDispatchAction {

	private boolean fSort;

	private boolean fSynchronized;

	private boolean fFinal;

	private int fVisibility;

	private boolean fGenerateComment;

	private int fNumEntries;

	private CompilationUnitEditor fEditor;

	private static final String DIALOG_TITLE= ActionMessages.AddGetterSetterAction_error_title;

	/**
	 * Creates a new <code>AddGetterSetterAction</code>. The action requires that the
	 * selection provided by the site's selection provider is of type <code>
	 * org.eclipse.jface.viewers.IStructuredSelection</code>.
	 *
	 * @param site the site providing context information for this action
	 */
	public AddGetterSetterAction(IWorkbenchSite site) {
		super(site);
		setText(ActionMessages.AddGetterSetterAction_label);
		setDescription(ActionMessages.AddGetterSetterAction_description);
		setToolTipText(ActionMessages.AddGetterSetterAction_tooltip);

		PlatformUI.getWorkbench().getHelpSystem().setHelp(this, IJavaHelpContextIds.GETTERSETTER_ACTION);
	}

	/**
	 * Note: This constructor is for internal use only. Clients should not call this
	 * constructor.
	 *
	 * @param editor the compilation unit editor
	 *
	 * @noreference This constructor is not intended to be referenced by clients.
	 */
	public AddGetterSetterAction(CompilationUnitEditor editor) {
		this(editor.getEditorSite());
		fEditor= editor;
		setEnabled(SelectionConverter.getInputAsCompilationUnit(editor) != null);
		fEditor.getEditorSite();
	}


	// ---- Structured Viewer -----------------------------------------------------------

	@Override
	public void selectionChanged(IStructuredSelection selection) {
		try {
			setEnabled(canEnable(selection));
		} catch (JavaModelException e) {
			// http://bugs.eclipse.org/bugs/show_bug.cgi?id=19253
			if (JavaModelUtil.isExceptionToBeLogged(e))
				JavaPlugin.log(e);
			setEnabled(false);
		}
	}

	@Override
	public void run(IStructuredSelection selection) {
		try {
			IField[] selectedFields= getSelectedFields(selection);
			if (canRunOn(selectedFields)) {
				run(selectedFields[0].getDeclaringType(), selectedFields, false);
				return;
			}
			Object firstElement= selection.getFirstElement();

			if (firstElement instanceof IType)
				run((IType) firstElement, new IField[0], false);
			else if (firstElement instanceof ICompilationUnit) {
				// http://bugs.eclipse.org/bugs/show_bug.cgi?id=38500
				IType type= ((ICompilationUnit) firstElement).findPrimaryType();
				// type can be null if file has a bad encoding
				if (type == null) {
					MessageDialog.openError(getShell(),
							ActionMessages.AddGetterSetterAction_no_primary_type_title,
							ActionMessages.AddGetterSetterAction_no_primary_type_message);
					notifyResult(false);
					return;
				}
				if (type.isAnnotation()) {
					MessageDialog.openInformation(getShell(), DIALOG_TITLE, ActionMessages.AddGetterSetterAction_annotation_not_applicable);
					notifyResult(false);
					return;
				} else if (type.isInterface()) {
					MessageDialog.openInformation(getShell(), DIALOG_TITLE, ActionMessages.AddGetterSetterAction_interface_not_applicable);
					notifyResult(false);
					return;
				} else
					run(((ICompilationUnit) firstElement).findPrimaryType(), new IField[0], false);
			}
		} catch (CoreException e) {
			ExceptionHandler.handle(e, getShell(), DIALOG_TITLE, ActionMessages.AddGetterSetterAction_error_actionfailed);
		}

	}

	private boolean canEnable(IStructuredSelection selection) throws JavaModelException {
		if (getSelectedFields(selection) != null)
			return true;

		if ((selection.size() == 1) && (selection.getFirstElement() instanceof IType)) {
			IType type= (IType) selection.getFirstElement();
			return type.getCompilationUnit() != null && !type.isInterface() && !type.isAnonymous();
		}

		if ((selection.size() == 1) && (selection.getFirstElement() instanceof ICompilationUnit))
			return true;

		return false;
	}

	private boolean canRunOn(IField[] fields) throws JavaModelException {
		if (fields == null || fields.length == 0)
			return false;
		int count= 0;
		for (IField field : fields) {
			if (!JdtFlags.isEnum(field)) {
				count++;
			}
		}
		if (count == 0)
			MessageDialog.openInformation(getShell(), DIALOG_TITLE, ActionMessages.AddGetterSetterAction_not_applicable);
		return (count > 0);
	}

	private void resetNumEntries() {
		fNumEntries= 0;
	}

	private void incNumEntries() {
		fNumEntries++;
	}

	private void run(IType type, IField[] preselected, boolean editor) throws CoreException {
		if (type.isAnnotation()) {
			MessageDialog.openInformation(getShell(), DIALOG_TITLE, ActionMessages.AddGetterSetterAction_annotation_not_applicable);
			notifyResult(false);
			return;
		} else if (type.isInterface()) {
			MessageDialog.openInformation(getShell(), DIALOG_TITLE, ActionMessages.AddGetterSetterAction_interface_not_applicable);
			notifyResult(false);
			return;
		} else if (type.getCompilationUnit() == null) {
			MessageDialog.openInformation(getShell(), DIALOG_TITLE, ActionMessages.AddGetterSetterAction_error_not_in_source_file);
			notifyResult(false);
			return;
		}
		if (!ElementValidator.check(type, getShell(), DIALOG_TITLE, editor)) {
			notifyResult(false);
			return;
		}
		if (!ActionUtil.isEditable(getShell(), type)) {
			notifyResult(false);
			return;
		}

		ILabelProvider lp= new AddGetterSetterLabelProvider();
		resetNumEntries();
		Map<IField, GetterSetterEntry[]> entries= createGetterSetterMapping(type);
		if (entries.isEmpty()) {
			MessageDialog.openInformation(getShell(), DIALOG_TITLE, ActionMessages.AddGettSetterAction_typeContainsNoFields_message);
			notifyResult(false);
			return;
		}
		AddGetterSetterContentProvider cp= new AddGetterSetterContentProvider(entries);
		GetterSetterTreeSelectionDialog dialog= new GetterSetterTreeSelectionDialog(getShell(), lp, cp, fEditor, type);
		dialog.setComparator(new JavaElementComparator());
		dialog.setTitle(DIALOG_TITLE);
		String message= ActionMessages.AddGetterSetterAction_dialog_label;
		dialog.setMessage(message);
		dialog.setValidator(createValidator(fNumEntries));
		dialog.setContainerMode(true);
		dialog.setSize(60, 18);
		dialog.setInput(type);

		if (preselected.length > 0) {
			dialog.setInitialSelections((Object[]) preselected);
			dialog.setExpandedElements(preselected);
		}
		final Set<IField> keySet= new LinkedHashSet<>(entries.keySet());
		int dialogResult= dialog.open();
		if (dialogResult == Window.OK) {
			Object[] result= dialog.getResult();
			if (result == null) {
				notifyResult(false);
				return;
			}
			fSort= dialog.getSortOrder();
			fSynchronized= dialog.getSynchronized();
			fFinal= dialog.getFinal();
			fVisibility= dialog.getVisibilityModifier();
			fGenerateComment= dialog.getGenerateComment();
			IField[] getterFields, setterFields, getterSetterFields;
			if (fSort) {
				getterFields= getGetterFields(result, keySet);
				setterFields= getSetterFields(result, keySet);
				getterSetterFields= new IField[0];
			} else {
				getterFields= getGetterOnlyFields(result, keySet);
				setterFields= getSetterOnlyFields(result, keySet);
				getterSetterFields= getGetterSetterFields(result, keySet);
			}
			generate(type, getterFields, setterFields, getterSetterFields, new RefactoringASTParser(IASTSharedValues.SHARED_AST_LEVEL).parse(type.getCompilationUnit(), true), dialog.getElementPosition());
		}
		notifyResult(dialogResult == Window.OK);
	}

	private static class AddGetterSetterSelectionStatusValidator implements ISelectionStatusValidator {

		private static int fEntries;

		AddGetterSetterSelectionStatusValidator(int entries) {
			fEntries= entries;
		}

		@Override
		public IStatus validate(Object[] selection) {
			// https://bugs.eclipse.org/bugs/show_bug.cgi?id=38478
			HashSet<Object> map= null;
			if ((selection != null) && (selection.length > 1)) {
				map= new HashSet<>(selection.length);
			}

			int selectedCount= 0;
			int possibleDuplicateCount= 0;
			for (Object element : selection) {
				try {
					if (element instanceof GetterSetterEntry) {
						Object key= element;
						IField getsetField= ((GetterSetterEntry) element).field;
						if (((GetterSetterEntry) element).isGetter) {
							if (!map.add(GetterSetterUtil.getGetterName(getsetField, null)))
								possibleDuplicateCount++;
						} else {
							key= createSignatureKey(GetterSetterUtil.getSetterName(getsetField, null), getsetField);
							if (!map.add(key))
								possibleDuplicateCount++;
						}
						selectedCount++;
					}
				} catch (JavaModelException e) {
				}
			}

			if (possibleDuplicateCount > 0) {
				return new StatusInfo(IStatus.WARNING, possibleDuplicateCount == 1
						? ActionMessages.AddGetterSetterAction_error_duplicate_methods_singular
						: Messages.format(ActionMessages.AddGetterSetterAction_error_duplicate_methods_plural, String.valueOf(possibleDuplicateCount)));
			}
			if (selectedCount == 0)
				return new StatusInfo(IStatus.ERROR, ""); //$NON-NLS-1$
			String message= Messages.format(ActionMessages.AddGetterSetterAction_methods_selected,
					new Object[] { String.valueOf(selectedCount), String.valueOf(fEntries)});
			return new StatusInfo(IStatus.INFO, message);
		}
	}

	/**
	 * Creates a key used in hash maps for a method signature (gettersettername+arguments(fqn)).
	 *
	 * @param methodName the method name
	 * @param field the filed
	 * @return the signature
	 * @throws JavaModelException if getting the field's type signature fails
	 */
	private static String createSignatureKey(String methodName, IField field) throws JavaModelException {
		StringBuilder buffer= new StringBuilder();
		buffer.append(methodName);
		String fieldType= field.getTypeSignature();
		String signature= Signature.getSimpleName(Signature.toString(fieldType));
		buffer.append("#"); //$NON-NLS-1$
		buffer.append(signature);

		return buffer.toString();
	}

	private static ISelectionStatusValidator createValidator(int entries) {
		AddGetterSetterSelectionStatusValidator validator= new AddGetterSetterSelectionStatusValidator(entries);
		return validator;
	}

	// returns a list of fields with setter entries checked
	private static IField[] getSetterFields(Object[] result, Set<IField> set) {
		List<IField> list= new ArrayList<>(0);
		for (Object each : result) {
			if ((each instanceof GetterSetterEntry)) {
				GetterSetterEntry entry= (GetterSetterEntry) each;
				if (!entry.isGetter) {
					list.add(entry.field);
				}
			}
		}
		list= reorderFields(list, set);
		return list.toArray(new IField[list.size()]);
	}

	// returns a list of fields with getter entries checked
	private static IField[] getGetterFields(Object[] result, Set<IField> set) {
		List<IField> list= new ArrayList<>(0);
		for (Object each : result) {
			if ((each instanceof GetterSetterEntry)) {
				GetterSetterEntry entry= (GetterSetterEntry) each;
				if (entry.isGetter) {
					list.add(entry.field);
				}
			}
		}
		list= reorderFields(list, set);
		return list.toArray(new IField[list.size()]);
	}

	// returns a list of fields with only getter entries checked
	private static IField[] getGetterOnlyFields(Object[] result, Set<IField> set) {
		List<IField> list= new ArrayList<>(0);
		boolean getterSet= false;
		for (Object each : result) {
			if ((each instanceof GetterSetterEntry)) {
				GetterSetterEntry entry= (GetterSetterEntry) each;
				if (entry.isGetter) {
					list.add(entry.field);
					getterSet= true;
				}
				if ((!entry.isGetter) && (getterSet == true)) {
					list.remove(entry.field);
					getterSet= false;
				}
			} else
				getterSet= false;
		}
		list= reorderFields(list, set);
		return list.toArray(new IField[list.size()]);
	}

	// returns a list of fields with only setter entries checked
	private static IField[] getSetterOnlyFields(Object[] result, Set<IField> set) {
		List<IField> list= new ArrayList<>(0);
		boolean getterSet= false;
		for (Object each : result) {
			if ((each instanceof GetterSetterEntry)) {
				GetterSetterEntry entry= (GetterSetterEntry) each;
				if (entry.isGetter) {
					getterSet= true;
				}
				if ((!entry.isGetter) && (getterSet != true)) {
					list.add(entry.field);
					getterSet= false;
				}
			} else
				getterSet= false;
		}
		list= reorderFields(list, set);
		return list.toArray(new IField[list.size()]);
	}

	// returns a list of fields with both entries checked
	private static IField[] getGetterSetterFields(Object[] result, Set<IField> set) {
		List<IField> list= new ArrayList<>(0);
		boolean getterSet= false;
		for (Object each : result) {
			if ((each instanceof GetterSetterEntry)) {
				GetterSetterEntry entry= (GetterSetterEntry) each;
				if (entry.isGetter) {
					getterSet= true;
				}
				if ((!entry.isGetter) && (getterSet == true)) {
					list.add(entry.field);
					getterSet= false;
				}
			} else
				getterSet= false;
		}
		list= reorderFields(list, set);
		return list.toArray(new IField[list.size()]);
	}

	private static List<IField> reorderFields(List<IField> collection, Set<IField> set) {
		final List<IField> list= new ArrayList<>(collection.size());
		for (IField field : set) {
			if (collection.contains(field))
				list.add(field);
		}
		return list;
	}

	private void generate(IType type, IField[] getterFields, IField[] setterFields, IField[] getterSetterFields, CompilationUnit unit, IJavaElement elementPosition) throws CoreException {
		if (getterFields.length == 0 && setterFields.length == 0 && getterSetterFields.length == 0)
			return;

		ICompilationUnit cu= null;
		if (getterFields.length != 0)
			cu= getterFields[0].getCompilationUnit();
		else if (setterFields.length != 0)
			cu= setterFields[0].getCompilationUnit();
		else
			cu= getterSetterFields[0].getCompilationUnit();
		// open the editor, forces the creation of a working copy
		run(cu, type, getterFields, setterFields, getterSetterFields, JavaUI.openInEditor(cu), unit, elementPosition);
	}

	// ---- Java Editor --------------------------------------------------------------

	@Override
	public void selectionChanged(ITextSelection selection) {
	}

	@Override
	public void run(ITextSelection selection) {
		try {
			if (!ActionUtil.isProcessable(fEditor)) {
				notifyResult(false);
				return;
			}

			IJavaElement[] elements= SelectionConverter.codeResolveForked(fEditor, true);
			if (elements.length == 1 && (elements[0] instanceof IField)) {
				IField field= (IField) elements[0];
				run(field.getDeclaringType(), new IField[] { field}, true);
				return;
			}
			IJavaElement element= SelectionConverter.getElementAtOffset(fEditor);

			if (element != null) {
				IType type= (IType) element.getAncestor(IJavaElement.TYPE);
				if (type != null) {
					if (type.isRecord()) {
						if (type.getRecordComponents().length > 0) {
							run(type, new IField[0], true);
							return;
						}
					} else if (type.getFields().length > 0) {
						run(type, new IField[0], true);
						return;
					}
				}
			}
			MessageDialog.openInformation(getShell(), DIALOG_TITLE, ActionMessages.AddGetterSetterAction_not_applicable);
		} catch (CoreException e) {
			ExceptionHandler.handle(e, getShell(), DIALOG_TITLE, ActionMessages.AddGetterSetterAction_error_actionfailed);
		} catch (InvocationTargetException e) {
			ExceptionHandler.handle(e, getShell(), DIALOG_TITLE, ActionMessages.AddGetterSetterAction_error_actionfailed);
		} catch (InterruptedException e) {
			// cancelled
		}
	}

	// ---- Helpers -------------------------------------------------------------------

	private void run(ICompilationUnit cu, IType type, IField[] getterFields, IField[] setterFields, IField[] getterSetterFields, IEditorPart editor, CompilationUnit unit, IJavaElement elementPosition) {
		IRewriteTarget target= editor.getAdapter(IRewriteTarget.class);
		if (target != null) {
			target.beginCompoundChange();
		}
		try {
			CodeGenerationSettings settings= JavaPreferencesSettings.getCodeGenerationSettings(cu);
			settings.createComments= fGenerateComment;

			AddGetterSetterOperation op= new AddGetterSetterOperation(type, getterFields, setterFields, getterSetterFields, unit, skipReplaceQuery(), elementPosition, settings, true, false);
			setOperationStatusFields(op);

			IRunnableContext context= JavaPlugin.getActiveWorkbenchWindow();
			if (context == null) {
				context= new BusyIndicatorRunnableContext();
			}

			PlatformUI.getWorkbench().getProgressService().runInUI(context, new WorkbenchRunnableAdapter(op, op.getSchedulingRule()), op.getSchedulingRule());

		} catch (InvocationTargetException e) {
			String message= ActionMessages.AddGetterSetterAction_error_actionfailed;
			ExceptionHandler.handle(e, getShell(), DIALOG_TITLE, message);
		} catch (InterruptedException e) {
			// operation canceled
		} finally {
			if (target != null) {
				target.endCompoundChange();
			}
		}
	}

	private void setOperationStatusFields(AddGetterSetterOperation op) {
		// Set the status fields corresponding to the visibility and modifiers set
		int flags= fVisibility;
		if (fSynchronized) {
			flags|= Flags.AccSynchronized;
		}
		if (fFinal) {
			flags|= Flags.AccFinal;
		}
		op.setSort(fSort);
		op.setVisibility(flags);
	}

	private IRequestQuery skipReplaceQuery() {
		return method -> {
			int[] returnCodes= { IRequestQuery.YES, IRequestQuery.NO, IRequestQuery.YES_ALL, IRequestQuery.CANCEL};
			String skipLabel= ActionMessages.AddGetterSetterAction_SkipExistingDialog_skip_label;
			String replaceLabel= ActionMessages.AddGetterSetterAction_SkipExistingDialog_replace_label;
			String skipAllLabel= ActionMessages.AddGetterSetterAction_SkipExistingDialog_skipAll_label;
			String[] options= { skipLabel, replaceLabel, skipAllLabel, IDialogConstants.CANCEL_LABEL};
			String methodName= JavaElementLabels.getElementLabel(method, JavaElementLabels.M_PARAMETER_TYPES);
			String formattedMessage= Messages.format(ActionMessages.AddGetterSetterAction_SkipExistingDialog_message, BasicElementLabels.getJavaElementName(methodName));
			return showQueryDialog(formattedMessage, options, returnCodes);
		};
	}

	private int showQueryDialog(final String message, final String[] buttonLabels, int[] returnCodes) {
		final Shell shell= getShell();
		if (shell == null) {
			JavaPlugin.logErrorMessage("AddGetterSetterAction.showQueryDialog: No active shell found"); //$NON-NLS-1$
			return IRequestQuery.CANCEL;
		}
		final int[] result= { Window.CANCEL};
		shell.getDisplay().syncExec(() -> {
			String title= ActionMessages.AddGetterSetterAction_QueryDialog_title;
			MessageDialog dialog= new MessageDialog(shell, title, null, message, MessageDialog.QUESTION, buttonLabels, 0);
			result[0]= dialog.open();
		});
		int returnVal= result[0];
		return returnVal < 0 ? IRequestQuery.CANCEL : returnCodes[returnVal];
	}

	/*
	 * Returns fields in the selection or <code>null</code> if the selection is empty or
	 * not valid.
	 */
	private IField[] getSelectedFields(IStructuredSelection selection) {
		List<?> elements= selection.toList();
		int nElements= elements.size();
		if (nElements > 0) {
			IField[] res= new IField[nElements];
			ICompilationUnit cu= null;
			for (int i= 0; i < nElements; i++) {
				Object curr= elements.get(i);
				if (curr instanceof IField) {
					IField fld= (IField) curr;

					if (i == 0) {
						// remember the cu of the first element
						cu= fld.getCompilationUnit();
						if (cu == null) {
							return null;
						}
					} else if (!cu.equals(fld.getCompilationUnit())) {
						// all fields must be in the same CU
						return null;
					}
					try {
						final IType declaringType= fld.getDeclaringType();
						if (declaringType.isInterface() || declaringType.isAnonymous())
							return null;
					} catch (JavaModelException e) {
						JavaPlugin.log(e);
						return null;
					}

					res[i]= fld;
				} else {
					return null;
				}
			}
			return res;
		}
		return null;
	}

	private static class AddGetterSetterLabelProvider extends JavaElementLabelProvider {

		AddGetterSetterLabelProvider() {
		}

		/*
		 * @see ILabelProvider#getText(Object)
		 */
		@Override
		public String getText(Object element) {
			if (element instanceof GetterSetterEntry) {
				GetterSetterEntry entry= (GetterSetterEntry) element;
				try {
					if (entry.isGetter) {
						return GetterSetterUtil.getGetterName(entry.field, null) + "()"; //$NON-NLS-1$
					} else {
						return GetterSetterUtil.getSetterName(entry.field, null) + '(' + Signature.getSimpleName(Signature.toString(entry.field.getTypeSignature())) + ')';
					}
				} catch (JavaModelException e) {
					return ""; //$NON-NLS-1$
				}
			}
			return super.getText(element);
		}

		/*
		 * @see ILabelProvider#getImage(Object)
		 */
		@Override
		public Image getImage(Object element) {
			if (element instanceof GetterSetterEntry) {
				int flags= 0;
				try {
					flags= ((GetterSetterEntry) element).field.getFlags();
				} catch (JavaModelException e) {
					JavaPlugin.log(e);
				}
				ImageDescriptor desc= JavaElementImageProvider.getFieldImageDescriptor(false, Flags.AccPublic);
				int adornmentFlags= Flags.isStatic(flags) ? JavaElementImageDescriptor.STATIC : 0;
				desc= new JavaElementImageDescriptor(desc, adornmentFlags, JavaElementImageProvider.BIG_SIZE);
				return JavaPlugin.getImageDescriptorRegistry().get(desc);
			}
			return super.getImage(element);
		}
	}

	/**
	 * @param type the type
	 * @return map IField -> GetterSetterEntry[]
	 * @throws JavaModelException if the type does not exist or if an exception occurs while
	 *             accessing its corresponding resource
	 */
	private Map<IField, GetterSetterEntry[]> createGetterSetterMapping(IType type) throws JavaModelException {
		Map<IField, GetterSetterEntry[]> result= new LinkedHashMap<>();
		for (IField field : type.getFields()) {
			int flags= field.getFlags();
			if (!Flags.isEnum(flags)) {
				List<GetterSetterEntry> l= new ArrayList<>(2);
				if (GetterSetterUtil.getGetter(field) == null) {
					l.add(new GetterSetterEntry(field, true, Flags.isFinal(flags)));
					incNumEntries();
				}

				if (GetterSetterUtil.getSetter(field) == null) {
					l.add(new GetterSetterEntry(field, false, Flags.isFinal(flags)));
					incNumEntries();
				}

				if (!l.isEmpty())
					result.put(field, l.toArray(new GetterSetterEntry[l.size()]));
			}
		}
		if (type.isRecord()) {
			for (IField field : type.getRecordComponents()) {
				int flags= field.getFlags();
				if (!Flags.isEnum(flags)) {
					List<GetterSetterEntry> l= new ArrayList<>(1);
					if (GetterSetterUtil.getGetter(field) == null) {
						l.add(new GetterSetterEntry(field, true, Flags.isFinal(flags)));
						incNumEntries();
					}

					if (!l.isEmpty())
						result.put(field, l.toArray(new GetterSetterEntry[l.size()]));
				}
			}
		}
		return result;
	}

	private static class AddGetterSetterContentProvider implements ITreeContentProvider {

		private static final Object[] EMPTY= new Object[0];

		private Map<IField, GetterSetterEntry[]> fGetterSetterEntries;

		public AddGetterSetterContentProvider(Map<IField, GetterSetterEntry[]> entries) {
			fGetterSetterEntries= entries;
		}

		/*
		 * @see IContentProvider#inputChanged(Viewer, Object, Object)
		 */
		@Override
		public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
		}

		/*
		 * @see ITreeContentProvider#getChildren(Object)
		 */
		@Override
		public Object[] getChildren(Object parentElement) {
			if (parentElement instanceof IField)
				return fGetterSetterEntries.get(parentElement);
			return EMPTY;
		}

		/*
		 * @see ITreeContentProvider#getParent(Object)
		 */
		@Override
		public Object getParent(Object element) {
			if (element instanceof IMember)
				return ((IMember) element).getDeclaringType();
			if (element instanceof GetterSetterEntry)
				return ((GetterSetterEntry) element).field;
			return null;
		}

		/*
		 * @see ITreeContentProvider#hasChildren(Object)
		 */
		@Override
		public boolean hasChildren(Object element) {
			return getChildren(element).length > 0;
		}

		/*
		 * @see IStructuredContentProvider#getElements(Object)
		 */
		@Override
		public Object[] getElements(Object inputElement) {
			return fGetterSetterEntries.keySet().toArray();
		}

		/*
		 * @see IContentProvider#dispose()
		 */
		@Override
		public void dispose() {
			fGetterSetterEntries.clear();
			fGetterSetterEntries= null;
		}
	}

	private static class SettersForFinalFieldsFilter extends ViewerFilter {

		private final AddGetterSetterContentProvider fContentProvider;

		public SettersForFinalFieldsFilter(AddGetterSetterContentProvider contentProvider) {
			fContentProvider= contentProvider;
		}

		@Override
		public boolean select(Viewer viewer, Object parentElement, Object element) {
			if (element instanceof GetterSetterEntry) {
				GetterSetterEntry getterSetterEntry= (GetterSetterEntry) element;
				return getterSetterEntry.isGetter || !getterSetterEntry.isFinal;
			} else if (element instanceof IField) {
				for (Object child : fContentProvider.getChildren(element)) {
					GetterSetterEntry curr= (GetterSetterEntry) child;
					if (curr.isGetter || !curr.isFinal) {
						return true;
					}
				}
				return false;
			}
			return true;
		}
	}


	private static class GetterSetterTreeSelectionDialog extends SourceActionDialog {

		private AddGetterSetterContentProvider fContentProvider;

		private static final int SELECT_GETTERS_ID= IDialogConstants.CLIENT_ID + 1;
		private static final int SELECT_SETTERS_ID= IDialogConstants.CLIENT_ID + 2;
		private static final String SETTINGS_SECTION= "AddGetterSetterDialog"; //$NON-NLS-1$
		private static final String SORT_ORDER= "SortOrdering"; //$NON-NLS-1$
		private static final String ALLOW_SETTERS_FOR_FINALS= "RemoveFinal"; //$NON-NLS-1$

		private IDialogSettings fSettings;
		private SettersForFinalFieldsFilter fSettersForFinalFieldsFilter;

		private boolean fSortOrder;
		private boolean fAllowSettersForFinals;

		private ArrayList<GetterSetterEntry> fPreviousSelectedFinals;


		public GetterSetterTreeSelectionDialog(Shell parent, ILabelProvider labelProvider, AddGetterSetterContentProvider contentProvider, CompilationUnitEditor editor, IType type) throws JavaModelException {
			super(parent, labelProvider, contentProvider, editor, type, false);
			fContentProvider= contentProvider;
			fPreviousSelectedFinals= new ArrayList<>();

			// http://bugs.eclipse.org/bugs/show_bug.cgi?id=19253
			IDialogSettings dialogSettings= JavaPlugin.getDefault().getDialogSettings();
			fSettings= dialogSettings.getSection(SETTINGS_SECTION);
			if (fSettings == null) {
				fSettings= dialogSettings.addNewSection(SETTINGS_SECTION);
				fSettings.put(SORT_ORDER, false);
				fSettings.put(ALLOW_SETTERS_FOR_FINALS, false);
			}

			fSortOrder= fSettings.getBoolean(SORT_ORDER);
			fAllowSettersForFinals= fSettings.getBoolean(ALLOW_SETTERS_FOR_FINALS);

			fSettersForFinalFieldsFilter= new SettersForFinalFieldsFilter(contentProvider);
		}

		public boolean getSortOrder() {
			return fSortOrder;
		}

		public void setSortOrder(boolean sort) {
			if (fSortOrder != sort) {
				fSortOrder= sort;
				fSettings.put(SORT_ORDER, sort);
				if (getTreeViewer() != null) {
					getTreeViewer().refresh();
				}
			}
		}

		private boolean allowSettersForFinals() {
			return fAllowSettersForFinals;
		}

		@Override
		protected Button createButton(Composite parent, int id, String label, boolean defaultButton) {
			if(id == IDialogConstants.OK_ID) {
				return super.createButton(parent, id, ActionMessages.GetterSetterTreeSelectionDialog_generate_button_label, defaultButton);
			} else {
				return super.createButton(parent, id, label, defaultButton);
			}
		}

		public void allowSettersForFinals(boolean allowSettersForFinals) {
			if (fAllowSettersForFinals != allowSettersForFinals) {
				fAllowSettersForFinals= allowSettersForFinals;
				fSettings.put(ALLOW_SETTERS_FOR_FINALS, allowSettersForFinals);
				CheckboxTreeViewer treeViewer= getTreeViewer();
				if (treeViewer != null) {
					ArrayList<GetterSetterEntry> newChecked= new ArrayList<>();
					if (allowSettersForFinals) {
						newChecked.addAll(fPreviousSelectedFinals);
					}
					fPreviousSelectedFinals.clear();
					for (Object checkedElement : treeViewer.getCheckedElements()) {
						if (checkedElement instanceof GetterSetterEntry) {
							GetterSetterEntry entry= (GetterSetterEntry) checkedElement;
							if (allowSettersForFinals || entry.isGetter || !entry.isFinal) {
								newChecked.add(entry);
							} else {
								fPreviousSelectedFinals.add(entry);
							}
						}
					}
					if (allowSettersForFinals) {
						treeViewer.removeFilter(fSettersForFinalFieldsFilter);
					} else {
						treeViewer.addFilter(fSettersForFinalFieldsFilter);
					}
					treeViewer.setCheckedElements(newChecked.toArray());
				}
				updateOKStatus();
			}
		}

		@Override
		protected CheckboxTreeViewer createTreeViewer(Composite parent) {
			CheckboxTreeViewer treeViewer= super.createTreeViewer(parent);
			if (!fAllowSettersForFinals) {
				treeViewer.addFilter(fSettersForFinalFieldsFilter);
			}
			return treeViewer;
		}

		@Override
		protected void configureShell(Shell shell) {
			super.configureShell(shell);
			PlatformUI.getWorkbench().getHelpSystem().setHelp(shell, IJavaHelpContextIds.ADD_GETTER_SETTER_SELECTION_DIALOG);
		}

		private void createGetterSetterButtons(Composite buttonComposite) {
			createButton(buttonComposite, SELECT_GETTERS_ID, ActionMessages.GetterSetterTreeSelectionDialog_select_getters, false);
			createButton(buttonComposite, SELECT_SETTERS_ID, ActionMessages.GetterSetterTreeSelectionDialog_select_setters, false);
		}

		@Override
		protected void buttonPressed(int buttonId) {
			super.buttonPressed(buttonId);
			switch (buttonId) {
				case SELECT_GETTERS_ID: {
					getTreeViewer().setCheckedElements(getGetterSetterElements(true));
					updateOKStatus();
					break;
				}
				case SELECT_SETTERS_ID: {
					getTreeViewer().setCheckedElements(getGetterSetterElements(false));
					updateOKStatus();
					break;
				}
			}
		}

		@Override
		protected Composite createInsertPositionCombo(Composite composite) {
			Button addRemoveFinalCheckbox= addAllowSettersForFinalslCheckbox(composite);
			addRemoveFinalCheckbox.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));

			Composite entryComposite= super.createInsertPositionCombo(composite);
			addSortOrder(entryComposite);
			addVisibilityAndModifiersChoices(entryComposite);
			return entryComposite;
		}

		private Button addAllowSettersForFinalslCheckbox(Composite entryComposite) {
			Button allowSettersForFinalsButton= new Button(entryComposite, SWT.CHECK);
			allowSettersForFinalsButton.setText(ActionMessages.AddGetterSetterAction_allow_setters_for_finals_description);

			allowSettersForFinalsButton.addSelectionListener(new SelectionListener() {
				@Override
				public void widgetSelected(SelectionEvent e) {
					boolean isSelected= (((Button) e.widget).getSelection());
					allowSettersForFinals(isSelected);
				}

				@Override
				public void widgetDefaultSelected(SelectionEvent e) {
					widgetSelected(e);
				}
			});
			allowSettersForFinalsButton.setSelection(allowSettersForFinals());
			return allowSettersForFinalsButton;
		}

		private Composite addSortOrder(Composite composite) {
			Label label= new Label(composite, SWT.NONE);
			label.setText(ActionMessages.GetterSetterTreeSelectionDialog_sort_label);
			GridData gd= new GridData(GridData.FILL_BOTH);
			label.setLayoutData(gd);

			final Combo combo= new Combo(composite, SWT.READ_ONLY);
			combo.setItems(ActionMessages.GetterSetterTreeSelectionDialog_alpha_pair_sort, ActionMessages.GetterSetterTreeSelectionDialog_alpha_method_sort);
			final int methodIndex= 1; // Hard-coded. Change this if the
			// list gets more complicated.
			// http://bugs.eclipse.org/bugs/show_bug.cgi?id=38400
			int sort= getSortOrder() ? 1 : 0;
			combo.setText(combo.getItem(sort));
			gd= new GridData(GridData.FILL_BOTH);
			combo.setLayoutData(gd);
			combo.addSelectionListener(new SelectionAdapter() {

				@Override
				public void widgetSelected(SelectionEvent e) {
					setSortOrder(combo.getSelectionIndex() == methodIndex);
				}
			});
			return composite;
		}

		private Object[] getGetterSetterElements(boolean isGetter) {
			Set<GetterSetterEntry> result= new HashSet<>();
			for (Object allField : fContentProvider.getElements(null)) {
				for (GetterSetterEntry entry : getEntries((IField) allField)) {
					if (entry.isGetter == isGetter)
						result.add(entry);
				}
			}
			return result.toArray();
		}

		private GetterSetterEntry[] getEntries(IField field) {
			List<Object> result= Arrays.asList(fContentProvider.getChildren(field));
			return result.toArray(new GetterSetterEntry[result.size()]);
		}

		@Override
		protected Composite createSelectionButtons(Composite composite) {
			Composite buttonComposite= super.createSelectionButtons(composite);

			GridLayout layout= new GridLayout();
			buttonComposite.setLayout(layout);

			createGetterSetterButtons(buttonComposite);

			layout.marginHeight= 0;
			layout.marginWidth= 0;
			layout.numColumns= 1;

			return buttonComposite;
		}

		/*
		 * @see org.eclipse.jdt.internal.ui.dialogs.SourceActionDialog#createLinkControl(org.eclipse.swt.widgets.Composite)
		 */
		@Override
		protected Control createLinkControl(Composite composite) {
			Link link= new Link(composite, SWT.WRAP);
			link.setText(ActionMessages.AddGetterSetterAction_template_link_description);
			link.addSelectionListener(new SelectionAdapter() {
				@Override
				public void widgetSelected(SelectionEvent e) {
					openCodeTempatePage(CodeTemplateContextType.GETTERCOMMENT_ID);
				}
			});
			link.setToolTipText(ActionMessages.AddGetterSetterAction_template_link_tooltip);

			GridData gridData= new GridData(SWT.FILL, SWT.BEGINNING, true, false);
			gridData.widthHint= convertWidthInCharsToPixels(40); // only expand further if anyone else requires it
			link.setLayoutData(gridData);
			return link;
		}
	}

	private static class GetterSetterEntry {
		public final IField field;
		public final boolean isGetter;
		public final boolean isFinal;

		GetterSetterEntry(IField field, boolean isGetterEntry, boolean isFinal) {
			this.field= field;
			this.isGetter= isGetterEntry;
			this.isFinal= isFinal;
		}
	}
}
