/*******************************************************************************
 * Copyright (c) 2000, 2020 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:
 *		Andrew McCullough - initial API and implementation
 *		IBM Corporation  - general improvement and bug fixes, partial reimplementation
 *******************************************************************************/
package org.eclipse.jdt.internal.ui.text.java;

import org.eclipse.swt.SWT;
import org.eclipse.swt.events.VerifyEvent;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.widgets.Shell;

import org.eclipse.jface.dialogs.MessageDialog;

import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.BadPositionCategoryException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IPositionUpdater;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITypedRegion;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.text.Region;
import org.eclipse.jface.text.TextUtilities;
import org.eclipse.jface.text.contentassist.ICompletionProposal;
import org.eclipse.jface.text.link.ILinkedModeListener;
import org.eclipse.jface.text.link.InclusivePositionUpdater;
import org.eclipse.jface.text.link.LinkedModeModel;
import org.eclipse.jface.text.link.LinkedModeUI;
import org.eclipse.jface.text.link.LinkedModeUI.ExitFlags;
import org.eclipse.jface.text.link.LinkedPosition;
import org.eclipse.jface.text.link.LinkedPositionGroup;
import org.eclipse.jface.text.link.ProposalPosition;

import org.eclipse.ui.IEditorPart;

import org.eclipse.ui.texteditor.link.EditorLinkedModeUI;

import org.eclipse.jdt.core.CompletionContext;
import org.eclipse.jdt.core.CompletionProposal;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.Signature;

import org.eclipse.jdt.internal.corext.template.java.SignatureUtil;

import org.eclipse.jdt.ui.text.IJavaPartitions;
import org.eclipse.jdt.ui.text.java.JavaContentAssistInvocationContext;

import org.eclipse.jdt.internal.ui.JavaPlugin;
import org.eclipse.jdt.internal.ui.javaeditor.EditorHighlightingSynchronizer;
import org.eclipse.jdt.internal.ui.javaeditor.JavaEditor;


/**
 * This is a {@link org.eclipse.jdt.internal.ui.text.java.JavaCompletionProposal} which includes templates
 * that represent the best guess completion for each parameter of a method.
 */
public class ParameterGuessingProposal extends JavaMethodCompletionProposal {

	/**
	 * Creates a {@link ParameterGuessingProposal} or <code>null</code> if the core context isn't available or extended.
	 *
	 * @param proposal the original completion proposal
	 * @param context the currrent context
	 * @param fillBestGuess if set, the best guess will be filled in
	 *
	 * @return a proposal or <code>null</code>
	 */
	public static ParameterGuessingProposal createProposal(CompletionProposal proposal, JavaContentAssistInvocationContext context, boolean fillBestGuess) {
		CompletionContext coreContext= context.getCoreContext();
 		if (coreContext != null && coreContext.isExtended()) {
			return new ParameterGuessingProposal(proposal, context, coreContext, fillBestGuess);
 		}
 		return null;
	}

	private ICompletionProposal[][] fChoices; // initialized by guessParameters()
	private Position[] fPositions; // initialized by guessParameters()

	private IRegion fSelectedRegion; // initialized by apply()
	private IPositionUpdater fUpdater;

	private final boolean fFillBestGuess;

	private final CompletionContext fCoreContext;

 	public ParameterGuessingProposal(CompletionProposal proposal, JavaContentAssistInvocationContext context, CompletionContext coreContext, boolean fillBestGuess) {
 		super(proposal, context);
		fCoreContext= coreContext;
		fFillBestGuess= fillBestGuess;
 	}

	private IJavaElement getEnclosingElement() {
		return fCoreContext.getEnclosingElement();
	}

	private IJavaElement[][] getAssignableElements() {
		char[] signature= SignatureUtil.fix83600(getProposal().getSignature());
		char[][] types= Signature.getParameterTypes(signature);

		IJavaElement[][] assignableElements= new IJavaElement[types.length][];
		for (int i= 0; i < types.length; i++) {
			assignableElements[i]= fCoreContext.getVisibleElements(new String(types[i]));
		}
		return assignableElements;
	}

	/*
	 * @see ICompletionProposalExtension#apply(IDocument, char)
	 */
	@Override
	public void apply(final IDocument document, char trigger, int offset) {
		try {
			super.apply(document, trigger, offset);

			int baseOffset= getReplacementOffset();
			String replacement= getReplacementString();

			if (fPositions != null && getTextViewer() != null) {

				LinkedModeModel model= new LinkedModeModel();

				for (int i= 0; i < fPositions.length; i++) {
					LinkedPositionGroup group= new LinkedPositionGroup();
					int positionOffset= fPositions[i].getOffset();
					int positionLength= fPositions[i].getLength();

					if (fChoices[i].length < 2) {
						group.addPosition(new LinkedPosition(document, positionOffset, positionLength, LinkedPositionGroup.NO_STOP));
					} else {
						ensurePositionCategoryInstalled(document, model);
						document.addPosition(getCategory(), fPositions[i]);
						group.addPosition(new ProposalPosition(document, positionOffset, positionLength, LinkedPositionGroup.NO_STOP, fChoices[i]));
					}
					model.addGroup(group);
				}

				model.forceInstall();
				JavaEditor editor= getJavaEditor();
				if (editor != null) {
					model.addLinkingListener(new EditorHighlightingSynchronizer(editor));
				}

				LinkedModeUI ui= new EditorLinkedModeUI(model, getTextViewer());
				ui.setExitPosition(getTextViewer(), baseOffset + replacement.length(), 0, Integer.MAX_VALUE);
				// exit character can be either ')' or ';'
				final char exitChar= replacement.charAt(replacement.length() - 1);
				ui.setExitPolicy(new ExitPolicy(exitChar, document) {
					@Override
					public ExitFlags doExit(LinkedModeModel model2, VerifyEvent event, int offset2, int length) {
						try {
							switch (event.character) {
							case ',':
								for (int i= 0; i < fPositions.length - 1; i++) { // not for the last one
									Position position= fPositions[i];
									if (position.offset <= offset2 && offset2 + length <= position.offset + position.length) {
										ITypedRegion partition= TextUtilities.getPartition(document, IJavaPartitions.JAVA_PARTITIONING, offset2 + length, false);
										if (IDocument.DEFAULT_CONTENT_TYPE.equals(partition.getType())
												|| offset2 + length == partition.getOffset() + partition.getLength()) {
											event.character= '\t';
											event.keyCode= SWT.TAB;
											return null;
										}
									}
								}
								break;

							case ')':
								if (!canExitLinkedMode(ui, document, offset2, length)) {
									return null;
								}
								if (exitChar != ')') {
									// exit from link mode when user is in the last ')' position.
									Position position= fPositions[fPositions.length - 1];
									if (position.offset <= offset2 && offset2 + length <= position.offset + position.length) {
										return new ExitFlags(ILinkedModeListener.UPDATE_CARET, false);
									}
								}
								break;

							case ';':
								ITypedRegion partition= TextUtilities.getPartition(document, IJavaPartitions.JAVA_PARTITIONING, offset2 + length, false);
								if (!IDocument.DEFAULT_CONTENT_TYPE.equals(partition.getType())) {
									return null;
								}
								break;

							default:
								break;
							}
						} catch (BadLocationException e) {
							// continue; not serious enough to log
						}
						return super.doExit(model2, event, offset2, length);
					}
				});
				ui.setCyclingMode(LinkedModeUI.CYCLE_WHEN_NO_PARENT);
				ui.setDoContextInfo(true);
				ui.enter();
				fSelectedRegion= ui.getSelectedRegion();

			} else {
				fSelectedRegion= new Region(baseOffset + replacement.length(), 0);
			}

		} catch (BadLocationException | BadPositionCategoryException e) {
			ensurePositionCategoryRemoved(document);
			JavaPlugin.log(e);
			openErrorDialog(e);
		}
	}

	private boolean canExitLinkedMode(LinkedModeUI ui, IDocument document, int offset, int length) throws BadLocationException {
		int selectionEnd= offset + length;
		ITypedRegion partition= TextUtilities.getPartition(document, IJavaPartitions.JAVA_PARTITIONING, selectionEnd, false);
		if (!IDocument.DEFAULT_CONTENT_TYPE.equals(partition.getType())) {
			return false;
		}

		IRegion argumentRegion= ui.getSelectedRegion();

		int openParens= 0;
		int pos= argumentRegion.getOffset();
		while (pos < selectionEnd) {
			ITypedRegion p= TextUtilities.getPartition(document, IJavaPartitions.JAVA_PARTITIONING, pos, false);
			int end= p.getOffset() + p.getLength();
			if (IDocument.DEFAULT_CONTENT_TYPE.equals(p.getType())) { // don't count parenthesis in strings, comments, etc.
				String argumentTextFromPartition= document.get(pos, Math.min(selectionEnd, end) - pos);
				for (int i= 0; i < argumentTextFromPartition.length(); i++) {
					char c= argumentTextFromPartition.charAt(i);
					if (c == '(') {
						openParens++;
					} else if (c == ')') {
						openParens--;
					}
				}
			}

			pos= end;
		}

		if (openParens > 0) {
			return false;
		}
		return true;
	}

	/*
	 * @see org.eclipse.jdt.internal.ui.text.java.JavaMethodCompletionProposal#needsLinkedMode()
	 */
	@Override
	protected boolean needsLinkedMode() {
		return false; // we handle it ourselves
	}

	/*
	 * @see org.eclipse.jdt.internal.ui.text.java.JavaMethodCompletionProposal#computeReplacementString()
	 */
	@Override
	protected String computeReplacementString() {

		if (!hasParameters() || !hasArgumentList())
			return super.computeReplacementString();

		long millis= JavaPlugin.DEBUG_RESULT_COLLECTOR ? System.currentTimeMillis() : 0;
		String replacement;
		try {
			replacement= computeGuessingCompletion();
		} catch (JavaModelException x) {
			fPositions= null;
			fChoices= null;
			JavaPlugin.log(x);
			openErrorDialog(x);
			return super.computeReplacementString();
		}
		if (JavaPlugin.DEBUG_RESULT_COLLECTOR) System.err.println("Parameter Guessing: " + (System.currentTimeMillis() - millis)); //$NON-NLS-1$

		return replacement;
	}

	/**
	 * Creates the completion string. Offsets and Lengths are set to the offsets and lengths of the
	 * parameters.
	 *
	 * @return the completion string
	 * @throws JavaModelException if parameter guessing failed
	 */
	private String computeGuessingCompletion() throws JavaModelException {

		StringBuffer buffer= new StringBuffer();
		appendMethodNameReplacement(buffer);

		FormatterPrefs prefs= getFormatterPrefs();

		setCursorPosition(buffer.length());

		if (prefs.afterOpeningParen)
			buffer.append(SPACE);

		char[][] parameterNames= fProposal.findParameterNames(null);

		fChoices= guessParameters(parameterNames);
		int count= fChoices.length;
		int replacementOffset= getReplacementOffset();

		for (int i= 0; i < count; i++) {
			if (i != 0) {
				if (prefs.beforeComma)
					buffer.append(SPACE);
				buffer.append(COMMA);
				if (prefs.afterComma)
					buffer.append(SPACE);
			}

			ICompletionProposal proposal= fChoices[i][0];
			String argument= proposal.getDisplayString();

			Position position= fPositions[i];
			position.setOffset(replacementOffset + buffer.length());
			position.setLength(argument.length());

			if (proposal instanceof JavaCompletionProposal) // handle the "unknown" case where we only insert a proposal.
				((JavaCompletionProposal) proposal).setReplacementOffset(replacementOffset + buffer.length());
			buffer.append(argument);
		}

		if (prefs.beforeClosingParen)
			buffer.append(SPACE);

		buffer.append(RPAREN);

		if (canAutomaticallyAppendSemicolon())
			buffer.append(SEMICOLON);

		return buffer.toString();
	}

	/**
	 * Returns the currently active java editor, or <code>null</code> if it
	 * cannot be determined.
	 *
	 * @return the currently active java editor, or <code>null</code>
	 */
	private JavaEditor getJavaEditor() {
		IEditorPart part= JavaPlugin.getActivePage().getActiveEditor();
		if (part instanceof JavaEditor)
			return (JavaEditor) part;
		else
			return null;
	}

	private ICompletionProposal[][] guessParameters(char[][] parameterNames) throws JavaModelException {
		// find matches in reverse order.  Do this because people tend to declare the variable meant for the last
		// parameter last.  That is, local variables for the last parameter in the method completion are more
		// likely to be closer to the point of code completion. As an example consider a "delegation" completion:
		//
		// 		public void myMethod(int param1, int param2, int param3) {
		// 			someOtherObject.yourMethod(param1, param2, param3);
		//		}
		//
		// The other consideration is giving preference to variables that have not previously been used in this
		// code completion (which avoids "someOtherObject.yourMethod(param1, param1, param1)";

		int count= parameterNames.length;
		fPositions= new Position[count];
		fChoices= new ICompletionProposal[count][];

		String[] parameterTypes= getParameterTypes();
		ParameterGuesser guesser= new ParameterGuesser(getEnclosingElement());
		IJavaElement[][] assignableElements= getAssignableElements();

		for (int i= count - 1; i >= 0; i--) {
			String paramName= new String(parameterNames[i]);
			Position position= new Position(0,0);

			boolean isLastParameter= i == count - 1;
			ICompletionProposal[] argumentProposals= guesser.parameterProposals(parameterTypes[i], paramName, position, assignableElements[i], fFillBestGuess, isLastParameter);
			if (argumentProposals.length == 0) {
				JavaCompletionProposal proposal= new JavaCompletionProposal(paramName, 0, paramName.length(), null, paramName, 0);
				if (isLastParameter)
					proposal.setTriggerCharacters(new char[] { ',' });
				argumentProposals= new ICompletionProposal[] { proposal };
			}

			fPositions[i]= position;
			fChoices[i]= argumentProposals;
		}

		return fChoices;
	}

	private String[] getParameterTypes() {
		char[] signature= SignatureUtil.fix83600(fProposal.getSignature());
		char[][] types= Signature.getParameterTypes(signature);

		String[] ret= new String[types.length];
		for (int i= 0; i < types.length; i++) {
			ret[i]= new String(Signature.toCharArray(types[i]));
		}
		return ret;
	}

	/*
	 * @see ICompletionProposal#getSelection(IDocument)
	 */
	@Override
	public Point getSelection(IDocument document) {
		if (fSelectedRegion == null)
			return new Point(getReplacementOffset(), 0);

		return new Point(fSelectedRegion.getOffset(), fSelectedRegion.getLength());
	}

	private void openErrorDialog(Exception e) {
		Shell shell= getTextViewer().getTextWidget().getShell();
		MessageDialog.openError(shell, JavaTextMessages.ParameterGuessingProposal_error_msg, e.getMessage());
	}

	private void ensurePositionCategoryInstalled(final IDocument document, LinkedModeModel model) {
		if (!document.containsPositionCategory(getCategory())) {
			document.addPositionCategory(getCategory());
			fUpdater= new InclusivePositionUpdater(getCategory());
			document.addPositionUpdater(fUpdater);

			model.addLinkingListener(new ILinkedModeListener() {

				/*
				 * @see org.eclipse.jface.text.link.ILinkedModeListener#left(org.eclipse.jface.text.link.LinkedModeModel, int)
				 */
				@Override
				public void left(LinkedModeModel environment, int flags) {
					ensurePositionCategoryRemoved(document);
				}

				@Override
				public void suspend(LinkedModeModel environment) {}
				@Override
				public void resume(LinkedModeModel environment, int flags) {}
			});
		}
	}

	private void ensurePositionCategoryRemoved(IDocument document) {
		if (document.containsPositionCategory(getCategory())) {
			try {
				document.removePositionCategory(getCategory());
			} catch (BadPositionCategoryException e) {
				// ignore
			}
			document.removePositionUpdater(fUpdater);
		}
	}

	private String getCategory() {
		return "ParameterGuessingProposal_" + toString(); //$NON-NLS-1$
	}

}
