/*******************************************************************************
 * Copyright (c) 2005, 2021 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
 *     Tom Eicher <eclipse@tom.eicher.name> - [content assist] prefix complete casted method proposals - https://bugs.eclipse.org/bugs/show_bug.cgi?id=247547
 *******************************************************************************/
package org.eclipse.jdt.internal.ui.text.java;

import org.eclipse.osgi.util.TextProcessor;

import org.eclipse.jface.preference.IPreferenceStore;

import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.TextUtilities;
import org.eclipse.jface.text.contentassist.IContextInformation;

import org.eclipse.jdt.core.CompletionProposal;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.ILocalVariable;
import org.eclipse.jdt.core.IMember;
import org.eclipse.jdt.core.ISourceRange;
import org.eclipse.jdt.core.ISourceReference;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.Signature;
import org.eclipse.jdt.core.compiler.CharOperation;
import org.eclipse.jdt.core.formatter.CodeFormatter;

import org.eclipse.jdt.internal.corext.util.CodeFormatterUtil;

import org.eclipse.jdt.ui.JavaElementLabels;
import org.eclipse.jdt.ui.PreferenceConstants;
import org.eclipse.jdt.ui.text.java.JavaContentAssistInvocationContext;

import org.eclipse.jdt.internal.ui.JavaPlugin;
import org.eclipse.jdt.internal.ui.preferences.formatter.FormatterProfileManager;


public class JavaMethodCompletionProposal extends LazyJavaCompletionProposal {
	/** Triggers for method proposals without parameters. Do not modify. */
	protected final static char[] METHOD_TRIGGERS= new char[] { ';', ',', '.', '\t', '[' };
	/** Triggers for method proposals. Do not modify. */
	protected final static char[] METHOD_WITH_ARGUMENTS_TRIGGERS= new char[] { ';', '(', '-', ' ' };
	/** Triggers for method name proposals (static imports). Do not modify. */
	protected final static char[] METHOD_NAME_TRIGGERS= new char[] { ';' };

	private boolean fHasParameters;
	private boolean fHasParametersComputed= false;
	private FormatterPrefs fFormatterPrefs;

	public JavaMethodCompletionProposal(CompletionProposal proposal, JavaContentAssistInvocationContext context) {
		super(proposal, context);
	}

	@Override
	public void apply(IDocument document, char trigger, int offset) {
		if (trigger == ' ' || trigger == '(')
			trigger= '\0';
		super.apply(document, trigger, offset);
		if (needsLinkedMode()) {
			setUpLinkedMode(document, ')');
		} else if (!fProposal.isConstructor() && getReplacementString().endsWith(";")) { //$NON-NLS-1$
			setUpLinkedMode(document, ';');
		}
	}

	protected boolean needsLinkedMode() {
		return hasArgumentList() && hasParameters();
	}

	@Override
	public int getPrefixCompletionStart(IDocument document, int completionOffset) {
		if (fProposal.getKind() == CompletionProposal.METHOD_REF_WITH_CASTED_RECEIVER) {
			return fProposal.getTokenStart();
		} else if (fProposal.getKind() == CompletionProposal.CONSTRUCTOR_INVOCATION)
			return fProposal.getRequiredProposals()[0].getReplaceStart();
		return super.getPrefixCompletionStart(document, completionOffset);
	}

	@Override
	public CharSequence getPrefixCompletionText(IDocument document, int completionOffset) {
		if (hasArgumentList() || fProposal.getKind() == CompletionProposal.CONSTRUCTOR_INVOCATION) {
			String completion= String.valueOf(fProposal.getName());
			if (isCamelCaseMatching()) {
				String prefix= getPrefix(document, completionOffset);
				return getCamelCaseCompound(prefix, completion);
			}
			return completion;
		}
		return super.getPrefixCompletionText(document, completionOffset);
	}

	@Override
	protected IContextInformation computeContextInformation() {
		// no context information for METHOD_NAME_REF proposals (e.g. for static imports)
		// https://bugs.eclipse.org/bugs/show_bug.cgi?id=94654
		if ((fProposal.getKind() == CompletionProposal.METHOD_REF || fProposal.getKind() == CompletionProposal.CONSTRUCTOR_INVOCATION) && hasParameters()
				&& (getReplacementString().endsWith(RPAREN) || getReplacementString().endsWith(SEMICOLON) || getReplacementString().length() == 0)) {
			ProposalContextInformation contextInformation= new ProposalContextInformation(fProposal);
			if (fContextInformationPosition != 0 && fProposal.getCompletion().length == 0)
				contextInformation.setContextInformationPosition(fContextInformationPosition);
			return contextInformation;
		}
		return super.computeContextInformation();
	}

	@Override
	protected char[] computeTriggerCharacters() {
		if (fProposal.getKind() == CompletionProposal.METHOD_NAME_REFERENCE)
			return METHOD_NAME_TRIGGERS;
		if (hasParameters())
			return METHOD_WITH_ARGUMENTS_TRIGGERS;
		return METHOD_TRIGGERS;
	}

	/**
	 * Returns <code>true</code> if the method being inserted has at least one parameter. Note
	 * that this does not say anything about whether the argument list should be inserted. This
	 * depends on the position in the document and the kind of proposal; see
	 * {@link #hasArgumentList() }.
	 *
	 * @return <code>true</code> if the method has any parameters, <code>false</code> if it has
	 *         no parameters
	 */
	protected final boolean hasParameters() {
		if (!fHasParametersComputed) {
			fHasParametersComputed= true;
			fHasParameters= computeHasParameters();
		}
		return fHasParameters;
	}

	public enum State {
		NORMAL, SEEN_SLASH, IN_LINE_COMMENT, IN_BLOCK_COMMENT, SEEN_ASTERISK
	}

	/**
	 * Returns whether we automatically complete the method with a semicolon.
	 *
	 * @return <code>true</code> if the return type of the method is void, <code>false</code>
	 *         otherwise
	 * @since 3.9
	 */
	protected final boolean canAutomaticallyAppendSemicolon() {
		if (!fProposal.isConstructor() && CharOperation.equals(new char[] { Signature.C_VOID }, Signature.getReturnType(fProposal.getSignature()))) {
			try {
				// don't want to add semicolon if we are completing a method parameter
				ICompilationUnit cu= fInvocationContext.getCompilationUnit();
				if (cu != null) {
					IJavaElement element= cu.getElementAt(getReplacementOffset());
					if (element instanceof IMember) {
						// check if parentheses' are matched
						return areParenthesesMatched(cu, (IMember)element);
					} else if (element instanceof ILocalVariable) {
						ILocalVariable localVar= (ILocalVariable)element;
						return !localVar.isParameter();
					}
				}
			} catch (JavaModelException e) {
				return false;
			}
			return true;
		}
		return false;
	}

	// Simple member parser to see if we are not in matched parentheses and thus might be in a parameter list
	private boolean areParenthesesMatched(ICompilationUnit cu, IMember element) throws JavaModelException {
		String source= cu.getSource();
		ISourceRange sourceRange= ((ISourceReference)element).getSourceRange();
		int cnt= 0;
		State state= State.NORMAL;
		for (int i= sourceRange.getOffset(); i < getReplacementOffset(); ++i) {
			char c= source.charAt(i);
			switch (c) {
				case '/':
					switch (state) {
						case SEEN_SLASH:
							state= State.IN_LINE_COMMENT;
							break;
						case NORMAL:
							state= State.SEEN_SLASH;
							break;
						case SEEN_ASTERISK:
							state= State.NORMAL;
							break;
						case IN_BLOCK_COMMENT:
						case IN_LINE_COMMENT:
						default:
							// no change
							break;
					}
					break;
				case '*':
					switch (state) {
						case SEEN_SLASH:
							state= State.IN_BLOCK_COMMENT;
							break;
						case IN_BLOCK_COMMENT:
							state= State.SEEN_ASTERISK;
							break;
						case SEEN_ASTERISK:
						case NORMAL:
						case IN_LINE_COMMENT:
						default:
							// no change
							break;
					}
					break;
				case '(':
				case ')':
					switch (state) {
						case SEEN_SLASH:
						case SEEN_ASTERISK:
							state= State.NORMAL;
							break;
						case NORMAL:
							if (c == '(') {
								++cnt;
							} else {
								--cnt;
							}
							break;
						case IN_BLOCK_COMMENT:
						case IN_LINE_COMMENT:
						default:
							// no change
							break;
					}
					break;
				case '\n':
					switch (state) {
						case SEEN_SLASH:
						case IN_LINE_COMMENT:
							state= State.NORMAL;
							break;
						case SEEN_ASTERISK:
							state= State.IN_BLOCK_COMMENT;
							break;
						case IN_BLOCK_COMMENT:
						case NORMAL:
						default:
							// no change
							break;
					}
					break;
				default:
					// do nothing
					break;
			}
		}
		return cnt == 0;
	}

	private boolean computeHasParameters() throws IllegalArgumentException {
		return Signature.getParameterCount(fProposal.getSignature()) > 0;
	}

	/**
	 * Returns <code>true</code> if the argument list should be inserted by the proposal,
	 * <code>false</code> if not.
	 *
	 * @return <code>true</code> when the proposal is not in javadoc nor within an import and comprises the
	 *         parameter list
	 */
	protected boolean hasArgumentList() {
		if (CompletionProposal.METHOD_NAME_REFERENCE == fProposal.getKind())
			return false;
		IPreferenceStore preferenceStore= JavaPlugin.getDefault().getPreferenceStore();
		boolean noOverwrite= preferenceStore.getBoolean(PreferenceConstants.CODEASSIST_INSERT_COMPLETION) ^ isToggleEating();
		char[] completion= fProposal.getCompletion();
		return !isInJavadoc() && completion.length > 0 && (noOverwrite  || completion[completion.length - 1] == ')');
	}

	/**
	 * Returns the method formatter preferences.
	 *
	 * @return the formatter settings
	 */
	@Override
	protected final FormatterPrefs getFormatterPrefs() {
		if (fFormatterPrefs == null)
			fFormatterPrefs= new FormatterPrefs(fInvocationContext.getProject());
		return fFormatterPrefs;
	}

	/*
	 * @see org.eclipse.jdt.internal.ui.text.java.LazyJavaCompletionProposal#computeReplacementString()
	 */
	@Override
	protected String computeReplacementString() {
		if (!hasArgumentList())
			return super.computeReplacementString();

		// we're inserting a method plus the argument list - respect formatter preferences
		StringBuffer buffer= new StringBuffer();
		appendMethodNameReplacement(buffer);

		FormatterPrefs prefs= getFormatterPrefs();

		if (hasParameters()) {
			setCursorPosition(buffer.length());

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

			// don't add the trailing space, but let the user type it in himself - typing the closing paren will exit
//			if (prefs.beforeClosingParen)
//				buffer.append(SPACE);
		} else {
			if (prefs.inEmptyList)
				buffer.append(SPACE);
		}

		buffer.append(RPAREN);

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

		return buffer.toString();

	}

	/**
	 * Appends everything up to the method name including
	 * the opening parenthesis.
	 * <p>
	 * In case of {@link CompletionProposal#METHOD_REF_WITH_CASTED_RECEIVER}
	 * it add cast.
	 * </p>
	 *
	 * @param buffer the string buffer
	 * @since 3.4
	 */
	protected void appendMethodNameReplacement(StringBuffer buffer) {
		if (fProposal.getKind() == CompletionProposal.METHOD_REF_WITH_CASTED_RECEIVER) {
			String coreCompletion= String.valueOf(fProposal.getCompletion());
			String lineDelimiter= TextUtilities.getDefaultLineDelimiter(getTextViewer().getDocument());
			String replacement= CodeFormatterUtil.format(CodeFormatter.K_EXPRESSION, coreCompletion, 0, lineDelimiter, FormatterProfileManager.getProjectSettings(fInvocationContext.getProject()));
			buffer.append(replacement.substring(0, replacement.lastIndexOf('.') + 1));
		}

		if (fProposal.getKind() != CompletionProposal.CONSTRUCTOR_INVOCATION)
			buffer.append(fProposal.getName());

		FormatterPrefs prefs= getFormatterPrefs();
		if (prefs.beforeOpeningParen)
			buffer.append(SPACE);
		buffer.append(LPAREN);
	}

	@Override
	protected ProposalInfo computeProposalInfo() {
		IJavaProject project= fInvocationContext.getProject();
		if (project != null)
			return new MethodProposalInfo(project, fProposal);
		return super.computeProposalInfo();
	}

	/*
	 * @see org.eclipse.jdt.internal.ui.text.java.LazyJavaCompletionProposal#computeSortString()
	 */
	@Override
	protected String computeSortString() {
		/*
		 * Lexicographical sort order:
		 * 1) by relevance (done by the proposal sorter)
		 * 2) by method name
		 * 3) by parameter count
		 * 4) by parameter type names
		 */
		char[] name= fProposal.getName();
		char[] parameterList= Signature.toCharArray(fProposal.getSignature(), null, null, false, false);
		int parameterCount= Signature.getParameterCount(fProposal.getSignature()) % 10; // we don't care about insane methods with >9 parameters
		StringBuilder buf= new StringBuilder(name.length + 2 + parameterList.length);

		buf.append(name);
		buf.append('\0'); // separator
		buf.append(parameterCount);
		buf.append(parameterList);
		return buf.toString();
	}

	/*
	 * @see org.eclipse.jdt.internal.ui.text.java.AbstractJavaCompletionProposal#isOffsetValid(int)
	 * @since 3.5
	 */
	@Override
	protected boolean isOffsetValid(int offset) {
		if (fProposal.getKind() != CompletionProposal.CONSTRUCTOR_INVOCATION)
			return super.isOffsetValid(offset);

		return fProposal.getRequiredProposals()[0].getReplaceStart() <= offset;
	}

	/*
	 * @see org.eclipse.jdt.internal.ui.text.java.AbstractJavaCompletionProposal#isValidPrefix(java.lang.String)
	 */
	@Override
	protected boolean isValidPrefix(String prefix) {
		if (super.isValidPrefix(prefix))
			return true;

		String word= TextProcessor.deprocess(getDisplayString());
		if (fProposal.getKind() == CompletionProposal.CONSTRUCTOR_INVOCATION) {
			int start= word.indexOf(JavaElementLabels.CONCAT_STRING) + JavaElementLabels.CONCAT_STRING.length();
			word= word.substring(start);
			return isPrefix(prefix, word) || isPrefix(prefix, new String(fProposal.getName()));
		}

		if (isInJavadoc()) {
			int idx = word.indexOf("{@link "); //$NON-NLS-1$
			if (idx==0) {
				word = word.substring(7);
			} else {
				idx = word.indexOf("{@value "); //$NON-NLS-1$
				if (idx==0) {
					word = word.substring(8);
				}
			}
		}
		return isPrefix(prefix, word);
	}

	/*
	 * @see org.eclipse.jdt.internal.ui.text.java.AbstractJavaCompletionProposal#isPrefix(java.lang.String, java.lang.String)
	 * @since 3.4
	 */
	@Override
	protected boolean isPrefix(String prefix, String string) {
		if (fProposal.getKind() == CompletionProposal.METHOD_REF_WITH_CASTED_RECEIVER && prefix != null)
			prefix= prefix.substring(fProposal.getReceiverEnd() - fProposal.getReceiverStart() + 1);
		return super.isPrefix(prefix, string);
	}

	/*
	 * @see org.eclipse.jdt.internal.ui.text.java.AbstractJavaCompletionProposal#getPrefix(org.eclipse.jface.text.IDocument, int)
	 * @since 3.5
	 */
	@Override
	protected String getPrefix(IDocument document, int offset) {
		if (fProposal.getKind() != CompletionProposal.CONSTRUCTOR_INVOCATION)
			return super.getPrefix(document, offset);

		int replacementOffset= fProposal.getRequiredProposals()[0].getReplaceStart();

		try {
			int length= offset - replacementOffset;
			if (length > 0)
				return document.get(replacementOffset, length);
		} catch (BadLocationException x) {
		}
		return ""; //$NON-NLS-1$

	}

	/*
	 * @see org.eclipse.jdt.internal.ui.text.java.AbstractJavaCompletionProposal#createRequiredTypeCompletionProposal(org.eclipse.jdt.core.CompletionProposal, org.eclipse.jdt.ui.text.java.JavaContentAssistInvocationContext)
	 * @since 3.7
	 */
	@Override
	protected LazyJavaCompletionProposal createRequiredTypeCompletionProposal(CompletionProposal completionProposal, JavaContentAssistInvocationContext invocationContext) {
		LazyJavaCompletionProposal requiredProposal= super.createRequiredTypeCompletionProposal(completionProposal, invocationContext);
		if (fProposal.getKind() == CompletionProposal.CONSTRUCTOR_INVOCATION && requiredProposal instanceof LazyGenericTypeProposal)
			((LazyGenericTypeProposal) requiredProposal).canUseDiamond(fProposal.canUseDiamond(fInvocationContext.getCoreContext()));
		return requiredProposal;
	}

}
