/*******************************************************************************
 * Copyright (c) 2000, 2018 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:
 *   Martin Moebius (m.moebius@gmx.de) - initial API and implementation
 *             (report 28793)
 *   IBM Corporation - updates
 *******************************************************************************/

package org.eclipse.jdt.ui.actions;

import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;

import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
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.core.runtime.NullProgressMonitor;

import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.operation.IRunnableContext;
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.ViewerComparator;
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.ICompilationUnit;
import org.eclipse.jdt.core.IField;
import org.eclipse.jdt.core.IJavaElement;
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.core.dom.IMethodBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.IVariableBinding;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
import org.eclipse.jdt.core.manipulation.SharedASTProviderCore;

import org.eclipse.jdt.internal.core.manipulation.CodeTemplateContextType;
import org.eclipse.jdt.internal.corext.codemanipulation.AddDelegateMethodsOperation;
import org.eclipse.jdt.internal.corext.codemanipulation.AddDelegateMethodsOperation.DelegateEntry;
import org.eclipse.jdt.internal.corext.codemanipulation.CodeGenerationSettings;
import org.eclipse.jdt.internal.corext.codemanipulation.StubUtility2Core;
import org.eclipse.jdt.internal.corext.dom.ASTNodes;
import org.eclipse.jdt.internal.corext.refactoring.structure.ASTNodeSearchUtil;
import org.eclipse.jdt.internal.corext.refactoring.util.JavaElementUtil;
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.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.BindingLabelProvider;

/**
 * Creates delegate methods for a type's fields. Opens a dialog with a list of fields for
 * which delegate methods can be generated. User is able to check or uncheck items before
 * methods 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.1
 *
 * @noextend This class is not intended to be subclassed by clients.
 */
public class AddDelegateMethodsAction extends SelectionDispatchAction {

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

	private static class AddDelegateMethodsActionStatusValidator implements ISelectionStatusValidator {

		private static int fEntries;

		AddDelegateMethodsActionStatusValidator(int entries) {
			fEntries= entries;
		}

		@Override
		public IStatus validate(Object[] selection) {
			int selectedCount= 0;
			int duplicateCount= 0;
			if (selection != null && selection.length > 0) {

				HashSet<String> signatures= new HashSet<>(selection.length);
				for (Object element : selection) {
					if (element instanceof DelegateEntry) {
						DelegateEntry delegateEntry = (DelegateEntry) element;
						if (!signatures.add(getSignature(delegateEntry.delegateMethod)))
							duplicateCount++;
						selectedCount++;
					}
				}
			}
			if (duplicateCount > 0) {
				return new StatusInfo(IStatus.ERROR, duplicateCount == 1
						? ActionMessages.AddDelegateMethodsAction_duplicate_methods_singular
						: Messages.format(ActionMessages.AddDelegateMethodsAction_duplicate_methods_plural, String.valueOf(duplicateCount)));
			}
			return new StatusInfo(IStatus.INFO, Messages.format(ActionMessages.AddDelegateMethodsAction_selectioninfo_more, new Object[] { String.valueOf(selectedCount), String.valueOf(fEntries) }));
		}

		private String getSignature(IMethodBinding binding) {
			StringBuilder buf= new StringBuilder(binding.getName()).append('(');
			for (ITypeBinding parameterType : binding.getParameterTypes()) {
				buf.append(parameterType.getTypeDeclaration().getName());
			}
			buf.append(')');
			return buf.toString();
		}

	}

	private static class AddDelegateMethodsContentProvider implements ITreeContentProvider {

		private DelegateEntry[] fDelegateEntries;
		private IVariableBinding[] fExpanded= new IVariableBinding[0];

		AddDelegateMethodsContentProvider(CompilationUnit astRoot, IType type, IField[] fields) throws JavaModelException {

			final ITypeBinding binding= ASTNodes.getTypeBinding(astRoot, type);
			if (binding != null) {
				fDelegateEntries= StubUtility2Core.getDelegatableMethods(binding);

				List<IVariableBinding> expanded= new ArrayList<>();
				for (IField field : fields) {
					VariableDeclarationFragment fragment= ASTNodeSearchUtil.getFieldDeclarationFragmentNode(field, astRoot);
					if (fragment != null) {
						IVariableBinding variableBinding= fragment.resolveBinding();
						if (variableBinding != null)
							expanded.add(variableBinding);
					}
				}
				fExpanded= expanded.toArray(new IVariableBinding[expanded.size()]);
			}
		}

		@Override
		public void dispose() {
		}

		@Override
		public Object[] getChildren(Object element) {
			if (element instanceof IVariableBinding) {
				List<DelegateEntry> result= new ArrayList<>();
				for (DelegateEntry delegateEntry : fDelegateEntries) {
					if (element == delegateEntry.field) {
						result.add(delegateEntry);
					}
				}
				return result.toArray();
			}
			return null;
		}

		public int getCount() {
			return fDelegateEntries.length;
		}

		@Override
		public Object[] getElements(Object inputElement) {
			HashSet<IVariableBinding> result= new HashSet<>();
			for (DelegateEntry curr : fDelegateEntries) {
				result.add(curr.field);
			}
			return result.toArray();
		}

		public IVariableBinding[] getExpandedElements() {
			return fExpanded;
		}

		public IVariableBinding[] getInitiallySelectedElements() {
			return fExpanded;
		}

		@Override
		public Object getParent(Object element) {
			if (element instanceof DelegateEntry)
				return ((DelegateEntry) element).field;
			return null;
		}

		@Override
		public boolean hasChildren(Object element) {
			return element instanceof IVariableBinding;
		}

		@Override
		public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
		}
	}

	private static class AddDelegateMethodsDialog extends SourceActionDialog {

		public AddDelegateMethodsDialog(Shell parent, ILabelProvider labelProvider, ITreeContentProvider contentProvider, CompilationUnitEditor editor, IType type, boolean isConstructor) throws JavaModelException {
			super(parent, labelProvider, contentProvider, editor, type, isConstructor);
		}

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

		@Override
		protected void createButtonsForButtonBar(Composite parent) {
			createButton(parent, IDialogConstants.OK_ID,ActionMessages.AddDelegateMethodsAction_generate_button , true);
			createButton(parent, IDialogConstants.CANCEL_ID, IDialogConstants.CANCEL_LABEL, false);

		}

		@Override
		protected Control createLinkControl(Composite composite) {
			Link link= new Link(composite, SWT.WRAP);
			link.setText(ActionMessages.AddDelegateMethodsAction_template_link_message);
			link.addSelectionListener(new SelectionAdapter() {
				@Override
				public void widgetSelected(SelectionEvent e) {
					openCodeTempatePage(CodeTemplateContextType.OVERRIDECOMMENT_ID);
				}
			});
			link.setToolTipText(ActionMessages.AddDelegateMethodsAction_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 AddDelegateMethodsLabelProvider extends BindingLabelProvider {

		@Override
		public Image getImage(Object element) {
			if (element instanceof DelegateEntry) {
				DelegateEntry delegateEntry= (DelegateEntry) element;
				return super.getImage(delegateEntry.delegateMethod);
			} else if (element instanceof IVariableBinding) {
				return super.getImage(element);
			}
			return null;
		}

		@Override
		public String getText(Object element) {
			if (element instanceof DelegateEntry) {
				DelegateEntry delegateEntry= (DelegateEntry) element;
				return super.getText(delegateEntry.delegateMethod);
			} else if (element instanceof IVariableBinding) {
				return super.getText(element);
			}
			return null;
		}
	}

	private static class AddDelegateMethodsViewerComparator extends ViewerComparator {

		@Override
		public int category(Object element) {
			if (element instanceof DelegateEntry)
				return 0;
			return 1;
		}

		@Override
		public int compare(Viewer viewer, Object o1, Object o2) {
			if (o1 instanceof DelegateEntry && o2 instanceof DelegateEntry) {
				String bindingLabel1= BindingLabelProvider.getBindingLabel(((DelegateEntry) o1).delegateMethod, BindingLabelProvider.DEFAULT_TEXTFLAGS);
				String bindingLabel2= BindingLabelProvider.getBindingLabel(((DelegateEntry) o2).delegateMethod, BindingLabelProvider.DEFAULT_TEXTFLAGS);
				return getComparator().compare(bindingLabel1, bindingLabel2);
			} else if (o1 instanceof IVariableBinding && o2 instanceof IVariableBinding) {
				return getComparator().compare(((IVariableBinding) o1).getName(), ((IVariableBinding) o2).getName());
			}
			return 0;
		}
	}

	private static final String DIALOG_TITLE= ActionMessages.AddDelegateMethodsAction_error_title;

	private static boolean hasPrimitiveType(IField field) throws JavaModelException {
		String signature= field.getTypeSignature();
		char first= Signature.getElementType(signature).charAt(0);
		return (first != Signature.C_RESOLVED && first != Signature.C_UNRESOLVED);
	}

	private static boolean isArray(IField field) throws JavaModelException {
		return Signature.getArrayCount(field.getTypeSignature()) > 0;
	}

	private CompilationUnitEditor fEditor;

	/**
	 * 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 AddDelegateMethodsAction(CompilationUnitEditor editor) {
		this(editor.getEditorSite());
		fEditor= editor;
		setEnabled(SelectionConverter.getInputAsCompilationUnit(editor) != null);
	}

	/**
	 * Creates a new <code>AddDelegateMethodsAction</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 AddDelegateMethodsAction(IWorkbenchSite site) {
		super(site);
		setText(ActionMessages.AddDelegateMethodsAction_label);
		setDescription(ActionMessages.AddDelegateMethodsAction_description);
		setToolTipText(ActionMessages.AddDelegateMethodsAction_tooltip);

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

	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) && !hasPrimitiveType(field) || isArray(field)) {
				count++;
			}
		}
		if (count == 0)
			MessageDialog.openInformation(getShell(), DIALOG_TITLE, ActionMessages.AddDelegateMethodsAction_not_applicable);
		return (count > 0);
	}

	private boolean canRunOn(IType type) throws JavaModelException {
		if (type == null || type.getCompilationUnit() == null) {
			MessageDialog.openInformation(getShell(), DIALOG_TITLE, ActionMessages.AddDelegateMethodsAction_not_in_source_file);
			return false;
		} else if (type.isAnnotation()) {
			MessageDialog.openInformation(getShell(), DIALOG_TITLE, ActionMessages.AddDelegateMethodsAction_annotation_not_applicable);
			return false;
		} else if (type.isInterface()) {
			MessageDialog.openInformation(getShell(), DIALOG_TITLE, ActionMessages.AddDelegateMethodsAction_interface_not_applicable);
			return false;
		} else if (type.isRecord()) {
			return (canRunOn(type.getRecordComponents()) || canRunOn(type.getFields()));
		}
		return canRunOn(type.getFields());
	}

	private IField[] getSelectedFields(IStructuredSelection selection) {
		List<?> elements= selection.toList();
		if (elements.size() > 0) {
			IField[] result= new IField[elements.size()];
			ICompilationUnit unit= null;
			for (int index= 0; index < elements.size(); index++) {
				if (elements.get(index) instanceof IField) {
					IField field= (IField) elements.get(index);

					if (index == 0) {
						// remember the CU of the first element
						unit= field.getCompilationUnit();
						if (unit == null) {
							return null;
						}
					} else if (!unit.equals(field.getCompilationUnit())) {
						// all fields must be in the same CU
						return null;
					}
					try {
						final IType type= field.getDeclaringType();
						if (type.isInterface() || type.isAnonymous()) {
							return null;
						}
					} catch (JavaModelException exception) {
						JavaPlugin.log(exception);
						return null;
					}

					result[index]= field;
				} else {
					return null;
				}
			}
			return result;
		}
		return null;
	}

	@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)
				run(JavaElementUtil.getMainType((ICompilationUnit) firstElement), new IField[0], false);
			else if (!(firstElement instanceof IField))
				MessageDialog.openInformation(getShell(), DIALOG_TITLE, ActionMessages.AddDelegateMethodsAction_not_applicable);
		} catch (CoreException e) {
			ExceptionHandler.handle(e, getShell(), DIALOG_TITLE, ActionMessages.AddDelegateMethodsAction_error_actionfailed);
		}

	}

	@Override
	public void run(ITextSelection selection) {
		try {
			if (!ActionUtil.isProcessable(fEditor))
				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.getFields().length > 0) {
						run(type, new IField[0], true);
						return;
					}
				}
			}
			MessageDialog.openInformation(getShell(), DIALOG_TITLE, ActionMessages.AddDelegateMethodsAction_not_applicable);
		} catch (CoreException e) {
			ExceptionHandler.handle(e, getShell(), DIALOG_TITLE, ActionMessages.AddDelegateMethodsAction_error_actionfailed);
		} catch (InvocationTargetException e) {
			ExceptionHandler.handle(e, getShell(), DIALOG_TITLE, ActionMessages.AddDelegateMethodsAction_error_actionfailed);
		} catch (InterruptedException e) {
			// cancelled
		}
	}

	private void run(IType type, IField[] preselected, boolean editor) throws CoreException {
		if (!ElementValidator.check(type, getShell(), DIALOG_TITLE, editor))
			return;
		if (!ActionUtil.isEditable(fEditor, getShell(), type))
			return;
		if (!canRunOn(type))
			return;
		showUI(type, preselected);
	}

	// ---- 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);
		}
	}

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

	@Override
	public void selectionChanged(ITextSelection selection) {
	}

	private void showUI(IType type, IField[] fields) {
		try {
			CompilationUnit astRoot= SharedASTProviderCore.getAST(type.getCompilationUnit(), SharedASTProviderCore.WAIT_YES, new NullProgressMonitor());

			AddDelegateMethodsContentProvider provider= new AddDelegateMethodsContentProvider(astRoot, type, fields);
			SourceActionDialog dialog= new AddDelegateMethodsDialog(getShell(), new AddDelegateMethodsLabelProvider(), provider, fEditor, type, false);
			dialog.setValidator(new AddDelegateMethodsActionStatusValidator(provider.getCount()));
			AddDelegateMethodsViewerComparator comparator= new AddDelegateMethodsViewerComparator();
			dialog.setComparator(comparator);
			dialog.setInput(new Object());
			dialog.setContainerMode(true);
			dialog.setMessage(ActionMessages.AddDelegateMethodsAction_message);
			dialog.setTitle(ActionMessages.AddDelegateMethodsAction_title);
			IVariableBinding[] expanded= provider.getExpandedElements();
			if (expanded.length > 0) {
				dialog.setExpandedElements(expanded);
			} else {
				Object[] elements= provider.getElements(null);
				if (elements.length > 0) {
					comparator.sort(null, elements);
					Object[] expand= { elements[0]};
					dialog.setExpandedElements(expand);
				}
			}
			dialog.setInitialSelections((Object[]) provider.getInitiallySelectedElements());
			dialog.setSize(60, 18);
			int result= dialog.open();
			if (result == Window.OK) {
				Object[] objectarray= dialog.getResult();
				if (objectarray == null) {
					notifyResult(false);
					return;
				}
				List<DelegateEntry> tuples= new ArrayList<>(objectarray.length);
				for (Object object : objectarray) {
					if (object instanceof DelegateEntry) {
						tuples.add((DelegateEntry) object);
					}
				}
				IEditorPart part= JavaUI.openInEditor(type);
				IRewriteTarget target= part.getAdapter(IRewriteTarget.class);
				try {
					if (target != null)
						target.beginCompoundChange();
					CodeGenerationSettings settings= JavaPreferencesSettings.getCodeGenerationSettings(type.getJavaProject());
					settings.createComments= dialog.getGenerateComment();

					DelegateEntry[] methodToDelegate= tuples.toArray(new DelegateEntry[tuples.size()]);

					AddDelegateMethodsOperation operation= new AddDelegateMethodsOperation(astRoot, methodToDelegate, dialog.getElementPosition(), settings, true, false);
					IRunnableContext context= JavaPlugin.getActiveWorkbenchWindow();
					if (context == null)
						context= new BusyIndicatorRunnableContext();
					try {
						PlatformUI.getWorkbench().getProgressService().runInUI(context, new WorkbenchRunnableAdapter(operation, operation.getSchedulingRule()), operation.getSchedulingRule());
					} catch (InterruptedException exception) {
						// User interruption
					}
				} finally {
					if (target != null)
						target.endCompoundChange();
				}
			}
			notifyResult(result == Window.OK);
		} catch (CoreException exception) {
			ExceptionHandler.handle(exception, DIALOG_TITLE, ActionMessages.AddDelegateMethodsAction_error_actionfailed);
		} catch (InvocationTargetException e) {
			ExceptionHandler.handle(e, DIALOG_TITLE, ActionMessages.AddDelegateMethodsAction_error_actionfailed);
		}
	}
}
